/*
Copyright 2023, Tax Administration of The Netherlands.
Licensed under the EUPL 1.2.
See LICENSE.md for details.
*/

package argocd_plugin_generator

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"time"

	"github.com/belastingdienst/opr-paas/v3/internal/logging"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
)

const pluginServerTimeout = 10 * time.Second

// ServerOptions contains configuration values for starting the plug-in generator HTTP server.
//
// Addr specifies the TCP address for the server to listen on.
// For example, ":4355" will bind on all interfaces at port 4355.
// Use "0" to disable the server entirely.
//
// TokenEnvVar specifies the name of the environment variable from which
// the bearer token will be read. This token is required for authenticating
// incoming requests from ArgoCD.
type ServerOptions struct {
	Addr        string
	TokenEnvVar string
}

// GeneratorServer represents the HTTP server that exposes the ArgoCD plug-in generator endpoint.
//
// It is responsible for starting, stopping, and serving HTTP requests
// using the provided handler. The server is typically added to a
// controller-runtime Manager so it can run alongside the main operator.
//
// Fields:
//   - opts: Holds the server configuration (address, token environment variable).
//   - handler: The HTTP handler that processes incoming plug-in generator requests.
//   - server: The underlying *http.Server used for network communication.
type GeneratorServer struct {
	opts    ServerOptions
	handler http.Handler
	server  *http.Server
	started bool
}

// NewServer returns a new GeneratorServer based on the ServerOptions and the http.Handler
func NewServer(opts ServerOptions, handler http.Handler) *GeneratorServer {
	return &GeneratorServer{
		opts:    opts,
		handler: handler,
	}
}

// Start starts the GeneratorServer. If it fails, it returns an error.
func (s *GeneratorServer) Start(ctx context.Context) error {
	ctx, componentLogger := logging.GetLogComponent(ctx, logging.PluginGeneratorComponent)
	logger := componentLogger.With().Str("server", s.opts.Addr).Logger()
	token := os.Getenv(s.opts.TokenEnvVar)
	if token == "" {
		logger.Error().Msg("token not set")
		return fmt.Errorf("environment variable %s not set", s.opts.TokenEnvVar)
	}

	s.server = &http.Server{
		Addr:         s.opts.Addr,
		Handler:      withMetrics(s.handler),
		ReadTimeout:  pluginServerTimeout,
		WriteTimeout: pluginServerTimeout,
	}

	ln, err := net.Listen("tcp", s.opts.Addr)
	if err != nil {
		logger.Error().AnErr("error", err).Msg("Failed to create listener")
		return err
	}
	s.started = true

	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
		defer cancel()
		_, shutdownLogger := logging.GetLogComponent(shutdownCtx, logging.PluginGeneratorComponent)
		logger = shutdownLogger.With().Str("server", s.opts.Addr).Logger()
		logger.Debug().Msg("shutting down")
		_ = s.server.Shutdown(shutdownCtx)
	}()

	return s.server.Serve(ln)
}

// StartedChecker returns a healthz.Checker which reports healthy after the
// server has been started and is reachable over TCP.
func (s *GeneratorServer) StartedChecker() healthz.Checker {
	return func(r *http.Request) error {
		_, logger := logging.GetLogComponent(r.Context(), logging.PluginGeneratorComponent)
		if !s.started {
			logger.Error().Msg("not yet started")
			return errors.New("argoCD plugin generator server has not been started yet")
		}

		d := &net.Dialer{Timeout: pluginServerTimeout}
		conn, err := d.Dial("tcp", s.opts.Addr)
		if err != nil {
			logger.Error().Msg("not yet reachable")
			return fmt.Errorf("argoCD plugin generator server is not reachable: %w", err)
		}
		_ = conn.Close()

		return nil
	}
}
