package modserver

import (
	"context"
	"log/slog"
	"net"
	"time"

	"buf.build/go/protovalidate"
	"github.com/redis/rueidis"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/event_tracker"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modshared"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/observability"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/usage_metrics"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/grpctool"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/version"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/event"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/kascfg"
	otelmetric "go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/trace/noop"
	"google.golang.org/grpc"
)

const (
	// SentryFieldTraceID is the name of the Sentry field for trace ID.
	SentryFieldTraceID      = "trace_id"
	SentryFieldTraceSampled = "trace_sampled"
	GRPCServiceSentryField  = "grpc.service"
	GRPCMethodSentryField   = "grpc.method"
)

// ApplyDefaults is a signature of a public function, exposed by modules to perform defaulting.
// The function should be called ApplyDefaults.
type ApplyDefaults func(*kascfg.ConfigurationFile)

// Config holds configuration for a Module.
type Config struct {
	// Log can be used for logging from the module.
	// It should not be used for logging from gRPC API methods. Use modshared.RPCAPIFromContext(ctx) instead.
	Log *slog.Logger
	// InstanceID can be used to distinguish kas replicas from each other.
	InstanceID   int64
	API          API
	Config       *kascfg.ConfigurationFile
	GitLabClient gitlab.ClientInterface
	UsageTracker usage_metrics.UsageTrackerRegisterer
	EventTracker event_tracker.EventTrackerRegisterer
	// AgentServer is the gRPC server agentk is talking to.
	// This can be used to add endpoints in Factory.New.
	// Request handlers can obtain the per-request logger using modshared.RPCAPIFromContext(requestContext).
	// Request handlers that block for a while must use context from
	// grpctool.MaxConnectionAgeContextFromStreamContext() or poll using rpcAPI.PollWithBackoff()
	// to respect RPC context abort, kas shutdown, max connection age reached conditions.
	AgentServer grpctool.GRPCServer
	// APIServer is the gRPC server GitLab is talking to.
	// This can be used to add endpoints in Factory.New.
	// Request handlers can obtain the per-request logger using modshared.RPCAPIFromContext(requestContext).
	// Request handlers that block for a while must use context from
	// grpctool.MaxConnectionAgeContextFromStreamContext() or poll using rpcAPI.PollWithBackoff()
	// to respect RPC context abort, kas shutdown, max connection age reached conditions.
	APIServer grpctool.GRPCServer
	// RegisterAgentAPI allows to register a gRPC API endpoint that kas proxies to agentk.
	RegisterAgentAPI func(*grpc.ServiceDesc)
	// AgentConnPool returns a gRPC connection that can be used to send requests to an agent instance.
	// Make sure module factory returns modshared.ModuleStartAfterServers if module uses this connection.
	AgentConnPool func(agentKey api.AgentKey) grpc.ClientConnInterface
	// ListenerMetrics wraps a named net.Listener to collect metrics.
	ListenerMetrics  func(l net.Listener, name string, ttl time.Duration, expectTLS bool) (net.Listener, error)
	Gitaly           gitaly.PoolInterface
	TraceProvider    trace.TracerProvider
	TracePropagator  propagation.TextMapPropagator
	MeterProvider    otelmetric.MeterProvider
	Meter            otelmetric.Meter
	StreamClientProm grpc.StreamClientInterceptor
	UnaryClientProm  grpc.UnaryClientInterceptor
	RedisClient      rueidis.Client
	// KASName is a string "gitlab-kas". Can be used as a user agent, server name, service name, etc.
	KASName string
	// KASName is a string "gitlab-kas/<version>/<git sha>". Can be used as a user agent, server name, service name, etc.
	KASNameVersion string
	// Version is the KAS version.
	Version version.Version
	// GitRef is the Git reference of the KAS build.
	GitRef string
	// ProbeRegistry is for registering liveness probes and readiness probes.
	ProbeRegistry *observability.ProbeRegistry
	// List of available GitLab Releases Information
	GitLabReleasesList []string
	Validator          protovalidate.Validator

	// Tracing enabled flags
	HTTPServerTracing bool
	HTTPClientTracing bool
	GRPCServerTracing bool
	GRPCClientTracing bool
}

func (c *Config) MaybeTraceProvider(flag bool) trace.TracerProvider {
	if flag {
		return c.TraceProvider
	}
	return noop.NewTracerProvider()
}

// API provides the API for the module to use.
type API interface {
	modshared.API
	// OnGitPushEvent subscribes the given callback function to receive git push events.
	// The git push event may come from any GitLab project and as such it's up to the
	// callback to filter out the events that it's interested in.
	// The callback MUST NOT block i.e. perform I/O or acquire contended locks. Perform those operations
	// asynchronously in a separate goroutine when required.
	// The callback is executed from a different goroutine, OnGitPushEvent() returns immediately after registering it.
	// The returned function can be used to unsubscribe from notifications.
	OnGitPushEvent(func(*event.GitPushEvent)) func()
}

type AgentInfoResolver interface {
	Get(ctx context.Context, agentKey api.AgentKey) (map[string]any, error)
}

type Factory interface {
	modshared.Factory
	// New creates a new instance of a Module.
	// Can return nil if no module is necessary.
	New(*Config) (Module, error)
}

type Module interface {
	// Run starts the module.
	// Run can block until the context is canceled or exit with nil if there is nothing to do.
	Run(context.Context) error
	// Name returns module's name.
	Name() string
}
