package server

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/agent_registrar/agentk_rpc"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/agent_tracker"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/event_tracker"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/server_api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/entity/agentk"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type RegisterAgentkEvent struct {
	ProjectID          int64             `json:"project_id"`
	AgentVersion       string            `json:"agent_version"`
	Architecture       string            `json:"architecture"`
	ConnectionID       int64             `json:"-"`
	AgentID            int64             `json:"agent_id"`
	KubernetesVersion  string            `json:"kubernetes_version"`
	ExtraTelemetryData map[string]string `json:"extra_telemetry_data"`
}

func (e *RegisterAgentkEvent) DeduplicateKey() string {
	return strconv.FormatInt(e.ConnectionID, 10)
}

type agentkServer struct {
	agentk_rpc.UnsafeAgentRegistrarServer
	agentRegisterer           agent_tracker.ExpiringRegisterer
	registerAgentEventTracker event_tracker.EventsInterface
}

func (s *agentkServer) Register(ctx context.Context, req *agentk_rpc.RegisterRequest) (*agentk_rpc.RegisterResponse, error) { //nolint:dupl
	rpcAPI := modserver.AgentRPCAPIFromContext(ctx)
	log := rpcAPI.Log()

	ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), registerTimeout) // we got the request, let's register even if the client cancels.
	defer cancel()

	// Get agent info
	a, err := rpcAPI.AgentInfo(ctx, log)
	if err != nil {
		return nil, err
	}

	err = s.registerAgentk(ctx, req, a, req.Meta)
	if err != nil {
		return nil, err
	}
	return &agentk_rpc.RegisterResponse{}, nil
}

func (s *agentkServer) registerAgentk(ctx context.Context, req *agentk_rpc.RegisterRequest, a server_api.AgentInfo, meta *agentk.Meta) error {
	agentInfo, ok := a.(*server_api.AgentkInfo)
	if !ok {
		return status.Error(codes.InvalidArgument, "expected agentk details to register agent")
	}

	// If GitRef is not set, use CommitId for backward compatibility
	if meta.GitRef == "" { //nolint:staticcheck,nolintlint
		meta.GitRef = meta.CommitId //nolint:staticcheck,nolintlint
	}

	connectedAgentInfo := &agent_tracker.ConnectedAgentkInfo{
		AgentMeta:    meta,
		ConnectedAt:  timestamppb.Now(),
		ConnectionId: req.InstanceId,
		AgentId:      agentInfo.Key.ID,
		ProjectId:    agentInfo.ProjectID,
	}

	// Register agent
	err := s.agentRegisterer.RegisterAgentkExpiring(ctx, connectedAgentInfo)
	if err != nil {
		rpcAPI := modserver.AgentRPCAPIFromContext(ctx)
		rpcAPI.HandleProcessingError(rpcAPI.Log(), "Failed to register agent", err, logz.AgentKey(agentInfo.Key))
		return status.Error(codes.Unavailable, "failed to register agent")
	}

	event := &RegisterAgentkEvent{
		ProjectID:          agentInfo.ProjectID,
		AgentVersion:       meta.Version,
		Architecture:       extractArch(meta.KubernetesVersion.Platform),
		ConnectionID:       req.InstanceId,
		AgentID:            agentInfo.Key.ID,
		KubernetesVersion:  fmt.Sprintf("%s.%s", meta.KubernetesVersion.Major, meta.KubernetesVersion.Minor),
		ExtraTelemetryData: meta.ExtraTelemetryData,
	}
	s.registerAgentEventTracker.EmitEvent(event)
	return nil
}

func (s *agentkServer) Unregister(ctx context.Context, req *agentk_rpc.UnregisterRequest) (*agentk_rpc.UnregisterResponse, error) { //nolint:dupl
	rpcAPI := modserver.AgentRPCAPIFromContext(ctx)
	log := rpcAPI.Log()

	ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), registerTimeout) // we got the request, let's unregister even if the client cancels.
	defer cancel()

	// Get agent info
	a, err := rpcAPI.AgentInfo(ctx, log)
	if err != nil {
		return nil, err
	}

	err = s.unregisterAgentk(ctx, req, a, req.Meta)
	if err != nil {
		return nil, err
	}

	return &agentk_rpc.UnregisterResponse{}, nil
}

func (s *agentkServer) unregisterAgentk(ctx context.Context, req *agentk_rpc.UnregisterRequest, a server_api.AgentInfo, meta *agentk.Meta) error {
	agentInfo, ok := a.(*server_api.AgentkInfo)
	if !ok {
		return status.Error(codes.InvalidArgument, "expected agentk details to unregister agent")
	}

	disconnectAgentInfo := &agent_tracker.DisconnectAgentkInfo{
		AgentMeta:    meta,
		ConnectionId: req.InstanceId,
		AgentId:      agentInfo.Key.ID,
		ProjectId:    agentInfo.ProjectID,
	}

	// Unregister agent
	err := s.agentRegisterer.UnregisterAgentk(ctx, disconnectAgentInfo)
	if err != nil {
		rpcAPI := modserver.AgentRPCAPIFromContext(ctx)
		rpcAPI.HandleProcessingError(rpcAPI.Log(), "Failed to unregister agent", err, logz.AgentKey(agentInfo.Key))
		return status.Error(codes.Unavailable, "failed to unregister agent")
	}
	return nil
}

func extractArch(platform string) string {
	_, arch, found := strings.Cut(platform, "/")
	if !found {
		return ""
	}
	return arch
}
