package auth

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"net/http"
	"strings"
	"time"

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

const (
	// FingerprintCookieName carries the OWASP "user context" — a high-entropy
	// random value bound to the session. Its SHA-256 hash is embedded in the JWT
	// as the userFingerprint claim. The cookie is HttpOnly + Secure + SameSite=Strict,
	// so a stolen JWT alone is not enough to impersonate the user.
	FingerprintCookieName = "__Secure-Fgp"

	// RefreshCookieName carries the long-lived refresh token. Scoped to /api/auth
	// so it is only sent to refresh/logout endpoints — not to every API call.
	RefreshCookieName = "__Secure-Refresh"

	// TokenLifetime matches the OWASP cheat-sheet example (15-minute access tokens).
	TokenLifetime   = 15 * time.Minute
	clockSkewLeeway = 30 * time.Second

	// RefreshTokenLifetime is how long a refresh token remains valid before the
	// user must log in again. 7 days is a common middle ground (shorter = more
	// re-logins, longer = bigger blast radius on theft).
	RefreshTokenLifetime = 7 * 24 * time.Hour

	// refreshTokenBytes is the size of the random secret carried in the
	// __Secure-Refresh cookie. 32 bytes = 256 bits, hex-encoded to 64 chars.
	refreshTokenBytes = 32

	refreshCookiePath = "/api/auth"
)

// Fingerprint pairs the raw cookie value with its SHA-256 hash.
// The raw value goes in the cookie; only the hash goes in the JWT.
type Fingerprint struct {
	Raw  string
	Hash string
}

func NewFingerprint() (Fingerprint, error) {
	raw := make([]byte, 50)
	if _, err := rand.Read(raw); err != nil {
		return Fingerprint{}, err
	}
	rawHex := hex.EncodeToString(raw)
	return Fingerprint{Raw: rawHex, Hash: hashFingerprint(rawHex)}, nil
}

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

func SetFingerprintCookie(w http.ResponseWriter, fp Fingerprint) {
	http.SetCookie(w, &http.Cookie{
		Name:     FingerprintCookieName,
		Value:    fp.Raw,
		Path:     "/api",
		MaxAge:   int(TokenLifetime.Seconds()),
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	})
}

func ClearFingerprintCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     FingerprintCookieName,
		Value:    "",
		Path:     "/api",
		MaxAge:   -1,
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	})
}

// RefreshToken pairs the raw cookie value with its SHA-256 hash. The raw value
// goes in the cookie; only the hash is stored in the refresh_tokens table.
// Same defence-in-depth as password hashing — a DB dump does not yield usable
// tokens.
type RefreshToken struct {
	Raw  string
	Hash string
}

func NewRefreshToken() (RefreshToken, error) {
	raw := make([]byte, refreshTokenBytes)
	if _, err := rand.Read(raw); err != nil {
		return RefreshToken{}, err
	}
	rawHex := hex.EncodeToString(raw)
	return RefreshToken{Raw: rawHex, Hash: HashRefreshToken(rawHex)}, nil
}

// HashRefreshToken hashes a raw refresh-token value the same way the DB row
// stored it. Exported so handlers can hash the incoming cookie value before
// querying refresh_tokens.token_hash.
func HashRefreshToken(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

func SetRefreshCookie(w http.ResponseWriter, t RefreshToken) {
	http.SetCookie(w, &http.Cookie{
		Name:     RefreshCookieName,
		Value:    t.Raw,
		Path:     refreshCookiePath,
		MaxAge:   int(RefreshTokenLifetime.Seconds()),
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	})
}

func ClearRefreshCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     RefreshCookieName,
		Value:    "",
		Path:     refreshCookiePath,
		MaxAge:   -1,
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	})
}

type claims struct {
	Username        string `json:"username"`
	UserFingerprint string `json:"userFingerprint"`
	jwt.RegisteredClaims
}

type contextKey string

const userKey contextKey = "authUser"

type User struct {
	UserID   uuid.UUID
	Username string
}

func EncodeJWT(userID uuid.UUID, username, fingerprintHash, secret, issuer string) (string, error) {
	now := time.Now()
	c := claims{
		Username:        username,
		UserFingerprint: fingerprintHash,
		RegisteredClaims: jwt.RegisteredClaims{
			Subject:   userID.String(),
			Issuer:    issuer,
			IssuedAt:  jwt.NewNumericDate(now),
			NotBefore: jwt.NewNumericDate(now),
			ExpiresAt: jwt.NewNumericDate(now.Add(TokenLifetime)),
		},
	}
	return jwt.NewWithClaims(jwt.SigningMethodHS256, c).SignedString([]byte(secret))
}

func DecodeJWT(tokenStr, secret, issuer, expectedFingerprintHash string) (*User, error) {
	parser := jwt.NewParser(
		jwt.WithValidMethods([]string{"HS256"}),
		jwt.WithIssuer(issuer),
		jwt.WithLeeway(clockSkewLeeway),
		jwt.WithExpirationRequired(),
	)
	tok, err := parser.ParseWithClaims(tokenStr, &claims{}, func(t *jwt.Token) (any, error) {
		return []byte(secret), nil
	})
	if err != nil || !tok.Valid {
		return nil, errUnauthorized
	}
	c, ok := tok.Claims.(*claims)
	if !ok {
		return nil, errUnauthorized
	}
	if !constantTimeEqual(c.UserFingerprint, expectedFingerprintHash) {
		return nil, errUnauthorized
	}
	userID, err := uuid.Parse(c.Subject)
	if err != nil {
		return nil, errUnauthorized
	}
	return &User{UserID: userID, Username: c.Username}, nil
}

func constantTimeEqual(a, b string) bool {
	return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}

// RequireAuth validates the Bearer JWT *and* the fingerprint cookie. Either missing → 401.
func RequireAuth(secret, issuer string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
			if !ok || token == "" {
				unauthorized(w)
				return
			}
			cookie, err := r.Cookie(FingerprintCookieName)
			if err != nil || cookie.Value == "" {
				unauthorized(w)
				return
			}
			user, err := DecodeJWT(token, secret, issuer, hashFingerprint(cookie.Value))
			if err != nil {
				unauthorized(w)
				return
			}
			ctx := context.WithValue(r.Context(), userKey, *user)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func UserFrom(r *http.Request) (User, bool) {
	u, ok := r.Context().Value(userKey).(User)
	return u, ok
}

var errUnauthorized = errors.New("unauthorized")

func unauthorized(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusUnauthorized)
	_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
}
