package services

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"golang.org/x/crypto/bcrypt"
)

type JWTClaims struct {
	UserID   uuid.UUID `json:"sub"`
	Username string    `json:"username"`
	SteamID  string    `json:"steam_id,omitempty"`
	Role     string    `json:"role"`
	Tier     string    `json:"tier"`
	Purpose  string    `json:"purpose,omitempty"`
	jwt.RegisteredClaims
}

const SSETokenTTL = 5 * time.Minute
const RefreshTokenTTL = 24 * time.Hour

var (
	jwtPrivateKey *rsa.PrivateKey
	jwtPublicKey  *rsa.PublicKey
	jwtExpiry     time.Duration
)

func InitAuth(privKey *rsa.PrivateKey, pubKey *rsa.PublicKey, expirySec int) {
	jwtPrivateKey = privKey
	jwtPublicKey = pubKey
	jwtExpiry = time.Duration(expirySec) * time.Second
}

func AccessTokenTTL() time.Duration {
	return jwtExpiry
}

func GenerateToken(userID uuid.UUID, username, steamID, role, tier string) (string, error) {
	return GenerateAccessToken(userID, username, steamID, role, tier)
}

func GenerateAccessToken(userID uuid.UUID, username, steamID, role, tier string) (string, error) {
	now := time.Now().UTC()
	claims := JWTClaims{
		UserID:   userID,
		Username: username,
		SteamID:  steamID,
		Role:     role,
		Tier:     tier,
		Purpose:  "access",
		RegisteredClaims: jwt.RegisteredClaims{
			IssuedAt:  jwt.NewNumericDate(now),
			ExpiresAt: jwt.NewNumericDate(now.Add(jwtExpiry)),
			Issuer:    "tapper",
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	return token.SignedString(jwtPrivateKey)
}

func GenerateRefreshToken() (raw string, hash string, err error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", "", err
	}
	raw = base64.RawURLEncoding.EncodeToString(buf)
	return raw, HashRefreshToken(raw), nil
}

func HashRefreshToken(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

// GenerateSSEToken issues a short-lived token used only for EventSource
// query-string auth, where the browser API cannot send Authorization headers.
// Middleware accepts query tokens only when Purpose == "sse".
func GenerateSSEToken(userID uuid.UUID, username, steamID, role, tier string) (string, error) {
	now := time.Now().UTC()
	claims := JWTClaims{
		UserID:   userID,
		Username: username,
		SteamID:  steamID,
		Role:     role,
		Tier:     tier,
		Purpose:  "sse",
		RegisteredClaims: jwt.RegisteredClaims{
			IssuedAt:  jwt.NewNumericDate(now),
			ExpiresAt: jwt.NewNumericDate(now.Add(SSETokenTTL)),
			Issuer:    "tapper",
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	return token.SignedString(jwtPrivateKey)
}

func ValidateToken(tokenString string) (*JWTClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(t *jwt.Token) (interface{}, error) {
		if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
		}
		return jwtPublicKey, nil
	}, jwt.WithValidMethods([]string{"RS256"}))
	if err != nil {
		return nil, err
	}

	claims, ok := token.Claims.(*JWTClaims)
	if !ok || !token.Valid {
		return nil, fmt.Errorf("invalid token claims")
	}

	return claims, nil
}

func ValidateAccessToken(tokenString string) (*JWTClaims, error) {
	claims, err := ValidateToken(tokenString)
	if err != nil {
		return nil, err
	}
	if claims.Purpose != "" && claims.Purpose != "access" {
		return nil, fmt.Errorf("token purpose %q is not valid for API access", claims.Purpose)
	}
	return claims, nil
}

func HashPassword(password string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
	if err != nil {
		return "", err
	}
	return string(hash), nil
}

func CheckPassword(password, hash string) bool {
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
