package auth

import (
	"context"
	"errors"
	"fmt"
	"time"

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

var (
	// ErrInvalidToken is returned when the token is malformed or signature is invalid.
	ErrInvalidToken = errors.New("invalid token")
	// ErrExpiredToken is returned when the token has expired.
	ErrExpiredToken = errors.New("token expired")
	// ErrInvalidClaims is returned when the token claims are invalid.
	ErrInvalidClaims = errors.New("invalid claims")
)

// JWTConfig configures the JWT validator.
type JWTConfig struct {
	// Secret is the HMAC secret key for HS256/HS384/HS512
	Secret []byte
	// PublicKey is the RSA/ECDSA public key for RS*/ES* algorithms (optional)
	PublicKey any
	// Issuer is the expected issuer claim (optional)
	Issuer string
	// Audience is the expected audience claim (optional)
	Audience string
}

// JWTClaims extends jwt.RegisteredClaims with custom fields.
type JWTClaims struct {
	jwt.RegisteredClaims
	// UserID is the user identifier
	UserID string `json:"uid,omitempty"`
	// Email is the user's email
	Email string `json:"email,omitempty"`
	// Roles are the user's roles
	Roles []string `json:"roles,omitempty"`
	// Scopes are the permitted scopes
	Scopes []string `json:"scopes,omitempty"`
	// SessionID links the token to a specific login session
	SessionID string `json:"sid,omitempty"`
}

// JWTValidator validates JWT tokens.
type JWTValidator struct {
	secret    []byte
	publicKey any
	issuer    string
	audience  string
}

// NewJWTValidator creates a new JWT validator.
func NewJWTValidator(cfg JWTConfig) *JWTValidator {
	return &JWTValidator{
		secret:    cfg.Secret,
		publicKey: cfg.PublicKey,
		issuer:    cfg.Issuer,
		audience:  cfg.Audience,
	}
}

// Validate validates a JWT token and returns the user.
func (v *JWTValidator) Validate(ctx context.Context, tokenString string) (*User, error) {
	// Parse and validate the token
	token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
		// Check signing method
		switch token.Method.(type) {
		case *jwt.SigningMethodHMAC:
			if v.secret == nil {
				return nil, fmt.Errorf("%w: HMAC secret not configured", ErrInvalidToken)
			}
			return v.secret, nil
		case *jwt.SigningMethodRSA, *jwt.SigningMethodECDSA:
			if v.publicKey == nil {
				return nil, fmt.Errorf("%w: public key not configured", ErrInvalidToken)
			}
			return v.publicKey, nil
		default:
			return nil, fmt.Errorf("%w: unexpected signing method %v", ErrInvalidToken, token.Header["alg"])
		}
	})

	if err != nil {
		if errors.Is(err, jwt.ErrTokenExpired) {
			return nil, ErrExpiredToken
		}
		return nil, fmt.Errorf("%w: %w", ErrInvalidToken, err)
	}

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

	// Validate issuer if configured
	if v.issuer != "" && claims.Issuer != v.issuer {
		return nil, fmt.Errorf("%w: invalid issuer", ErrInvalidClaims)
	}

	// Validate audience if configured
	if v.audience != "" {
		found := false
		for _, aud := range claims.Audience {
			if aud == v.audience {
				found = true
				break
			}
		}
		if !found {
			return nil, fmt.Errorf("%w: invalid audience", ErrInvalidClaims)
		}
	}

	// Build user from claims
	user := &User{
		ID:       claims.UserID,
		Email:    claims.Email,
		Roles:    claims.Roles,
		Scopes:   claims.Scopes,
		Metadata: make(map[string]any),
	}

	// Fallback to subject if no user ID
	if user.ID == "" {
		user.ID = claims.Subject
	}

	// Store session ID in metadata for downstream checks
	if claims.SessionID != "" {
		user.Metadata["sid"] = claims.SessionID
	}

	return user, nil
}

// -----------------------------------------------------------------------------
// Token Generation (for testing and admin tools)
// -----------------------------------------------------------------------------

