package api

import (
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/0x2e/fusion/auth"
	"github.com/0x2e/fusion/conf"
	"github.com/0x2e/fusion/frontend"
	"github.com/0x2e/fusion/repo"
	"github.com/0x2e/fusion/server"

	"github.com/go-playground/locales/en"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	en_translations "github.com/go-playground/validator/v10/translations/en"
	"github.com/gorilla/sessions"
	"github.com/labstack/echo-contrib/session"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type Params struct {
	Host            string
	Port            int
	PasswordHash    *auth.HashedPassword
	UseSecureCookie bool
	TLSCert         string
	TLSKey          string
}

func Run(params Params) {
	r := echo.New()

	if conf.Debug {
		r.Debug = true
		r.Use(middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) {
			if len(resBody) > 500 {
				resBody = append(resBody[:500], []byte("...")...)
			}
			slog.Debug("body dump", "req", reqBody, "resp", resBody)
		}))
	}

	r.HideBanner = true
	r.HTTPErrorHandler = errorHandler
	r.Validator = newCustomValidator()
	r.Use(middleware.Recover())
	r.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
		LogStatus:   true,
		LogURI:      true,
		LogError:    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 {
			if !strings.HasPrefix(v.URI, "/api") {
				return nil
			}
			if v.Error == nil {
				slog.Info("REQUEST", "uri", v.URI, "status", v.Status)
			} else {
				slog.Error(v.Error.Error(), "uri", v.URI, "status", v.Status)
			}
			return nil
		},
	}))
	r.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
		Timeout: 30 * time.Second,
	}))
	if params.PasswordHash != nil {
		r.Use(session.Middleware(sessions.NewCookieStore(params.PasswordHash.Bytes())))
	}
	r.Pre(middleware.RemoveTrailingSlash())
	r.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if strings.HasPrefix(c.Request().URL.Path, "/_app/") {
				c.Response().Header().Set("Cache-Control", "public, max-age=2592000")
			}
			return next(c)
		}
	})
	r.Use(middleware.StaticWithConfig(middleware.StaticConfig{
		HTML5:      true,
		Index:      "index.html",
		Filesystem: http.FS(frontend.Content),
		Browse:     false,
	}))

	authed := r.Group("/api")

	if params.PasswordHash != nil {
		loginAPI := Session{
			PasswordHash:    *params.PasswordHash,
			UseSecureCookie: params.UseSecureCookie,
		}
		r.POST("/api/sessions", loginAPI.Create)

		authed.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
			return func(c echo.Context) error {
				if err := loginAPI.Check(c); err != nil {
					return echo.NewHTTPError(http.StatusUnauthorized)
				}
				return next(c)
			}
		})

		authed.DELETE("/sessions", loginAPI.Delete)
	}

	feeds := authed.Group("/feeds")
	feedAPIHandler := newFeedAPI(server.NewFeed(repo.NewFeed(repo.DB)))
	feeds.GET("", feedAPIHandler.List)
	feeds.GET("/:id", feedAPIHandler.Get)
	feeds.POST("", feedAPIHandler.Create)
	feeds.POST("/validation", feedAPIHandler.CheckValidity)
	feeds.PATCH("/:id", feedAPIHandler.Update)
	feeds.DELETE("/:id", feedAPIHandler.Delete)
	feeds.POST("/refresh", feedAPIHandler.Refresh)

	groups := authed.Group("/groups")
	groupAPIHandler := newGroupAPI(server.NewGroup(repo.NewGroup(repo.DB)))
	groups.GET("", groupAPIHandler.All)
	groups.POST("", groupAPIHandler.Create)
	groups.PATCH("/:id", groupAPIHandler.Update)
	groups.DELETE("/:id", groupAPIHandler.Delete)

	items := authed.Group("/items")
	itemAPIHandler := newItemAPI(server.NewItem(repo.NewItem(repo.DB)))
	items.GET("", itemAPIHandler.List)
	items.GET("/:id", itemAPIHandler.Get)
	items.PATCH("/:id/bookmark", itemAPIHandler.UpdateBookmark)
	items.PATCH("/-/unread", itemAPIHandler.UpdateUnread)
	items.DELETE("/:id", itemAPIHandler.Delete)

	var err error
	addr := fmt.Sprintf("%s:%d", params.Host, params.Port)
	if params.TLSCert != "" {
		err = r.StartTLS(addr, params.TLSCert, params.TLSKey)
	} else {
		err = r.Start(addr)
	}
	if err != nil {
		slog.Error(err.Error())
		return
	}
}

func errorHandler(err error, c echo.Context) {
	if errors.Is(err, repo.ErrNotFound) {
		err = echo.NewHTTPError(http.StatusNotFound, "Resource not exists")
	} else {
		if bizerr, ok := err.(server.BizError); ok {
			err = echo.NewHTTPError(int(bizerr.HTTPCode), bizerr.FEMessage)
		}
	}

	c.Echo().DefaultHTTPErrorHandler(err, c)
}

type CustomValidator struct {
	handler *validator.Validate
	trans   ut.Translator
}

func newCustomValidator() *CustomValidator {
	en := en.New()
	uni := ut.New(en, en)
	trans, _ := uni.GetTranslator("en")
	validate := validator.New()
	en_translations.RegisterDefaultTranslations(validate, trans)
	return &CustomValidator{
		handler: validate,
		trans:   trans,
	}
}

func (v *CustomValidator) Validate(i interface{}) error {
	err := v.handler.Struct(i)
	if err != nil {
		errs := err.(validator.ValidationErrors)
		msg := strings.Builder{}
		for _, content := range errs.Translate(v.trans) {
			msg.WriteString(content)
			msg.WriteString(".")
		}
		err = echo.NewHTTPError(http.StatusBadRequest, msg.String())
	}
	return err
}

func bindAndValidate(i interface{}, c echo.Context) error {
	if err := c.Bind(i); err != nil {
		return err
	}
	return c.Validate(i)
}
