package ekko

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

	"github.com/labstack/echo-contrib/echoprometheus"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	slogecho "github.com/samber/slog-echo"
	"go.ntppool.org/common/logger"
	"go.ntppool.org/common/version"
	"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	"golang.org/x/net/http2"
	"golang.org/x/sync/errgroup"
)

func New(name string, options ...func(*Ekko)) (*Ekko, error) {
	ek := &Ekko{
		writeTimeout:      60 * time.Second,
		readHeaderTimeout: 30 * time.Second,
	}

	for _, o := range options {
		o(ek)
	}
	return ek, nil
}

// Setup Echo; only intended for testing
func (ek *Ekko) SetupEcho(ctx context.Context) (*echo.Echo, error) {
	return ek.setup(ctx)
}

// Setup Echo and start the server. Will return if the http server
// returns or the context is done.
func (ek *Ekko) Start(ctx context.Context) error {
	log := logger.Setup()

	e, err := ek.setup(ctx)
	if err != nil {
		return err
	}

	g, ctx := errgroup.WithContext(ctx)
	g.Go(func() error {
		e.Server.Addr = fmt.Sprintf(":%d", ek.port)
		log.Info("server starting", "port", ek.port)
		// err := e.Server.ListenAndServe()
		err := e.StartH2CServer(e.Server.Addr, &http2.Server{})
		if err == http.ErrServerClosed {
			return nil
		}
		return err
	})

	g.Go(func() error {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		return e.Shutdown(shutdownCtx)
	})

	return g.Wait()
}

func (ek *Ekko) setup(ctx context.Context) (*echo.Echo, error) {
	log := logger.Setup()

	e := echo.New()

	e.Server.ReadHeaderTimeout = ek.readHeaderTimeout
	e.Server.WriteTimeout = ek.writeTimeout

	e.Server.BaseContext = func(_ net.Listener) context.Context {
		return ctx
	}

	trustOptions := []echo.TrustOption{
		echo.TrustLoopback(true),
		echo.TrustLinkLocal(false),
		echo.TrustPrivateNet(true),
	}
	e.IPExtractor = echo.ExtractIPFromXFFHeader(trustOptions...)

	if ek.otelmiddleware == nil {
		e.Use(otelecho.Middleware(ek.name))
	} else {
		e.Use(ek.otelmiddleware)
	}

	e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
		LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
			log.ErrorContext(c.Request().Context(), err.Error(), "stack", string(stack))
			fmt.Println(string(stack))
			return err
		},
	}))

	e.Use(slogecho.NewWithConfig(log,
		slogecho.Config{
			WithTraceID: false, // done by logger already
			Filters:     ek.logFilters,
		},
	))

	if ek.prom != nil {
		e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
			Subsystem:  ek.name,
			Registerer: ek.prom,
		}))
	}

	if ek.gzipConfig != nil {
		e.Use(middleware.GzipWithConfig(*ek.gzipConfig))
	} else {
		e.Use(middleware.Gzip())
	}

	secureConfig := middleware.DefaultSecureConfig
	// secureConfig.ContentSecurityPolicy = "default-src *"
	secureConfig.ContentSecurityPolicy = ""
	secureConfig.HSTSMaxAge = int(time.Hour * 168 * 30 / time.Second)
	secureConfig.HSTSPreloadEnabled = true

	e.Use(middleware.SecureWithConfig(secureConfig))

	e.Use(
		func(next echo.HandlerFunc) echo.HandlerFunc {
			return func(c echo.Context) error {
				request := c.Request()

				span := trace.SpanFromContext(request.Context())
				if span.IsRecording() {

					span.SetAttributes(attribute.String("http.real_ip", c.RealIP()))
					span.SetAttributes(attribute.String("url.path", c.Request().RequestURI))
					if q := c.QueryString(); len(q) > 0 {
						span.SetAttributes(attribute.String("url.query", q))
					}
					c.Response().Header().Set("Traceparent", span.SpanContext().TraceID().String())
				}
				return next(c)
			}
		},
	)

	e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		vinfo := version.VersionInfo()
		v := ek.name + "/" + vinfo.Version + "+" + vinfo.GitRevShort
		return func(c echo.Context) error {
			c.Response().Header().Set(echo.HeaderServer, v)
			return next(c)
		}
	})

	if ek.routeFn != nil {
		err := ek.routeFn(e)
		if err != nil {
			return nil, err
		}
	}

	return e, nil
}