package services_astrologer

import (
	"errors"
	"os"
	"strconv"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

const (

	// Access Token
	AccessTokenExpire = time.Hour * 24 * 30

	// Refresh Token
	RefreshTokenExpire = time.Hour * 24 * 30
)

func jwtSecret() []byte {

	secret := os.Getenv("JWT_SECRET")

	if secret == "" {
		secret = "astrology-secret-key"
	}

	return []byte(secret)
}

func GenerateJWT(userID uint, roleID uint) (string, error) {

	claims := jwt.MapClaims{

		"id": userID,

		// sub carries the same id in the registered claim, which is what any
		// standard JWT reader looks for. The middleware still reads "id".
		"sub": strconv.FormatUint(uint64(userID), 10),

		"role_id": roleID,

		"type": "access",

		"exp": time.Now().
			Add(AccessTokenExpire).
			Unix(),

		"iat": time.Now().Unix(),
	}

	token := jwt.NewWithClaims(

		jwt.SigningMethodHS256,

		claims,
	)

	return token.SignedString(jwtSecret())
}

func GenerateRefreshToken(userID uint, roleID uint) (string, error) {

	claims := jwt.MapClaims{

		"id": userID,

		// Without sub the refresh endpoint had no id to read: it parsed these
		// tokens as registered claims and got an empty Subject, so every refresh
		// failed. "id" is still written for tokens issued before that was fixed.
		"sub": strconv.FormatUint(uint64(userID), 10),

		"role_id": roleID,

		"type": "refresh",

		"exp": time.Now().
			Add(RefreshTokenExpire).
			Unix(),

		"iat": time.Now().Unix(),
	}

	token := jwt.NewWithClaims(

		jwt.SigningMethodHS256,

		claims,
	)

	return token.SignedString(jwtSecret())
}

func ValidateJWT(tokenString string) (jwt.MapClaims, error) {

	token, err := jwt.Parse(

		tokenString,

		func(token *jwt.Token) (interface{}, error) {

			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

				return nil, errors.New("invalid signing method")
			}

			return jwtSecret(), nil
		},
	)

	if err != nil {

		return nil, err
	}

	if !token.Valid {

		return nil, errors.New("invalid token")
	}

	claims, ok := token.Claims.(jwt.MapClaims)

	if !ok {

		return nil, errors.New("invalid claims")
	}

	return claims, nil
}

// ValidateRefreshToken parses a refresh token and returns its claims.
//
// It reads the same secret as the generators through jwtSecret(), rather than
// os.Getenv directly: with JWT_SECRET unset the two disagreed, and every token
// signed with the fallback failed to verify here.
func ValidateRefreshToken(tokenString string) (jwt.MapClaims, error) {

	claims, err := ValidateJWT(tokenString)

	if err != nil {
		return nil, err
	}

	// An access token must not be spendable as a refresh token. Both kinds have
	// carried "type" from the start, so this rejects nothing that used to work.
	if tokenType, ok := claims["type"].(string); ok && tokenType != "refresh" {
		return nil, errors.New("invalid refresh token")
	}

	return claims, nil
}

// ClaimUserID reads the account id out of a token's claims.
//
// Accepts "sub" and "id", in either the string or the number form a JSON decode
// can produce, so refresh tokens issued before "sub" was added keep working.
func ClaimUserID(claims jwt.MapClaims) (uint, error) {

	for _, key := range []string{"sub", "id"} {

		switch value := claims[key].(type) {

		case float64:

			if value > 0 {
				return uint(value), nil
			}

		case int:

			if value > 0 {
				return uint(value), nil
			}

		case uint:

			if value > 0 {
				return value, nil
			}

		case string:

			parsed, err := strconv.ParseUint(value, 10, 64)

			if err == nil && parsed > 0 {
				return uint(parsed), nil
			}
		}
	}

	return 0, errors.New("invalid token payload")
}
