package kas

import (
	"context"
	"log/slog"
	"strconv"
	"strings"
	"sync"

	"github.com/getsentry/sentry-go"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modshared"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/featureflag"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/fieldz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/grpctool"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	otelmetric "go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/grpc/metadata"
)

const (
	// ffNamePrefix is the prefix used for KAS-scoped feature flags.
	ffNamePrefix = "kas-feature-"
)

type serverRPCAPI struct {
	modshared.RPCAPIStub
	sentryHubRoot *sentry.Hub

	service string
	method  string
	traceID trace.TraceID

	agentInfoResolver modserver.AgentInfoResolver

	sentryHubOnce sync.Once
	sentryHub     SentryHub
	transaction   string

	featureFlags featureflag.Set
}

func (a *serverRPCAPI) HandleProcessingError(log *slog.Logger, msg string, err error, fields ...fieldz.Field) {
	handleProcessingError(a.StreamCtx, a.hub, log, msg, err, a.agentInfoResolver, fields...)
}

func (a *serverRPCAPI) HandleIOError(log *slog.Logger, msg string, err error) error {
	// The problem is almost certainly with the client's connection.
	// Still log it on Debug.
	log.Debug(msg, logz.Error(err))
	return grpctool.HandleIOError(msg, err)
}

func (a *serverRPCAPI) hub() (SentryHub, string) {
	a.sentryHubOnce.Do(a.hubOnce)
	return a.sentryHub, a.transaction
}

func (a *serverRPCAPI) hubOnce() {
	hub := a.sentryHubRoot.Clone()
	scope := hub.Scope()
	scope.SetTag(modserver.GRPCServiceSentryField, a.service)
	scope.SetTag(modserver.GRPCMethodSentryField, a.method)
	a.transaction = a.service + "::" + a.method                            // Like in Gitaly
	scope.SetFingerprint([]string{"{{ default }}", "grpc", a.transaction}) // use Sentry's default error hash but also split by gRPC transaction
	if a.traceID.IsValid() {
		scope.SetTag(modserver.SentryFieldTraceID, a.traceID.String())
	}
	a.sentryHub = hub
}

func (a *serverRPCAPI) IsEnabled(ff *featureflag.FeatureFlag) bool {
	return a.featureFlags.IsEnabled(ff)
}

func (a *serverRPCAPI) IsDisabled(ff *featureflag.FeatureFlag) bool {
	return a.featureFlags.IsDisabled(ff)
}

type serverRPCAPIFactory struct {
	log               *slog.Logger
	sentryHub         *sentry.Hub
	agentInfoResolver modserver.AgentInfoResolver
	ffCheckCounter    otelmetric.Int64Counter
}

func (f *serverRPCAPIFactory) New(ctx context.Context, fullMethodName string) modshared.RPCAPI {
	service, method := grpctool.SplitGRPCMethod(fullMethodName)
	traceID := trace.SpanContextFromContext(ctx).TraceID()
	return &serverRPCAPI{
		RPCAPIStub: modshared.RPCAPIStub{
			Logger: f.log.With(
				logz.TraceID(traceID),
				logz.GRPCService(service),
				logz.GRPCMethod(method),
			),
			StreamCtx: ctx,
		},
		sentryHubRoot:     f.sentryHub,
		service:           service,
		method:            method,
		traceID:           traceID,
		agentInfoResolver: f.agentInfoResolver,
		featureFlags:      featureFlagsFromContext(ctx, f.ffCheckCounter),
	}
}

func featureFlagsFromContext(ctx context.Context, counter otelmetric.Int64Counter) featureflag.Set {
	md, _ := metadata.FromIncomingContext(ctx) // loop handles nil md

	var ffs map[string]bool
	for name, value := range md {
		if len(value) == 0 {
			continue
		}

		ffName, found := strings.CutPrefix(name, ffNamePrefix)
		if !found {
			continue
		}

		enabled, err := strconv.ParseBool(value[0])
		if err != nil {
			continue
		}

		if ffs == nil {
			ffs = make(map[string]bool)
		}
		ffs[strings.ReplaceAll(ffName, "-", "_")] = enabled
	}

	return featureflag.NewSet(ffs, counter)
}
