package http

import (
	"context"
	"crypto/subtle"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/nextlevelbuilder/goclaw/internal/bus"
	"github.com/nextlevelbuilder/goclaw/internal/crypto"
	"github.com/nextlevelbuilder/goclaw/internal/i18n"
	"github.com/nextlevelbuilder/goclaw/internal/permissions"
	"github.com/nextlevelbuilder/goclaw/internal/store"
)

// extractBearerToken extracts a bearer token from the Authorization header.
func extractBearerToken(r *http.Request) string {
	auth := r.Header.Get("Authorization")
	if auth == "" {
		return ""
	}
	if !strings.HasPrefix(auth, "Bearer ") {
		return ""
	}
	return strings.TrimPrefix(auth, "Bearer ")
}

// tokenMatch performs a constant-time comparison of a provided token against the expected token.
// Returns true if expected is empty (no auth configured) or if tokens match.
func tokenMatch(provided, expected string) bool {
	if expected == "" {
		return true
	}
	return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1
}

// extractUserID extracts the external user ID from the request header.
// Returns "" if no user ID is provided (anonymous).
// Rejects IDs exceeding MaxUserIDLength (VARCHAR(255) DB constraint).
func extractUserID(r *http.Request) string {
	id := r.Header.Get("X-GoClaw-User-Id")
	if id == "" {
		return ""
	}
	if err := store.ValidateUserID(id); err != nil {
		slog.Warn("security.user_id_too_long", "length", len(id), "max", store.MaxUserIDLength)
		return ""
	}
	return id
}

// extractAgentID determines the target agent from the request.
// Checks model field, headers, and falls back to "default".
func extractAgentID(r *http.Request, model string) string {
	// From model field: "goclaw:<agentId>" or "agent:<agentId>"
	if after, ok := strings.CutPrefix(model, "goclaw:"); ok {
		return after
	}
	if after, ok := strings.CutPrefix(model, "agent:"); ok {
		return after
	}

	// From headers
	if id := r.Header.Get("X-GoClaw-Agent-Id"); id != "" {
		return id
	}
	if id := r.Header.Get("X-GoClaw-Agent"); id != "" {
		return id
	}

	return "default"
}

// --- Package-level API key cache for shared auth ---

var pkgAPIKeyCache *apiKeyCache
var pkgPairingStore store.PairingStore

// InitAPIKeyCache initializes the shared API key cache with TTL and pubsub invalidation.
// Must be called once during server startup before handling requests.
func InitAPIKeyCache(s store.APIKeyStore, mb *bus.MessageBus) {
	pkgAPIKeyCache = newAPIKeyCache(s, 5*time.Minute)
	if mb != nil {
		mb.Subscribe("http-api-key-cache", func(e bus.Event) {
			if p, ok := e.Payload.(bus.CacheInvalidatePayload); ok && p.Kind == bus.CacheKindAPIKeys {
				pkgAPIKeyCache.invalidateAll()
			}
		})
	}
}

// InitPairingAuth sets the pairing store for HTTP auth.
// Allows browser-paired users to access HTTP APIs via X-GoClaw-Sender-Id header.
func InitPairingAuth(ps store.PairingStore) {
	pkgPairingStore = ps
}

// ResolveAPIKey checks if the bearer token is a valid API key using the shared cache.
// Returns the key data and derived role, or nil if not found/expired/revoked.
func ResolveAPIKey(ctx context.Context, token string) (*store.APIKeyData, permissions.Role) {
	if pkgAPIKeyCache == nil || token == "" {
		return nil, ""
	}
	hash := crypto.HashAPIKey(token)
	return pkgAPIKeyCache.getOrFetch(ctx, hash)
}

// authResult holds the resolved authentication state for an HTTP request.
type authResult struct {
	Role          permissions.Role
	Authenticated bool
}

// resolveAuth determines the caller's role from the request.
// Priority: gateway token → API key → no-auth fallback.
func resolveAuth(r *http.Request, gatewayToken string) authResult {
	return resolveAuthBearer(r, gatewayToken, extractBearerToken(r))
}

