package agentw

import (
	"context"
	"fmt"
	"math/rand/v2"
	"net/url"
	"runtime"

	"github.com/ash2k/stager"
	"github.com/go-logr/logr"
	"github.com/spf13/cobra"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/agentwcfg"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/cmd"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/cmd/agent"
	agent2kas_tunnel_agent "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/agent2kas_tunnel/agent"
	agent2kas_tunnel_agentw "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/agent2kas_tunnel/agentw"
	agent_registrar_agentw "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/agent_registrar/agentw"
	gitlab_access_rpc "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/gitlab_access/rpc"
	google_profiler_agentw "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/google_profiler/agentw"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modagent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modagentw"
	observability_agent "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/observability/agent"
	observability_agentw "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/observability/agentw"
	workspaces_agent "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/workspaces/agent"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/errz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/grpctool"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/metric"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/syncz"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/entity"
	"go.opentelemetry.io/otel"
	"google.golang.org/grpc/grpclog"
)

const (
	defaultLogLevel     agentwcfg.LogLevelEnum = 0 // whatever is 0 is the default value
	defaultGRPCLogLevel                        = agentwcfg.LogLevelEnum_error

	envVarAgentToken = "AGENTW_TOKEN"
)

type options struct {
	agent.Options

	agentMeta *entity.AgentwMeta
}

func newOptions() *options {
	return &options{
		Options: agent.Options{
			AgentKey:                   syncz.NewValueHolder[api.AgentKey](),
			GitLabExternalURL:          syncz.NewValueHolder[url.URL](),
			ObservabilityListenNetwork: agent.DefaultObservabilityListenNetwork,
			ObservabilityListenAddress: agent.DefaultObservabilityListenAddress,
		},
		agentMeta: &entity.AgentwMeta{
			Version:            cmd.Version,
			GitRef:             cmd.GitRef,
			ExtraTelemetryData: make(map[string]string),
			Architecture:       runtime.GOARCH,
		},
	}
}

// setGlobals shouldn't exist, yet here we are.
func (o *options) setGlobals() {
	grpclog.SetLoggerV2(&grpctool.Logger{Handler: o.GRPCHandler}) // pipe gRPC logs into slog
	logrLogger := logr.FromSlogHandler(o.Log.Handler())
	otel.SetLogger(logrLogger)
	otel.SetErrorHandler((*metric.OtelErrorHandler)(o.Log))
}

func (o *options) run(ctx context.Context) (retErr error) {
	ot, stop, err := o.ConstructObservabilityTools()
	if err != nil {
		return err
	}
	defer errz.SafeCall(stop, &retErr)

	// Construct gRPC connection to gitlab-kas
	kasConn, err := o.ConstructKASConnection(ot, o.agentNameVersion(), api.AgentTypeWorkspace)
	if err != nil {
		return err
	}
	defer errz.SafeClose(kasConn, &retErr)

	// Construct in-mem API gRPC Server
	rpcAPIFactory := o.NewRPCAPIFactory()
	apiSrv, err := agent.NewInMemAPIServer(ot, rpcAPIFactory, o.Validator)
	if err != nil {
		return fmt.Errorf("in-mem API Server: %w", err)
	}
	defer errz.SafeClose(apiSrv, &retErr)

	instanceID := rand.Int64() //nolint: gosec
	runner := o.newModuleRunner()
	accessClient := gitlab_access_rpc.NewGitlabAccessClient(kasConn)
	configForFactory := func(name string) *modagentw.Config {
		return &modagentw.Config{
			Config: modagent.Config{
				Log:        o.Log.With(logz.ModuleName(name)),
				InstanceID: instanceID,
				API: &agent.AgentAPI{
					ModuleName:        name,
					AgentKey:          o.AgentKey,
					GitLabExternalURL: o.GitLabExternalURL,
					Client:            accessClient,
				},
				KASConn:          kasConn,
				APIServer:        apiSrv.Server,
				AgentName:        agent.AgentName,
				AgentNameVersion: o.agentNameVersion(),
				Validator:        o.Validator,
			},
			AgentMeta: o.agentMeta,
		}
	}

	// Start things up. Stages are shut down in reverse order.
	return syncz.RunLayers(ctx,
		&agent.FactoriesLayer[*modagentw.Config, *agentwcfg.AgentConfiguration]{
			Log:    o.Log,
			Config: configForFactory,
			LR:     nil,
			MR:     runner,
			Factories: []modagentw.Factory{
				&google_profiler_agentw.Factory{},
				&observability_agentw.Factory{
					Factory: observability_agent.Factory{
						LogLevel:      o.LogLevel,
						GRPCLogLevel:  o.GRPCLogLevel,
						Gatherer:      ot.Reg,
						Registerer:    ot.Reg,
						ListenNetwork: o.ObservabilityListenNetwork,
						ListenAddress: o.ObservabilityListenAddress,
						CertFile:      o.ObservabilityCertFile,
						KeyFile:       o.ObservabilityKeyFile,
					},
					DefaultGRPCLogLevel: defaultGRPCLogLevel,
				},
				&workspaces_agent.Factory{},
			},
		},
		syncz.LayerFunc(func(stage stager.Stage) {
			// Start server.
			apiSrv.Start(stage)
		}),
		&agent.FactoriesLayer[*modagentw.Config, *agentwcfg.AgentConfiguration]{
			Log:    o.Log,
			Config: configForFactory,
			LR:     nil,
			MR:     runner,
			Factories: []modagentw.Factory{
				// Uses kas connection.
				&agent_registrar_agentw.Factory{},
				// Uses kas connection.
				&agent2kas_tunnel_agentw.Factory{
					Factory: agent2kas_tunnel_agent.Factory{
						APIServerConn: apiSrv.InMemConn,
					},
				},
			},
		},
		// Start configuration refresh.
		syncz.LayerGoFunc(runner.RunConfigurationRefresh),
	)
}

func (o *options) newModuleRunner() *agent.ModuleRunner[*agentwcfg.AgentConfiguration, *agentwcfg.AgentConfiguration] {
	return &agent.ModuleRunner[*agentwcfg.AgentConfiguration, *agentwcfg.AgentConfiguration]{
		Log:     o.Log,
		Watcher: &configurationWatcher{},
		Data2Config: func(data *agentwcfg.AgentConfiguration) (*agentwcfg.AgentConfiguration, []any) {
			return data, nil
		},
	}
}

// agentNameVersion return a string to use as User-Agent or Server HTTP header.
func (o *options) agentNameVersion() string {
	return fmt.Sprintf("%s/%s/%s", agent.AgentName, o.agentMeta.Version, o.agentMeta.GitRef)
}

func NewCommand() *cobra.Command {
	o := newOptions()
	c := &cobra.Command{
		Use:   "agentw",
		Short: "GitLab Agent for Remote Development Workspace",
		Args:  cobra.NoArgs,
		Run: func(c *cobra.Command, args []string) {
			cmd.LogAndExitOnError(o.Log, o.Complete(envVarAgentToken, defaultLogLevel.String(), defaultGRPCLogLevel.String()))
			o.setGlobals()
			cmd.LogAndExitOnError(o.Log, o.run(c.Context()))
		},
		SilenceErrors: true, // Don't want Cobra to print anything, we handle error printing/logging ourselves.
		SilenceUsage:  true,
	}

	agent.AddCommonFlagsToCommand(c, &o.Options)

	return c
}