// GenerateToken creates a new JWT token for the given user.
// expiresIn specifies the token lifetime.
func GenerateToken(secret []byte, user *User, expiresIn time.Duration) (string, error) {
	now := time.Now()

	claims := JWTClaims{
		RegisteredClaims: jwt.RegisteredClaims{
			Subject:   user.ID,
			ExpiresAt: jwt.NewNumericDate(now.Add(expiresIn)),
			IssuedAt:  jwt.NewNumericDate(now),
			NotBefore: jwt.NewNumericDate(now),
		},
		UserID: user.ID,
		Email:  user.Email,
		Roles:  user.Roles,
		Scopes: user.Scopes,
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(secret)
}

// ValidateAllowExpired is like Validate but tolerates expired tokens.
// It still validates the signature, issuer, and audience — only the expiry check is relaxed.
// Used for token refresh: the caller presents an expired access token to prove identity,
// and the service layer checks session validity separately.
func (v *JWTValidator) ValidateAllowExpired(ctx context.Context, tokenString string) (*User, error) {
	// Use a generous leeway to accept expired tokens for refresh purposes.
	token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
		switch token.Method.(type) {
		case *jwt.SigningMethodHMAC:
			if v.secret == nil {
				return nil, fmt.Errorf("%w: HMAC secret not configured", ErrInvalidToken)
			}
			return v.secret, nil
		case *jwt.SigningMethodRSA, *jwt.SigningMethodECDSA:
			if v.publicKey == nil {
				return nil, fmt.Errorf("%w: public key not configured", ErrInvalidToken)
			}
			return v.publicKey, nil
		default:
			return nil, fmt.Errorf("%w: unexpected signing method %v", ErrInvalidToken, token.Header["alg"])
		}
	}, jwt.WithLeeway(30*24*time.Hour)) // 30 days — matches session lifetime

	if err != nil {
		// Even with generous leeway, signature failures still fail.
		return nil, fmt.Errorf("%w: %w", ErrInvalidToken, err)
	}

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

	if v.issuer != "" && claims.Issuer != v.issuer {
		return nil, fmt.Errorf("%w: invalid issuer", ErrInvalidClaims)
	}
	if v.audience != "" {
		found := false
		for _, aud := range claims.Audience {
			if aud == v.audience {
				found = true
				break
			}
		}
		if !found {
			return nil, fmt.Errorf("%w: invalid audience", ErrInvalidClaims)
		}
	}

	user := &User{
		ID:       claims.UserID,
		Email:    claims.Email,
		Roles:    claims.Roles,
		Scopes:   claims.Scopes,
		Metadata: make(map[string]any),
	}
	if user.ID == "" {
		user.ID = claims.Subject
	}
	if claims.SessionID != "" {
		user.Metadata["sid"] = claims.SessionID
	}

	return user, nil
}

// GenerateTokenWithIssuer creates a new JWT token with issuer and audience claims.
func GenerateTokenWithIssuer(secret []byte, user *User, expiresIn time.Duration, issuer, audience string) (string, error) {
	return GenerateTokenWithSession(secret, user, expiresIn, issuer, audience, "")
}

// GenerateTokenWithSession creates a JWT token with a session ID embedded.
// The session ID links the token to a login session for revocation support.
func GenerateTokenWithSession(secret []byte, user *User, expiresIn time.Duration, issuer, audience, sessionID string) (string, error) {
	now := time.Now()

	claims := JWTClaims{
		RegisteredClaims: jwt.RegisteredClaims{
			Subject:   user.ID,
			Issuer:    issuer,
			Audience:  []string{audience},
			ExpiresAt: jwt.NewNumericDate(now.Add(expiresIn)),
			IssuedAt:  jwt.NewNumericDate(now),
			NotBefore: jwt.NewNumericDate(now),
		},
		UserID:    user.ID,
		Email:     user.Email,
		Roles:     user.Roles,
		Scopes:    user.Scopes,
		SessionID: sessionID,
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(secret)
}
