package metrics

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/tech/sendico/fx/ingestor/internal/config"
	"github.com/tech/sendico/pkg/api/routers"
	"github.com/tech/sendico/pkg/api/routers/health"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

const (
	defaultAddress        = ":9102"
	readHeaderTimeout     = 5 * time.Second
	defaultShutdownWindow = 5 * time.Second
)

type Server interface {
	SetStatus(status health.ServiceStatus)
	Close(ctx context.Context)
}

//nolint:ireturn
func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error) {
	if logger == nil {
		return nil, merrors.InvalidArgument("metrics: logger is nil")
	}

	if cfg == nil || !cfg.Enabled {
		logger.Debug("Metrics disabled; using noop server")

		return noopServer{}, nil
	}

	address := strings.TrimSpace(cfg.Address)
	if address == "" {
		address = defaultAddress
	}

	metricsLogger := logger.Named("metrics")
	router := chi.NewRouter()
	router.Handle("/metrics", promhttp.Handler())

	var healthRouter routers.Health

	hr, err := routers.NewHealthRouter(metricsLogger, router, "")
	if err != nil {
		metricsLogger.Warn("Failed to initialise health router", zap.Error(err))
	} else {
		hr.SetStatus(health.SSStarting)
		healthRouter = hr
	}

	httpServer := &http.Server{
		Addr:              address,
		Handler:           router,
		ReadHeaderTimeout: readHeaderTimeout,
	}

	wrapper := &httpServerWrapper{
		logger:  metricsLogger,
		server:  httpServer,
		health:  healthRouter,
		timeout: defaultShutdownWindow,
	}

	go func() {
		metricsLogger.Info("Prometheus endpoint listening", zap.String("address", address))

		err := httpServer.ListenAndServe()
		if err != nil && !errors.Is(err, http.ErrServerClosed) {
			metricsLogger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err))
			if healthRouter != nil {
				healthRouter.SetStatus(health.SSTerminating)
			}
		}
	}()

	return wrapper, nil
}

type httpServerWrapper struct {
	logger  mlogger.Logger
	server  *http.Server
	health  routers.Health
	timeout time.Duration
}

func (s *httpServerWrapper) SetStatus(status health.ServiceStatus) {
	if s == nil || s.health == nil {
		return
	}

	s.logger.Debug("Updating metrics health status", zap.String("status", string(status)))
	s.health.SetStatus(status)
}

func (s *httpServerWrapper) Close(ctx context.Context) {
	if s == nil {
		return
	}

	if s.health != nil {
		s.health.SetStatus(health.SSTerminating)
		s.health.Finish()
		s.health = nil
	}

	if s.server == nil {
		return
	}

	//nolint:contextcheck
	shutdownCtx := ctx
	if shutdownCtx == nil {
		shutdownCtx = context.Background()
	}

	if s.timeout > 0 {
		var cancel context.CancelFunc
		shutdownCtx, cancel = context.WithTimeout(shutdownCtx, s.timeout)
		defer cancel()
	}

	if err := s.server.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) {
		s.logger.Warn("Failed to stop metrics server", zap.Error(err))
	} else {
		s.logger.Info("Metrics server stopped")
	}
}

type noopServer struct{}

func (noopServer) SetStatus(_ health.ServiceStatus) {}

func (noopServer) Close(_ context.Context) {}
