package httpapi

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"log/slog"
	"net/http"
	"net/url"
	"time"

	"commilitia.net/cdrop/internal/db"
)

// Shared HTTP helpers for the auth surface. After the Auth Broker migration (path A)
// cdrop no longer keeps server-side sessions, cookies, or refresh tokens — identity
// and session lifecycle live in the broker. What remains here is the CSRF origin
// check, the scan-login poll-secret hash, and the login-request reaper.

// deriveSiteOrigin extracts scheme://host from the configured public URL to give the
// CSRF Origin check a fixed expected value (and the scan-login QR its link origin).
// Empty / unparseable (dev) disables the origin check.
func deriveSiteOrigin(publicURL string) string {
	if publicURL == "" {
		return ""
	}
	u, err := url.Parse(publicURL)
	if err != nil || u.Scheme == "" || u.Host == "" {
		return ""
	}
	return u.Scheme + "://" + u.Host
}

// sessionID hashes an opaque token to its storage form (hex SHA-256). The scan-login
// poll_secret is stored as this hash — the new device holds the plaintext, so a leaked
// DB never yields a usable secret.
func sessionID(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

// sameOrigin is belt-and-suspenders CSRF defence: when the browser sends an Origin
// header (always, on fetch POST) it must match the deployment's own origin. Absent
// Origin (non-browser clients) is allowed, as is an unconfigured site origin (dev).
func (s *Server) sameOrigin(r *http.Request) bool {
	origin := r.Header.Get("Origin")
	if origin == "" || s.siteOrigin == "" {
		return true
	}
	return origin == s.siteOrigin
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "…"
}

// RunLoginRequestReaper periodically reclaims expired scan-login request rows. They
// are short-lived (default 120s) and already rejected at use time; this sweeps the
// stragglers that are opened and never collected.
func RunLoginRequestReaper(ctx context.Context, q *db.Queries) {
	ticker := time.NewTicker(1 * time.Hour)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if n, err := q.DeleteExpiredLoginRequests(ctx, time.Now().Unix()); err != nil {
				slog.Warn("login request reaper failed", "err", err)
			} else if n > 0 {
				slog.Info("login requests reaped", "count", n)
			}
		}
	}
}
