// Package jwks fetches JSON Web Key Sets and validates RS256 JWTs against them.
//
// A Fetcher caches parsed RSA public keys in memory for 5 minutes per kid so
// repeated token validation does not re-hit the issuer's JWKS endpoint. It is
// safe for concurrent use.
package jwks

import (
	"context"
	"crypto/rsa"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"math/big"
	"net/http"
	"sync"
	"time"

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

// cacheTTL is how long a cached public key is considered fresh before the
// Fetcher re-fetches the JWKS document.
const cacheTTL = 5 * time.Minute

// Claims is the set of assertions carried by an Edu access token. The
// embedded RegisteredClaims provides exp, iss, aud and the other standard
// JWT claims.
type Claims struct {
	UserID    string `json:"user_id"`
	Role      string `json:"role"`
	DataScope string `json:"data_scope"`
	jwt.RegisteredClaims
}

// cachedKey holds a parsed public key together with the moment it expires.
type cachedKey struct {
	key       *rsa.PublicKey
	expiresAt time.Time
}

// Fetcher downloads and caches a JWKS document and validates tokens against
// the cached keys.
type Fetcher struct {
	jwksURL    string
	httpClient *http.Client
	cache      sync.Map
}

// NewFetcher returns a Fetcher that retrieves keys from jwksURL.
func NewFetcher(jwksURL string) *Fetcher {
	return &Fetcher{
		jwksURL:    jwksURL,
		httpClient: &http.Client{Timeout: 10 * time.Second},
	}
}

// GetPublicKey returns the RSA public key identified by kid. The key is served
// from the in-memory cache when fresh; otherwise the JWKS document is
// re-fetched and the cache repopulated.
func (f *Fetcher) GetPublicKey(kid string) (*rsa.PublicKey, error) {
	if v, ok := f.cache.Load(kid); ok {
		if ck, ok := v.(*cachedKey); ok && time.Now().Before(ck.expiresAt) {
			return ck.key, nil
		}
	}

	if err := f.refreshCache(); err != nil {
		return nil, err
	}

	v, ok := f.cache.Load(kid)
	if !ok {
		return nil, fmt.Errorf("jwks: kid %q not found in JWKS", kid)
	}
	ck, ok := v.(*cachedKey)
	if !ok {
		return nil, fmt.Errorf("jwks: cached entry for kid %q has unexpected type", kid)
	}
	return ck.key, nil
}

// refreshCache downloads the JWKS document and replaces every cached key.
func (f *Fetcher) refreshCache() error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, f.jwksURL, nil)
	if err != nil {
		return fmt.Errorf("jwks: building request: %w", err)
	}

	resp, err := f.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("jwks: fetching JWKS: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("jwks: unexpected status %d from %s", resp.StatusCode, f.jwksURL)
	}

	var body struct {
		Keys []struct {
			Kid string `json:"kid"`
			Kty string `json:"kty"`
			N   string `json:"n"`
			E   string `json:"e"`
		} `json:"keys"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return fmt.Errorf("jwks: decoding response: %w", err)
	}

	expiresAt := time.Now().Add(cacheTTL)
	for _, k := range body.Keys {
		if k.Kty != "RSA" {
			continue
		}
		pk, err := buildRSAPublicKey(k.N, k.E)
		if err != nil {
			return fmt.Errorf("jwks: parsing key %q: %w", k.Kid, err)
		}
		f.cache.Store(k.Kid, &cachedKey{key: pk, expiresAt: expiresAt})
	}
	return nil
}

// buildRSAPublicKey reconstructs an rsa.PublicKey from the base64url-encoded
// modulus (n) and exponent (e) fields of a JWK.
func buildRSAPublicKey(nStr, eStr string) (*rsa.PublicKey, error) {
	nBytes, err := base64.RawURLEncoding.DecodeString(nStr)
	if err != nil {
		return nil, fmt.Errorf("decoding modulus: %w", err)
	}
	eBytes, err := base64.RawURLEncoding.DecodeString(eStr)
	if err != nil {
		return nil, fmt.Errorf("decoding exponent: %w", err)
	}

	var e int
	for _, b := range eBytes {
		e = e<<8 + int(b)
	}
	if e == 0 {
		return nil, fmt.Errorf("empty exponent")
	}

	return &rsa.PublicKey{
		N: new(big.Int).SetBytes(nBytes),
		E: e,
	}, nil
}

// ValidateToken parses and validates an RS256 JWT using the cached JWKS keys.
// The kid in the token header selects the verification key. On success the
// populated Claims (including user_id, role, data_scope and the standard
// registered claims) are returned.
func (f *Fetcher) ValidateToken(token string) (*Claims, error) {
	parsed, err := jwt.ParseWithClaims(token, &Claims{}, func(t *jwt.Token) (any, error) {
		if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf("jwks: unexpected signing method %v", t.Header["alg"])
		}
		kid, ok := t.Header["kid"].(string)
		if !ok {
			return nil, fmt.Errorf("jwks: token header missing kid")
		}
		pk, err := f.GetPublicKey(kid)
		if err != nil {
			return nil, err
		}
		return pk, nil
	})
	if err != nil {
		return nil, fmt.Errorf("jwks: parsing token: %w", err)
	}

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