package main

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"slices"
	"sort"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	_ "github.com/zoriya/kyoo/transcoder/docs"

	echoSwagger "github.com/swaggo/echo-swagger/v2"
	"github.com/zoriya/kyoo/transcoder/src"
	"github.com/zoriya/kyoo/transcoder/src/api"
	"github.com/zoriya/kyoo/transcoder/src/utils"

	"github.com/MicahParks/keyfunc/v3"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

// otelecho & RequestLoggerWithConfig middleware call c.Error
// otelecho docs: https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho#WithOnError
func ErrorHandler(c *echo.Context, err error) {
	if resp, _ := echo.UnwrapResponse(c.Response()); resp != nil && resp.Committed {
		return
	}

	code := http.StatusInternalServerError
	var message string
	if he, ok := err.(*echo.HTTPError); ok {
		code = he.Code
		message = fmt.Sprint(he.Message)
	} else {
		c.Logger().Error(err.Error())
		message = "Internal server error"
	}
	c.JSON(code, struct {
		Errors []string `json:"errors"`
	}{Errors: []string{message}})
}

func RequireCorePlayPermission(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c *echo.Context) error {
		user := c.Get("user")
		if user == nil {
			return echo.NewHTTPError(http.StatusForbidden, "missing jwt")
		}
		token, ok := user.(*jwt.Token)
		if !ok {
			return echo.NewHTTPError(http.StatusForbidden, "invalid jwt")
		}
		claims, ok := token.Claims.(jwt.MapClaims)
		if !ok {
			return echo.NewHTTPError(http.StatusForbidden, "invalid jwt claims")
		}
		permissions, ok := claims["permissions"]
		if !ok {
			return echo.NewHTTPError(http.StatusForbidden, "missing permissions claim")
		}
		perms, ok := permissions.([]any)
		if !ok {
			return echo.NewHTTPError(http.StatusForbidden, "permissions claim is not an array")
		}
		if !slices.Contains(perms, "core.play") {
			return echo.NewHTTPError(http.StatusForbidden, "missing core.play permission")
		}
		return next(c)
	}
}

// @title gocoder - Kyoo's transcoder
// @version 1.0
// @description Real time transcoder.

// @contact.name Repository
// @contact.url https://github.com/zoriya/kyoo

// @license.name  GPL-3.0
// @license.url https://www.gnu.org/licenses/gpl-3.0.en.html

// @host kyoo.zoriya.dev
// @BasePath /video

// @securityDefinitions.apiKey Token
// @in header
// @name Authorization

// @securityDefinitions.apiKey Jwt
// @in header
// @name Authorization
func main() {
	ctx := context.Background()

	_, err := SetupLogger(ctx)
	if err != nil {
		slog.ErrorContext(ctx, "logger init", "err", err)
	}

	cleanup, err := setupOtel(ctx)
	if err != nil {
		slog.ErrorContext(ctx, "Failed to setup otel: ", "err", err)
		return
	}
	defer cleanup(ctx)

	e := echo.New()
	e.Logger = slog.Default()
	// workaround for not supporting absolute paths
	// https://github.com/labstack/echo/issues/2922
	e.Filesystem = os.DirFS("/")
	instrument(e)

	e.Use(middleware.Recover())

	ignorepath := []string{
		"/video/health",
		"/video/ready",
	}
	slog.InfoContext(ctx, "Skipping request logging for these paths", "paths", func() string {
		sort.Strings(ignorepath)
		return strings.Join(ignorepath, ",")
	}())

	// using example from https://echo.labstack.com/docs/middleware/logger#examples
	// full configs https://github.com/labstack/echo/blob/master/middleware/request_logger.go
	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
		// declare a small set of paths to ignore
		Skipper: func(c *echo.Context) bool {
			p := c.Request().URL.Path
			return slices.Contains(ignorepath, p)
		},
		LogStatus:    true,
		LogURI:       true,
		LogHost:      true,
		LogMethod:    true,
		LogUserAgent: 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 {
			rCtx := c.Request().Context()
			uri := v.URI
			if u, err := url.Parse(v.URI); err == nil {
				if q := u.Query(); q.Has("x-presign") {
					q.Del("x-presign")
					u.RawQuery = q.Encode()
					uri = u.String()
				}
			}
			if v.Error == nil {
				slog.LogAttrs(rCtx, slog.LevelInfo,
					fmt.Sprintf("%s %s%s %d", v.Method, v.Host, uri, v.Status),
					slog.String("method", v.Method),
					slog.Int("status", v.Status),
					slog.String("host", v.Host),
					slog.String("uri", uri),
					slog.String("agent", v.UserAgent),
				)
			} else {
				slog.LogAttrs(rCtx, slog.LevelError,
					fmt.Sprintf("%s %s%s %d err=%s",
						v.Method, v.Host, uri, v.Status, v.Error.Error()),
					slog.String("method", v.Method),
					slog.Int("status", v.Status),
					slog.String("host", v.Host),
					slog.String("uri", uri),
					slog.String("agent", v.UserAgent),
					slog.String("err", v.Error.Error()),
				)
			}
			return nil
		},
	}))

	e.GET("/video/swagger/*", echoSwagger.WrapHandler)
	e.HTTPErrorHandler = ErrorHandler

	metadata, err := src.NewMetadataService()
	if err != nil {
		e.Logger.Error("failed to create metadata service: ", slog.Any("err", err))
		return
	}
	defer utils.CleanupWithErr(&err, metadata.Close, "failed to close metadata service")

	transcoder, err := src.NewTranscoder(metadata)
	if err != nil {
		e.Logger.Error("failed to create transcoder: ", slog.Any("err", err))
		return
	}

	g := e.Group("/video")

	if src.Settings.JwksUrl != "" {
		ctx, cancel := context.WithCancel(ctx)
		defer cancel()

		k, err := keyfunc.NewDefaultCtx(ctx, []string{src.Settings.JwksUrl})
		if err != nil {
			e.Logger.Error("Failed to get a jwks: ", slog.Any("err", err))
		}
		g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
			return func(c *echo.Context) error {
				authHeader := c.Request().Header.Get("Authorization")
				if !strings.HasPrefix(authHeader, "Bearer ") {
					return echo.NewHTTPError(http.StatusUnauthorized, "Missing or invalid token")
				}

				token, err := jwt.Parse(strings.TrimPrefix(authHeader, "Bearer "), k.Keyfunc)
				if err != nil {
					return echo.NewHTTPError(http.StatusForbidden, "Invalid token: "+err.Error())
				}
				if !token.Valid {
					return echo.NewHTTPError(http.StatusForbidden, "Token is invalid")
				}
				c.Set("user", token)

				return next(c)
			}
		})

		g.Use(RequireCorePlayPermission)
	}

	api.RegisterHealthHandlers(e.Group("/video"), metadata.Database)
	api.RegisterStreamHandlers(g, transcoder)
	api.RegisterMetadataHandlers(g, metadata)
	api.RegisterPProfHandlers(e)

	sc := echo.StartConfig{
		Address:         ":7666",
		GracefulTimeout: 10 * time.Second,
		HideBanner:      true,
	}
	if err := sc.Start(ctx, e); err != nil {
		e.Logger.Error("server failed", "err", err)
		os.Exit(1)
	}
}
