package server

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

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/agent_registrar/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/tool/fieldz"
	"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 RegisterAgentwEvent struct {
	AgentVersion       string            `json:"agent_version"`
	Architecture       string            `json:"architecture"`
	ConnectionID       int64             `json:"-"`
	AgentID            int64             `json:"agent_id"`
	ExtraTelemetryData map[string]string `json:"extra_telemetry_data"`
}

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

type server struct {
	rpc.UnsafeAgentRegistrarServer
	agentRegisterer           agent_tracker.ExpiringRegisterer
	registerAgentEventTracker event_tracker.EventsInterface
}

func (s *server) Register(ctx context.Context, req *rpc.RegisterRequest) (*rpc.RegisterResponse, error) {
	rpcAPI := modserver.AgentRPCAPIFromContext(ctx)
	log := rpcAPI.Log()

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

	switch meta := req.Meta.(type) {
	case *rpc.RegisterRequest_AgentkMeta:
		// If GitRef is not set, use CommitId for backward compatibility
		if meta.AgentkMeta.GitRef == "" { //nolint:staticcheck,nolintlint
			meta.AgentkMeta.GitRef = meta.AgentkMeta.CommitId //nolint:staticcheck,nolintlint
		}

		agentInfo, ok := a.(*api.AgentkInfo)
		if !ok {
			return nil, status.Error(codes.InvalidArgument, "expected agentk details to register agent")
		}

		connectedAgentInfo := &agent_tracker.ConnectedAgentkInfo{
			AgentMeta:    meta.AgentkMeta,
			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.HandleProcessingError(log, "Failed to register agent", err, fieldz.AgentKey(agentInfo.Key))
			return nil, status.Error(codes.Unavailable, "failed to register agent")
		}

		event := &RegisterAgentkEvent{
			ProjectID:          agentInfo.ProjectID,
			AgentVersion:       meta.AgentkMeta.Version,
			Architecture:       extractArch(meta.AgentkMeta.KubernetesVersion.Platform),
			ConnectionID:       req.InstanceId,
			AgentID:            agentInfo.Key.ID,
			KubernetesVersion:  fmt.Sprintf("%s.%s", meta.AgentkMeta.KubernetesVersion.Major, meta.AgentkMeta.KubernetesVersion.Minor),
			ExtraTelemetryData: meta.AgentkMeta.ExtraTelemetryData,
		}
		s.registerAgentEventTracker.EmitEvent(event)
	case *rpc.RegisterRequest_AgentwMeta:
		agentInfo, ok := a.(*api.AgentwInfo)
		if !ok {
			return nil, status.Error(codes.InvalidArgument, "expected agentw details to register agent")
		}

		connectedAgentInfo := &agent_tracker.ConnectedAgentwInfo{
			AgentMeta:    meta.AgentwMeta,
			ConnectedAt:  timestamppb.Now(),
			ConnectionId: req.InstanceId,
			WorkspaceId:  agentInfo.Key.ID,
		}

		// Register agent
		err = s.agentRegisterer.RegisterAgentwExpiring(ctx, connectedAgentInfo)
		if err != nil {
			rpcAPI.HandleProcessingError(log, "Failed to register agent", err, fieldz.AgentKey(agentInfo.Key))
			return nil, status.Error(codes.Unavailable, "failed to register agent")
		}

		event := &RegisterAgentwEvent{
			AgentVersion:       meta.AgentwMeta.Version,
			Architecture:       meta.AgentwMeta.Architecture,
			ConnectionID:       req.InstanceId,
			AgentID:            agentInfo.Key.ID,
			ExtraTelemetryData: meta.AgentwMeta.ExtraTelemetryData,
		}
		s.registerAgentEventTracker.EmitEvent(event)
	default:
		return nil, status.Error(codes.Internal, fmt.Sprintf("unknown meta type: %T", req.Meta))
	}
	return &rpc.RegisterResponse{}, nil
}

func (s *server) Unregister(ctx context.Context, req *rpc.UnregisterRequest) (*rpc.UnregisterResponse, error) {
	rpcAPI := modserver.AgentRPCAPIFromContext(ctx)
	log := rpcAPI.Log()

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

	switch meta := req.Meta.(type) {
	case *rpc.UnregisterRequest_AgentkMeta:
		agentInfo, ok := a.(*api.AgentkInfo)
		if !ok {
			return nil, status.Error(codes.InvalidArgument, "expected agentk details to unregister agent")
		}

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

		// Unregister agent
		err = s.agentRegisterer.UnregisterAgentk(ctx, disconnectAgentInfo)
		if err != nil {
			rpcAPI.HandleProcessingError(log, "Failed to unregister agent", err, fieldz.AgentKey(agentInfo.Key))
			return nil, status.Error(codes.Unavailable, "failed to unregister agent")
		}
	case *rpc.UnregisterRequest_AgentwMeta:
		// TODO
	default:
		return nil, status.Error(codes.Internal, fmt.Sprintf("unknown meta type: %T", req.Meta))
	}

	return &rpc.UnregisterResponse{}, nil
}

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