package http

import (
	"context"
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/getsentry/sentry-go"
	sentryecho "github.com/getsentry/sentry-go/echo"
	"github.com/go-playground/validator"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	echoSwagger "github.com/swaggo/echo-swagger"
	middlewareOtel "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"

	_ "github.com/chaitin/panda-wiki/docs"

	"github.com/chaitin/panda-wiki/config"
	"github.com/chaitin/panda-wiki/log"
	PWMiddleware "github.com/chaitin/panda-wiki/middleware"
)

type HTTPServer struct {
	Echo *echo.Echo
}

type echoValidator struct {
	validator *validator.Validate
}

func (v *echoValidator) Validate(i any) error {
	if err := v.validator.Struct(i); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	return nil
}

func NewEcho(
	logger *log.Logger,
	config *config.Config,
	pwMiddleware *PWMiddleware.ReadOnlyMiddleware,
	sessionMiddleware *PWMiddleware.SessionMiddleware,
) *echo.Echo {

	// Initialize Sentry if enabled
	if config.Sentry.Enabled && config.Sentry.DSN != "" {
		err := sentry.Init(sentry.ClientOptions{
			Dsn: config.Sentry.DSN,
		})
		if err != nil {
			logger.Error("Failed to initialize Sentry", log.Error(err))
		} else {
			logger.Info("Sentry initialized successfully")
			// Flush buffered events on the default client before the program terminates.
			defer sentry.Flush(2 * time.Second)
		}
	}

	e := echo.New()
	e.HideBanner = true
	e.HidePort = true

	e.Binder = &MyBinder{}

	if os.Getenv("ENV") == "local" {
		e.Debug = true
		e.GET("/swagger/*", echoSwagger.WrapHandler)
	}
	// register validator
	e.Validator = &echoValidator{validator: validator.New()}

	// Add Sentry middleware if enabled
	if config.Sentry.Enabled && config.Sentry.DSN != "" {
		e.Use(sentryecho.New(sentryecho.Options{
			Repanic: true,
			Timeout: 5 * time.Second,
		}))
		sentry.CaptureMessage("It works!")
	}

	if config.GetBool("apm.enabled") {
		e.Use(middlewareOtel.Middleware(config.GetString("apm.service_name")))
	}

	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
		LogStatus:   true,
		LogURI:      true,
		LogLatency:  true,
		LogError:    true,
		LogMethod:   true,
		LogRemoteIP: true,
		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
			// Get the real IP address
			realIP := c.RealIP()
			method := c.Request().Method
			uri := v.URI
			status := v.Status
			latency := v.Latency.Milliseconds()
			if v.Error == nil {
				logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
					slog.String("remote_ip", realIP),
					slog.String("method", method),
					slog.String("uri", uri),
					slog.Int("status", status),
					slog.Int("latency", int(latency)),
				)
			} else {
				logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
					slog.String("remote_ip", realIP),
					slog.String("method", method),
					slog.String("uri", uri),
					slog.Int("status", status),
					slog.Int("latency", int(latency)),
					slog.String("err", v.Error.Error()),
				)
			}
			return nil
		},
	}))

	e.Use(pwMiddleware.ReadOnly)
	e.Use(sessionMiddleware.Session())

	return e
}

type MyBinder struct {
	echo.DefaultBinder
}

func (b *MyBinder) Bind(i interface{}, c echo.Context) (err error) {
	if err := b.BindPathParams(c, i); err != nil {
		return err
	}

	method := c.Request().Method
	if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead {
		if err = b.BindQueryParams(c, i); err != nil {
			return err
		}
		return nil
	}
	return b.BindBody(c, i)
}
