package routes

import (
	"dashboard/logger"
	"dashboard/models"
	"dashboard/pkg/jwt"
	"dashboard/pkg/rate"
	"dashboard/settings"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

func GinLogger(log *logger.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery

		c.Next()

		cost := time.Since(start)

		log.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

func GinRecovery(log *logger.Logger, stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						expectStr := strings.ToLower(se.Error())
						if strings.Contains(expectStr, "broken pipe") ||
							strings.Contains(expectStr, "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					log.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)

					c.Error(err.(error))
					c.Abort()
					return
				}

				if stack {
					log.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					log.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}

				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()

		c.Next()
	}
}

func GinLog(log *logger.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set(models.GinContxtLog, log)
		c.Next()
	}
}

func GinJwtAuthor(jwtC *jwt.Jwt) appHandler {
	return func(c *gin.Context) error {
		authHeader, err := c.Cookie(models.GinAuthorKey)
		if err != nil || !strings.HasPrefix(authHeader, models.GinAuthorPrefixKey) {
			c.Abort()
			return &models.BaseError{
				Code: http.StatusUnauthorized,
				Msg:  "Missing or invalid token",
			}
		}

		tokenStr := strings.TrimPrefix(authHeader, models.GinAuthorPrefixKey)

		_, err = jwtC.ParseToken(tokenStr)
		if err != nil {
			c.Abort()
			if jwt.ErrorIsJwtExpired(err) {
				return &models.BaseError{
					Code: http.StatusUnauthorized,
					Msg:  "Expired token",
				}
			}

			return &models.BaseError{
				Code: http.StatusUnauthorized,
				Msg:  "Invalid token",
			}
		}

		c.Next()

		return nil
	}
}

func GinRateLimit(rateC settings.RateLimitConfig) appHandler {
	lrate, err := rate.New(rate.WithCapacity(rateC.Capacity),
		rate.WithFillInterval(time.Duration(rateC.FillInterval)*time.Millisecond),
	)

	if err != nil || lrate.Bucket == nil {
		panic(err)
	}

	return func(c *gin.Context) error {
		if !lrate.WaitMaxDuration(1, time.Duration(rateC.MaxWait)*time.Second) {
			time.Sleep(time.Duration(rateC.MaxWait) * time.Second)
			c.Abort()
			return &models.BaseError{
				Code: http.StatusServiceUnavailable,
				Msg:  "Exceeded rate limit",
			}
		}

		c.Next()

		return nil
	}
}