// resolveAuthBearer is like resolveAuth but accepts a pre-extracted bearer token.
// Useful for handlers that also accept tokens from query params.
func resolveAuthBearer(r *http.Request, gatewayToken, bearer string) authResult {
	// Gateway token → admin
	if gatewayToken != "" && tokenMatch(bearer, gatewayToken) {
		return authResult{Role: permissions.RoleAdmin, Authenticated: true}
	}
	// API key → role from scopes
	if _, role := ResolveAPIKey(r.Context(), bearer); role != "" {
		return authResult{Role: role, Authenticated: true}
	}
	// Browser pairing → operator (via X-GoClaw-Sender-Id header)
	if senderID := r.Header.Get("X-GoClaw-Sender-Id"); senderID != "" && pkgPairingStore != nil {
		paired, err := pkgPairingStore.IsPaired(senderID, "browser")
		if err == nil && paired {
			return authResult{Role: permissions.RoleOperator, Authenticated: true}
		}
		if err != nil {
			slog.Warn("security.http_pairing_check_failed", "sender_id", senderID, "error", err)
		} else {
			slog.Warn("security.http_pairing_auth_failed", "sender_id", senderID, "ip", r.RemoteAddr)
		}
	}
	// No auth configured → admin (no token = dev/single-user mode, full access)
	if gatewayToken == "" {
		return authResult{Role: permissions.RoleAdmin, Authenticated: true}
	}
	return authResult{}
}

// httpMinRole returns the minimum role required for an HTTP endpoint based on HTTP method.
func httpMinRole(method string) permissions.Role {
	switch method {
	case http.MethodGet, http.MethodHead, http.MethodOptions:
		return permissions.RoleViewer
	default: // POST, PUT, PATCH, DELETE
		return permissions.RoleOperator
	}
}

// requireAuth is a middleware that checks authentication and minimum role.
// Pass "" for minRole to auto-detect from HTTP method (GET→Viewer, POST→Operator).
// Injects locale and userID into request context.
func requireAuth(token string, minRole permissions.Role, next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		locale := extractLocale(r)
		auth := resolveAuth(r, token)

		if !auth.Authenticated {
			writeJSON(w, http.StatusUnauthorized, map[string]string{
				"error": i18n.T(locale, i18n.MsgUnauthorized),
			})
			return
		}

		required := minRole
		if required == "" {
			required = httpMinRole(r.Method)
		}

		if !permissions.HasMinRole(auth.Role, required) {
			writeJSON(w, http.StatusForbidden, map[string]string{
				"error": i18n.T(locale, i18n.MsgPermissionDenied, r.URL.Path),
			})
			return
		}

		ctx := store.WithLocale(r.Context(), locale)
		if userID := extractUserID(r); userID != "" {
			ctx = store.WithUserID(ctx, userID)
		}
		next(w, r.WithContext(ctx))
	}
}

// requireAuthBearer is like requireAuth but accepts a pre-extracted bearer token.
// Used by handlers that accept tokens from query params (files, media).
func requireAuthBearer(token string, minRole permissions.Role, bearer string, w http.ResponseWriter, r *http.Request) bool {
	locale := extractLocale(r)
	auth := resolveAuthBearer(r, token, bearer)

	if !auth.Authenticated {
		writeJSON(w, http.StatusUnauthorized, map[string]string{
			"error": i18n.T(locale, i18n.MsgUnauthorized),
		})
		return false
	}

	required := minRole
	if required == "" {
		required = httpMinRole(r.Method)
	}

	if !permissions.HasMinRole(auth.Role, required) {
		writeJSON(w, http.StatusForbidden, map[string]string{
			"error": i18n.T(locale, i18n.MsgPermissionDenied, r.URL.Path),
		})
		return false
	}
	return true
}

// extractLocale parses the Accept-Language header and returns a supported locale.
// Falls back to "en" if no supported language is found.
func extractLocale(r *http.Request) string {
	accept := r.Header.Get("Accept-Language")
	if accept == "" {
		return i18n.DefaultLocale
	}
	// Simple parser: take the first language tag before comma or semicolon
	for part := range strings.SplitSeq(accept, ",") {
		tag := strings.TrimSpace(strings.SplitN(part, ";", 2)[0])
		locale := i18n.Normalize(tag)
		if locale != i18n.DefaultLocale || strings.HasPrefix(tag, "en") {
			return locale
		}
	}
	return i18n.DefaultLocale
}
