package jwtauth

import (
	"context"
	"crypto/rsa"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"sync"
	"time"

	"github.com/go-jose/go-jose/v4"
)

// jwksCache fetches & caches the Casdoor JWKS.
//
// Behavior:
//   - successful fetch: refreshes the entire key set
//   - kid miss: forces a single refresh attempt
//   - stale-while-error: if cached entry exists, return it even when refresh fails
type jwksCache struct {
	url string
	ttl time.Duration

	mu        sync.RWMutex
	keys      map[string]*rsa.PublicKey
	fetchedAt time.Time

	httpClient *http.Client
}

func newJWKSCache(url string, ttl time.Duration) *jwksCache {
	return &jwksCache{
		url:        url,
		ttl:        ttl,
		keys:       map[string]*rsa.PublicKey{},
		httpClient: &http.Client{Timeout: 5 * time.Second},
	}
}

func (j *jwksCache) GetKey(ctx context.Context, kid string) (*rsa.PublicKey, error) {
	if kid == "" {
		return nil, errors.New("missing kid")
	}

	j.mu.RLock()
	cached, found := j.keys[kid]
	fresh := !j.fetchedAt.IsZero() && time.Since(j.fetchedAt) < j.ttl
	j.mu.RUnlock()

	if found && fresh {
		return cached, nil
	}

	if err := j.refresh(ctx); err != nil {
		if found {
			slog.Warn("jwks refresh failed; returning stale key",
				"err", err, "kid", kid)
			return cached, nil
		}
		return nil, fmt.Errorf("jwks refresh: %w", err)
	}

	j.mu.RLock()
	defer j.mu.RUnlock()
	if k, ok := j.keys[kid]; ok {
		return k, nil
	}
	return nil, fmt.Errorf("kid %q not found in JWKS", kid)
}

func (j *jwksCache) refresh(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, j.url, nil)
	if err != nil {
		return err
	}
	resp, err := j.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("status %d", resp.StatusCode)
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	var set jose.JSONWebKeySet
	if err := json.Unmarshal(body, &set); err != nil {
		return fmt.Errorf("parse JWKS: %w", err)
	}

	next := map[string]*rsa.PublicKey{}
	for _, k := range set.Keys {
		pk, ok := k.Key.(*rsa.PublicKey)
		if !ok || k.KeyID == "" {
			continue
		}
		next[k.KeyID] = pk
	}

	j.mu.Lock()
	defer j.mu.Unlock()
	j.keys = next
	j.fetchedAt = time.Now()
	return nil
}
