package server

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"log/slog"
	"net/http"
	"sync"
	"time"
)

// credCache memoises successful Basic-auth validations for a short TTL. Browsers
// resend the Authorization header on every request, so without this each one
// would trigger a full PAM round-trip (slow, and a brute-force/lockout risk).
// Only positive results are cached, keyed by a salted SHA-256 of user+password
// so the cache never holds a recoverable secret. A fresh random salt per process
// keeps keys from being precomputable.
type credCache struct {
	mu      sync.Mutex
	ttl     time.Duration
	salt    []byte
	entries map[string]time.Time
}

func newCredCache(ttl time.Duration) *credCache {
	salt := make([]byte, 16)
	_, _ = rand.Read(salt)
	return &credCache{ttl: ttl, salt: salt, entries: map[string]time.Time{}}
}

func (c *credCache) key(user, pass string) string {
	h := sha256.New()
	h.Write(c.salt)
	h.Write([]byte(user))
	h.Write([]byte{0})
	h.Write([]byte(pass))
	return hex.EncodeToString(h.Sum(nil))
}

// valid reports whether user/pass was validated within the TTL.
func (c *credCache) valid(user, pass string) bool {
	if c.ttl <= 0 {
		return false
	}
	k := c.key(user, pass)
	c.mu.Lock()
	defer c.mu.Unlock()
	exp, ok := c.entries[k]
	if !ok {
		return false
	}
	if time.Now().After(exp) {
		delete(c.entries, k)
		return false
	}
	return true
}

// store records a successful validation, opportunistically pruning expired keys.
func (c *credCache) store(user, pass string) {
	if c.ttl <= 0 {
		return
	}
	k := c.key(user, pass)
	now := time.Now()
	c.mu.Lock()
	defer c.mu.Unlock()
	c.entries[k] = now.Add(c.ttl)
	if len(c.entries) > 1024 {
		for kk, exp := range c.entries {
			if now.After(exp) {
				delete(c.entries, kk)
			}
		}
	}
}

// basicAuth wraps next with HTTP Basic authentication validated by authFn (PAM,
// see internal/pamauth). It mirrors kerberosAuth: a successful login writes the
// username into userHeader for the existing access pipeline (accessMiddleware /
// wsHandler), and any inbound value of userHeader is always discarded first so a
// client cannot spoof an identity.
//
// challenge controls the no/invalid-credentials case:
//   - challenge=true  (REST/page API): reply 401 + WWW-Authenticate: Basic so the
//     browser prompts for credentials.
//   - challenge=false (WebSocket upgrade): fall through unauthenticated; the
//     session resolves to default_user. Browsers cannot show a login dialog for a
//     WebSocket, but once they have cached credentials from the page's API calls
//     they resend them on the upgrade, so the validated path is still taken.
func basicAuth(authFn func(user, pass string) error, userHeader, realm string, challenge bool, cache *credCache, log *slog.Logger, next http.Handler) http.Handler {
	if realm == "" {
		realm = "uopi"
	}
	challengeValue := `Basic realm="` + realm + `", charset="UTF-8"`
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Never trust a client-supplied identity header; only a validated login sets it.
		r.Header.Del(userHeader)

		if user, pass, ok := r.BasicAuth(); ok && user != "" {
			if cache.valid(user, pass) {
				r.Header.Set(userHeader, user)
				next.ServeHTTP(w, r)
				return
			}
			if err := authFn(user, pass); err == nil {
				cache.store(user, pass)
				r.Header.Set(userHeader, user)
				next.ServeHTTP(w, r)
				return
			} else {
				log.Warn("basic auth failed", "user", user, "err", err)
			}
		}

		// No or invalid credentials.
		if challenge {
			w.Header().Set("WWW-Authenticate", challengeValue)
			http.Error(w, "authentication required", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r) // best-effort (WebSocket): downstream → default_user
	})
}
