package middleware

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
)

const (
	HeaderUserID   = "X-User-ID"
	HeaderTenantID = "X-Tenant-ID"
	HeaderIsAdmin  = "X-Is-Admin"
	HeaderRole     = "X-Role"

	CtxUserID   = "user_id"
	CtxTenantID = "tenant_id"
	CtxIsAdmin  = "is_admin"
	CtxRole     = "role"
)

type ErrorResponse struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

// JWTAuth validates the bearer token and injects claims into both gin context and upstream request headers.
func JWTAuth(secret string) gin.HandlerFunc {
	return func(c *gin.Context) {
		if validateAndInject(c, secret) {
			c.Next()
		}
	}
}

// JWTAuthSkip behaves like JWTAuth but skips authentication entirely when skip(c)
// returns true. Used for catch-all routes that mix public and protected sub-paths
// (e.g. /payments/* where /callback/* and /webhook/* must stay public) — gin forbids
// registering a catch-all alongside static child segments, so the branch lives here.
func JWTAuthSkip(secret string, skip func(*gin.Context) bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		if skip(c) {
			c.Next()
			return
		}
		if validateAndInject(c, secret) {
			c.Next()
		}
	}
}

// validateAndInject validates the bearer token and injects claims into the gin context
// and upstream request headers. On failure it aborts with 401 and returns false.
// It does NOT call c.Next() — the caller decides whether to continue the chain.
func validateAndInject(c *gin.Context, secret string) bool {
	hdr := c.GetHeader("Authorization")
	if !strings.HasPrefix(hdr, "Bearer ") {
		c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse{Code: "unauthorized", Message: "missing bearer token"})
		return false
	}
	tokenStr := hdr[7:]
	token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, jwt.ErrSignatureInvalid
		}
		return []byte(secret), nil
	})
	if err != nil || !token.Valid {
		c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse{Code: "unauthorized", Message: "invalid or expired token"})
		return false
	}
	claims, _ := token.Claims.(jwt.MapClaims)
	userID, _ := uuid.Parse(fmt.Sprintf("%v", claims["sub"]))
	tenantID, _ := uuid.Parse(fmt.Sprintf("%v", claims["tenant_id"]))
	isAdmin := false
	switch v := claims["is_admin"].(type) {
	case bool:
		isAdmin = v
	case string:
		isAdmin = v == "true"
	}
	role, _ := claims["role"].(string)

	c.Set(CtxUserID, userID)
	c.Set(CtxTenantID, tenantID)
	c.Set(CtxIsAdmin, isAdmin)
	c.Set(CtxRole, role)

	// Inject for upstream services
	c.Request.Header.Set(HeaderUserID, userID.String())
	c.Request.Header.Set(HeaderTenantID, tenantID.String())
	if isAdmin {
		c.Request.Header.Set(HeaderIsAdmin, "true")
	}
	if role != "" {
		c.Request.Header.Set(HeaderRole, role)
	}
	return true
}

// OptionalJWTAuth parses the token if present but does not abort on missing/invalid token.
func OptionalJWTAuth(secret string) gin.HandlerFunc {
	return func(c *gin.Context) {
		hdr := c.GetHeader("Authorization")
		if !strings.HasPrefix(hdr, "Bearer ") {
			c.Next()
			return
		}
		token, err := jwt.Parse(hdr[7:], func(t *jwt.Token) (interface{}, error) {
			if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, jwt.ErrSignatureInvalid
			}
			return []byte(secret), nil
		})
		if err != nil || !token.Valid {
			c.Next()
			return
		}
		claims, _ := token.Claims.(jwt.MapClaims)
		userID, _ := uuid.Parse(fmt.Sprintf("%v", claims["sub"]))
		tenantID, _ := uuid.Parse(fmt.Sprintf("%v", claims["tenant_id"]))
		isAdmin := false
		switch v := claims["is_admin"].(type) {
		case bool:
			isAdmin = v
		case string:
			isAdmin = v == "true"
		}
		c.Request.Header.Set(HeaderUserID, userID.String())
		c.Request.Header.Set(HeaderTenantID, tenantID.String())
		if isAdmin {
			c.Request.Header.Set(HeaderIsAdmin, "true")
		}
		c.Next()
	}
}

func GetUserID(c *gin.Context) (uuid.UUID, bool) {
	v, ok := c.Get(CtxUserID)
	if !ok {
		return uuid.Nil, false
	}
	id, ok := v.(uuid.UUID)
	return id, ok && id != uuid.Nil
}
