package agent

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

	"github.com/ash2k/stager"
	"github.com/prometheus/client_golang/prometheus"
	"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/tool/logz"
)

type ModuleCfg struct {
	LogLevel     string
	GRPCLogLevel string
}

type Module[CM any] struct {
	Log                                     *slog.Logger
	LogLevel                                *slog.LevelVar
	GRPCLogLevel                            *slog.LevelVar
	API                                     modshared.API
	Gatherer                                prometheus.Gatherer
	Registerer                              prometheus.Registerer
	Listener                                func(context.Context) (net.Listener, error)
	ServerName                              string
	CfgAccessor                             func(CM) *ModuleCfg
	DefaultAndValidateConfigurationCallback func(CM) error
}

const (
	prometheusURLPath     = "/metrics"
	livenessProbeURLPath  = "/liveness"
	readinessProbeURLPath = "/readiness"
)

func (m *Module[CM]) Run(ctx context.Context, cmCh <-chan CM) error {
	return stager.RunStages(ctx,
		func(stage stager.Stage) {
			// Listen for config changes and apply to logger
			stage.Go(func(ctx context.Context) error {
				done := ctx.Done()
				for {
					select {
					case <-done:
						return nil
					case cm, ok := <-cmCh:
						if !ok {
							return nil
						}
						config := m.CfgAccessor(cm)
						err := m.setConfigurationLogging(config)
						if err != nil {
							m.Log.Error("Failed to apply logging configuration", logz.Error(err))
							continue
						}
					}
				}
			})
			// Start metrics server
			stage.Go(func(ctx context.Context) error {
				lis, err := m.Listener(ctx)
				if err != nil {
					return fmt.Errorf("observability listener failed to start: %w", err)
				}
				// Error is ignored because metricSrv.Run() closes the listener and
				// a second close always produces an error.
				defer lis.Close() //nolint:errcheck

				m.Log.Info("Observability endpoint is up",
					logz.NetNetworkFromAddr(lis.Addr()),
					logz.NetAddressFromAddr(lis.Addr()),
				)

				metricSrv := observability.MetricServer{
					Log:                   m.Log,
					API:                   m.API,
					Name:                  m.ServerName,
					Listener:              lis,
					PrometheusURLPath:     prometheusURLPath,
					LivenessProbeURLPath:  livenessProbeURLPath,
					ReadinessProbeURLPath: readinessProbeURLPath,
					Gatherer:              m.Gatherer,
					Registerer:            m.Registerer,
					ProbeRegistry:         observability.NewProbeRegistry(),
				}

				return metricSrv.Run(ctx)
			})
		},
	)
}

func (m *Module[CM]) DefaultAndValidateConfiguration(cm CM) error {
	return m.DefaultAndValidateConfigurationCallback(cm)
}

func (m *Module[CM]) Name() string {
	return observability.ModuleName
}

func (m *Module[CM]) setConfigurationLogging(config *ModuleCfg) error {
	err := setLogLevel(m.LogLevel, config.LogLevel)
	if err != nil {
		return err
	}
	return setLogLevel(m.GRPCLogLevel, config.GRPCLogLevel) // not nil after defaulting
}

func setLogLevel(logLevel *slog.LevelVar, val string) error {
	return logLevel.UnmarshalText([]byte(val))
}
