package api

import (
	"context"
	"encoding/json"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	"regexp"

	"archivmail/config"
	"archivmail/internal/audit"
	"archivmail/internal/auth"
	imapstore "archivmail/internal/imap"
	"archivmail/internal/index"
	ldapcfg "archivmail/internal/ldapconfig"
	"archivmail/internal/mailer"
	pop3store "archivmail/internal/pop3"
	"archivmail/internal/reconciliation"
	"archivmail/internal/smtpoutconfig"
	"archivmail/internal/smtpd"
	"archivmail/internal/storage"
	"archivmail/internal/tenantstore"
	"archivmail/internal/tokenstore"
	"archivmail/internal/userstore"
)

// SEC-22: Compiled regex for mail ID validation to prevent path traversal.
var mailIDRegex = regexp.MustCompile(`^[0-9a-f]{64}$`)

// isValidMailID validates that a mail ID matches the expected hex format.
func isValidMailID(id string) bool {
	return mailIDRegex.MatchString(id)
}

// roleLevel returns the privilege level for a role string.
// Hierarchy: superadmin=5 > domain_admin=4 > domain_auditor=3 > auditor=2 > user=1
// Separation of duties: admins (superadmin, domain_admin) have NO mail access.
// Mail access: domain_auditor (all tenant mails), auditor (own mails), user (own mails).
func roleLevel(role string) int {
	levels := map[string]int{
		userstore.RoleUser:          1,
		userstore.RoleAuditor:       2,
		userstore.RoleDomainAuditor: 3,
		userstore.RoleDomainAdmin:   4,
		userstore.RoleAdmin:         4, // legacy alias for domain_admin
		userstore.RoleSuperAdmin:    5,
	}
	return levels[role]
}

type contextKey string

const (
	sessionKey contextKey = "session"
	tenantKey  contextKey = "tenant_id"
)

// Server is the archivmail HTTP API server.
type Server struct {
	cfg        config.APIConfig
	metricsCfg config.MetricsConfig
	startTime  time.Time
	store      *storage.Store
	idx        index.Indexer
	authMgr    *auth.Manager
	users      *userstore.Store
	audlog     *audit.Logger
	logger     *slog.Logger
	mux        *http.ServeMux
	smtpDaemon    *smtpd.Daemon
	imapStore     *imapstore.Store
	imapImporter  *imapstore.Importer
	imapScheduler *imapstore.Scheduler
	pop3Store     *pop3store.Store
	pop3Importer  *pop3store.Importer
	uploadJobs    sync.Map // jobID → *UploadJob
	ldapStore         *ldapcfg.Store
	tenantStore       *tenantstore.Store
	tenantLdapStore   *ldapcfg.TenantStore
	idxMgr            index.TenantIndexer
	appVersion           string
	moduleVersions       map[string]string
	globalRetentionDays  int // from storage config (PROJ-34)
	mailer               *mailer.Mailer
	tokenStore           *tokenstore.Store
	fqdn                 string // from server.fqdn config (PROJ-28)
	smtpOutStore         *smtpoutconfig.Store
	apiKeyMw             *auth.APIKeyMiddleware // PROJ-13: external API auth
	reconStore           *reconciliation.Store  // PROJ-52: completeness reconciliation
	reconThresholdPct    int                    // PROJ-52: alert threshold (percent below 7-day avg)
}

// SetSMTPDaemon wires the SMTP daemon into the API server after construction.
func (s *Server) SetSMTPDaemon(d *smtpd.Daemon) {
	s.smtpDaemon = d
}

// SetImap wires the IMAP store, importer, and scheduler into the API server after construction.
func (s *Server) SetImap(store *imapstore.Store, importer *imapstore.Importer, scheduler *imapstore.Scheduler) {
	s.imapStore = store
	s.imapImporter = importer
	s.imapScheduler = scheduler
}

// SetPop3 wires the POP3 store and importer into the API server after construction.
func (s *Server) SetPop3(store *pop3store.Store, importer *pop3store.Importer) {
	s.pop3Store = store
	s.pop3Importer = importer
}

// SetIndexManager wires the per-tenant index manager into the API server (PROJ-21 Phase 4).
func (s *Server) SetIndexManager(mgr index.TenantIndexer) {
	s.idxMgr = mgr
}

// SetVersion wires app version and module versions into the API server.
func (s *Server) SetVersion(appVersion string, modules map[string]string) {
	s.appVersion = appVersion
	s.moduleVersions = modules
}

// SetGlobalRetentionDays wires the global retention_days from storage config into the API server.
func (s *Server) SetGlobalRetentionDays(days int) {
	s.globalRetentionDays = days
}

// SetMetrics wires the metrics config into the API server.
func (s *Server) SetMetrics(cfg config.MetricsConfig) {
	s.metricsCfg = cfg
}

// SetMailer wires the SMTP-Out mailer into the API server (PROJ-28).
func (s *Server) SetMailer(m *mailer.Mailer) {
	s.mailer = m
}

// SetTokenStore wires the token store into the API server (PROJ-28).
func (s *Server) SetTokenStore(ts *tokenstore.Store) {
	s.tokenStore = ts
}

// SetFQDN wires the server FQDN for link generation (PROJ-28).
func (s *Server) SetFQDN(fqdn string) {
	s.fqdn = fqdn
}

// SetSMTPOutStore wires the SMTP-Out config store into the API server.
func (s *Server) SetSMTPOutStore(store *smtpoutconfig.Store) {
	s.smtpOutStore = store
}

// SetReconciliation wires the completeness-reconciliation store and the alert
// threshold (percent below the trailing 7-day average) into the API server
// (PROJ-52).
func (s *Server) SetReconciliation(store *reconciliation.Store, thresholdPct int) {
	s.reconStore = store
	s.reconThresholdPct = thresholdPct
}

// New creates and wires up a new API server.
func New(
	cfg config.APIConfig,
	store *storage.Store,
	idx index.Indexer,
	authMgr *auth.Manager,
	users *userstore.Store,
	audlog *audit.Logger,
	logger *slog.Logger,
) *Server {
	s := &Server{
		cfg:       cfg,
		store:     store,
		idx:       idx,
		authMgr:   authMgr,
		users:     users,
		audlog:    audlog,
		logger:    logger,
		mux:       http.NewServeMux(),
		startTime: time.Now(),
		apiKeyMw:  auth.NewAPIKeyMiddleware(store), // PROJ-13
	}
	s.routes()
	return s
}

// auth wraps a handler with authentication + tenant context propagation.
func (s *Server) auth(h http.HandlerFunc) http.HandlerFunc {
	return s.authMiddleware(s.tenantMiddleware(h))
}

// authAdmin wraps a handler requiring at least admin role.
func (s *Server) authAdmin(h http.HandlerFunc) http.HandlerFunc {
	return s.authMiddleware(s.tenantMiddleware(s.requireRole(userstore.RoleDomainAdmin, h)))
}

func (s *Server) routes() {
	s.mux.HandleFunc("GET /api/health", s.handleHealth)
	s.mux.HandleFunc("GET /metrics", s.handleMetrics)
	s.mux.HandleFunc("GET /api/version", s.handleVersion)
	s.mux.HandleFunc("GET /api/system/info", s.auth(s.handleSystemInfo))
	s.mux.HandleFunc("POST /api/auth/login", s.handleLogin)
	s.mux.HandleFunc("GET /api/auth/me", s.auth(s.handleMe))
	s.mux.HandleFunc("POST /api/auth/logout", s.auth(s.handleLogout))

	// PROJ-28: Self-Service Onboarding
	s.mux.HandleFunc("POST /api/auth/signup", s.handleSignup)
	s.mux.HandleFunc("GET /api/auth/verify", s.handleVerifyEmail)
	s.mux.HandleFunc("POST /api/auth/forgot-password", s.handleForgotPassword)
	s.mux.HandleFunc("POST /api/auth/reset-password", s.handleResetPassword)
	s.mux.HandleFunc("GET /api/auth/invite", s.handleCheckInvite)
	s.mux.HandleFunc("POST /api/admin/invite", s.authAdmin(s.handleCreateInvite))
	s.mux.HandleFunc("GET /api/users", s.authAdmin(s.handleListUsers))
	s.mux.HandleFunc("POST /api/users", s.authAdmin(s.handleCreateUser))
	s.mux.HandleFunc("PATCH /api/users/{id}", s.authAdmin(s.handleUpdateUser))
	s.mux.HandleFunc("DELETE /api/users/{id}", s.authAdmin(s.handleDeleteUser))
	s.mux.HandleFunc("GET /api/search", s.auth(s.handleSearch))
	s.mux.HandleFunc("GET /api/audit", s.auth(s.requireRole(userstore.RoleAuditor, s.handleAuditLog)))
	s.mux.HandleFunc("GET /api/admin/smtp/status", s.authAdmin(s.handleSMTPStatus))
	s.mux.HandleFunc("GET /api/admin/storage/stats", s.authAdmin(s.handleStorageStats))
	s.mux.HandleFunc("GET /api/mails/{id}", s.auth(s.requireMailAccess(s.handleGetMail)))
	s.mux.HandleFunc("GET /api/mails/{id}/attachments/{index}", s.auth(s.requireMailAccess(s.handleGetAttachment)))
	s.mux.HandleFunc("GET /api/threads/{threadID}", s.auth(s.requireMailAccess(s.handleGetThread)))
	s.mux.HandleFunc("GET /api/mails/{id}/raw", s.auth(s.requireMailAccess(s.handleGetRaw)))
	// PROJ-44: OCR-Text-Download — gleicher ACL-Pfad wie /raw.
	s.mux.HandleFunc("GET /api/mails/{id}/ocr-text", s.auth(s.requireMailAccess(s.handleGetOCRText)))
	s.mux.HandleFunc("GET /api/admin/services", s.authAdmin(s.handleListServices))
	s.mux.HandleFunc("POST /api/admin/services/{name}/action", s.authAdmin(s.handleServiceAction))

	s.mux.HandleFunc("GET /api/admin/system/stats", s.authAdmin(s.handleSystemStats))
	s.mux.HandleFunc("GET /api/admin/stats/timeseries", s.authAdmin(s.handleMailTimeseries))
	// PROJ-52: Vollständigkeits-Reconciliation (Dashboard + CSV-Export) — admin, tenant-scoped.
	s.mux.HandleFunc("GET /api/admin/reconciliation", s.authAdmin(s.handleReconciliation))
	s.mux.HandleFunc("GET /api/admin/reconciliation/export.csv", s.authAdmin(s.handleReconciliationExport))
	s.mux.HandleFunc("GET /api/admin/security/audit", s.authAdmin(s.handleSecurityAudit))
	// SEC-17: Security fix actions require superadmin, not just domain_admin.
	s.mux.HandleFunc("POST /api/admin/security/fix", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleSecurityFix)))

	// PROJ-34: Retention — superadmin only
	s.mux.HandleFunc("POST /api/admin/purge", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handlePurge)))
	s.mux.HandleFunc("GET /api/admin/retention", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleGetRetention)))
	s.mux.HandleFunc("PUT /api/admin/tenant/{id}/retention", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleSetTenantRetention)))
	// PROJ-51: Archiving rules (retention categories) CRUD — superadmin only.
	s.mux.HandleFunc("GET /api/admin/archiving-rules", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleListArchivingRules)))
	s.mux.HandleFunc("POST /api/admin/archiving-rules", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleCreateArchivingRule)))
	s.mux.HandleFunc("PUT /api/admin/archiving-rules/{id}", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleUpdateArchivingRule)))
	s.mux.HandleFunc("DELETE /api/admin/archiving-rules/{id}", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleDeleteArchivingRule)))
	// PROJ-43: Tenant routing rules CRUD + dry-run — domain_admin+, tenant-scoped
	// (superadmin sees all tenants, domain admins only their own).
	s.mux.HandleFunc("GET /api/admin/routing-rules", s.authAdmin(s.handleListRoutingRules))
	s.mux.HandleFunc("POST /api/admin/routing-rules", s.authAdmin(s.handleCreateRoutingRule))
	s.mux.HandleFunc("PUT /api/admin/routing-rules/{id}", s.authAdmin(s.handleUpdateRoutingRule))
	s.mux.HandleFunc("DELETE /api/admin/routing-rules/{id}", s.authAdmin(s.handleDeleteRoutingRule))
	s.mux.HandleFunc("POST /api/admin/routing-rules/dry-run", s.authAdmin(s.handleDryRunRoutingRule))

	// PROJ-56c: pro-Mail Löschmarkierung — domain_admin+, tenant-scoped (kein
	// Mail-Lesezugriff nötig, daher requireRole statt requireMailAccess).
	s.mux.HandleFunc("GET /api/admin/retention/expired", s.authAdmin(s.handleListExpiredMails))
	s.mux.HandleFunc("PUT /api/admin/mails/{id}/mark-deletion", s.authAdmin(s.handleSetMarkedForDeletion))

	// PROJ-50: DSGVO Löschersuchen — admin (manage) / auditor (read-only),
	// role enforced inside the handlers.
	s.mux.HandleFunc("POST /api/admin/dsgvo", s.auth(s.handleCreateDSGVORequest))
	s.mux.HandleFunc("GET /api/admin/dsgvo", s.auth(s.handleListDSGVORequests))
	s.mux.HandleFunc("GET /api/admin/dsgvo/{id}", s.auth(s.handleGetDSGVORequest))
	s.mux.HandleFunc("GET /api/admin/dsgvo/{id}/export", s.auth(s.handleExportDSGVORequest))
	s.mux.HandleFunc("POST /api/admin/dsgvo/{id}/delete", s.auth(s.handleDeleteDSGVOMails))

	// SMTP-Out Relay Konfiguration — superadmin only
	s.mux.HandleFunc("GET /api/admin/smtp-out", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleGetSMTPOut)))
	s.mux.HandleFunc("PUT /api/admin/smtp-out", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleSaveSMTPOut)))
	s.mux.HandleFunc("DELETE /api/admin/smtp-out", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleDeleteSMTPOut)))
	s.mux.HandleFunc("POST /api/admin/smtp-out/test", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleTestSMTPOut)))

	// PROJ-29: Quotas — superadmin only
	s.mux.HandleFunc("GET /api/admin/quotas", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleGetAllTenantUsage)))
	s.mux.HandleFunc("GET /api/admin/tenant/{id}/quota", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleGetTenantUsage)))
	s.mux.HandleFunc("PUT /api/admin/tenant/{id}/quota", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleSetTenantQuota)))
	// Spec-konforme Alias-Route (PROJ-29 Acceptance Criterion 4)
	s.mux.HandleFunc("GET /api/admin/tenants/{id}/usage", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleGetTenantUsage)))

	// PROJ-33: IMAP mode settings — domain_admin only
	s.mux.HandleFunc("GET /api/admin/settings/imap-mode", s.authAdmin(s.handleGetIMAPMode))
	s.mux.HandleFunc("PUT /api/admin/settings/imap-mode", s.authAdmin(s.handleSetIMAPMode))

	// PROJ-42: Gespeicherte Suchanfragen
	s.mux.HandleFunc("GET /api/searches/saved", s.auth(s.handleListSavedSearches))
	s.mux.HandleFunc("POST /api/searches/saved", s.auth(s.handleCreateSavedSearch))
	s.mux.HandleFunc("DELETE /api/searches/saved/{id}", s.auth(s.handleDeleteSavedSearch))

	// Export routes
	s.mux.HandleFunc("GET /api/export/pdf/{id}", s.auth(s.requireMailAccess(s.handleExportPDF)))
	s.mux.HandleFunc("POST /api/export/zip", s.auth(s.requireMailAccess(s.handleExportZIP)))
	s.mux.HandleFunc("POST /api/export/ediscovery", s.auth(s.requireMailAccess(s.handleExportEDiscovery)))

	// Upload routes (admin only)
	s.mux.HandleFunc("POST /api/admin/upload", s.authAdmin(s.handleUpload))
	s.mux.HandleFunc("GET /api/admin/upload/{jobID}/progress", s.authAdmin(s.handleUploadProgress))

	// Upload routes (all authenticated users)
	s.mux.HandleFunc("POST /api/upload", s.auth(s.handleUpload))
	s.mux.HandleFunc("GET /api/upload/{jobID}/progress", s.auth(s.handleUploadProgress))

	// IMAP routes (accessible to all authenticated users)
	s.mux.HandleFunc("GET /api/imap", s.auth(s.handleListImap))
	s.mux.HandleFunc("POST /api/imap", s.auth(s.handleCreateImap))
	s.mux.HandleFunc("DELETE /api/imap/{id}", s.auth(s.handleDeleteImap))
	s.mux.HandleFunc("PATCH /api/imap/{id}", s.auth(s.handleUpdateImapInterval))
	s.mux.HandleFunc("POST /api/imap/test", s.auth(s.handleTestImap))
	s.mux.HandleFunc("POST /api/imap/{id}/import", s.auth(s.handleStartImport))
	s.mux.HandleFunc("GET /api/imap/{id}/progress", s.auth(s.handleImapProgress))
	s.mux.HandleFunc("POST /api/imap/{id}/sync", s.auth(s.handleSyncNow))

	// POP3 routes (accessible to all authenticated users)
	s.mux.HandleFunc("GET /api/pop3", s.auth(s.handleListPop3))
	s.mux.HandleFunc("POST /api/pop3", s.auth(s.handleCreatePop3))
	s.mux.HandleFunc("DELETE /api/pop3/{id}", s.auth(s.handleDeletePop3))
	s.mux.HandleFunc("POST /api/pop3/test", s.auth(s.handleTestPop3))
	s.mux.HandleFunc("POST /api/pop3/{id}/import", s.auth(s.handleStartPop3Import))
	s.mux.HandleFunc("GET /api/pop3/{id}/progress", s.auth(s.handlePop3Progress))

	// PROJ-25: Profile routes (password & email change)
	s.mux.HandleFunc("PATCH /api/auth/password", s.auth(s.handleChangePassword))
	s.mux.HandleFunc("PATCH /api/auth/email", s.auth(s.handleChangeEmail))
	// PROJ-53: konfigurierbare Listenanzahl pro Seite
	s.mux.HandleFunc("PATCH /api/auth/preferences", s.auth(s.handleChangePreferences))

	// PROJ-24: TOTP 2FA routes
	s.mux.HandleFunc("GET /api/auth/totp/setup", s.auth(s.handleTOTPSetupGet))
	s.mux.HandleFunc("POST /api/auth/totp/setup", s.auth(s.handleTOTPSetupPost))
	s.mux.HandleFunc("DELETE /api/auth/totp", s.auth(s.handleTOTPDisable))
	s.mux.HandleFunc("POST /api/auth/totp", s.handleTOTPLogin) // no auth middleware — uses pending token
	s.mux.HandleFunc("POST /api/admin/users/{id}/totp/reset", s.authAdmin(s.handleTOTPReset))

	// PROJ-13: External REST API v1 (API-key auth)
	s.mux.HandleFunc("GET /api/v1/docs", s.handleV1Docs)
	s.mux.HandleFunc("/api/v1/mails", s.apiKeyMw.Wrap(s.handleV1SearchMails))
	s.mux.HandleFunc("GET /api/v1/mails/{message_id}", s.apiKeyMw.Wrap(s.handleV1GetMail))
	s.mux.HandleFunc("GET /api/v1/mails/{message_id}/raw", s.apiKeyMw.Wrap(s.handleV1GetMailRaw))
	// PROJ-13: Catch-all for non-GET methods on v1 single-mail endpoints
	s.mux.HandleFunc("POST /api/v1/mails/{message_id}", s.apiKeyMw.Wrap(s.handleV1MethodNotAllowed))
	s.mux.HandleFunc("PUT /api/v1/mails/{message_id}", s.apiKeyMw.Wrap(s.handleV1MethodNotAllowed))
	s.mux.HandleFunc("DELETE /api/v1/mails/{message_id}", s.apiKeyMw.Wrap(s.handleV1MethodNotAllowed))
	s.mux.HandleFunc("PATCH /api/v1/mails/{message_id}", s.apiKeyMw.Wrap(s.handleV1MethodNotAllowed))

	// PROJ-13: API key management (admin)
	s.mux.HandleFunc("POST /api/admin/apikeys", s.authAdmin(s.handleCreateAPIKey))
	s.mux.HandleFunc("GET /api/admin/apikeys", s.authAdmin(s.handleListAPIKeys))
	s.mux.HandleFunc("DELETE /api/admin/apikeys/{id}", s.authAdmin(s.handleDeleteAPIKey))

	// Certificate management routes (superadmin only)
	s.mux.HandleFunc("GET /api/admin/cert/info", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleCertInfo)))
	s.mux.HandleFunc("POST /api/admin/cert/upload", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleCertUpload)))
	s.mux.HandleFunc("POST /api/admin/cert/self-signed", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleCertSelfSigned)))
	s.mux.HandleFunc("POST /api/admin/cert/acme", s.auth(s.requireRole(userstore.RoleSuperAdmin, s.handleCertACME)))
}

// ServeHTTP implements http.Handler.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.mux.ServeHTTP(w, r)
}

// --- handlers ---


// --- middleware ---

const sessionCookieName = "archivmail_session"

func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Prefer httpOnly cookie; fall back to Bearer token for CLI/API clients.
		token := ""
		if c, err := r.Cookie(sessionCookieName); err == nil {
			token = c.Value
		}
		if token == "" {
			token = extractBearerToken(r)
		}
		if token == "" {
			writeError(w, http.StatusUnauthorized, "missing authorization")
			return
		}

		sess, err := s.authMgr.ValidateToken(token)
		if err != nil {
			writeError(w, http.StatusUnauthorized, "invalid or expired token")
			return
		}

		ctx := context.WithValue(r.Context(), sessionKey, sess)
		next(w, r.WithContext(ctx))
	}
}

func (s *Server) requireRole(role string, next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		sess := sessionFromCtx(r.Context())
		if sess == nil || !auth.HasRole(sess.Role, role) {
			writeError(w, http.StatusForbidden, "insufficient permissions")
			return
		}
		next(w, r)
	}
}

// ── Mail access middleware ────────────────────────────────────────────────

// requireMailAccess checks that the caller may read mail content.
// SEC-29: Strict separation of duties — admins manage, auditors review.
// Mail access is granted ONLY to: user, auditor, domain_auditor.
// superadmin and domain_admin are explicitly denied (manage system/tenant, not content).
func (s *Server) requireMailAccess(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		sess := sessionFromCtx(r.Context())
		if sess == nil {
			writeError(w, http.StatusUnauthorized, "not authenticated")
			return
		}
		// SEC-29: Admins must not access mail content.
		switch sess.Role {
		case userstore.RoleSuperAdmin, userstore.RoleDomainAdmin:
			writeError(w, http.StatusForbidden, "access denied")
			return
		}
		next(w, r)
	}
}

// --- helpers ---

func writeJSON(w http.ResponseWriter, code int, v interface{}) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, code int, msg string) {
	writeJSON(w, code, map[string]string{"error": msg})
}

func extractBearerToken(r *http.Request) string {
	h := r.Header.Get("Authorization")
	if strings.HasPrefix(h, "Bearer ") {
		return strings.TrimPrefix(h, "Bearer ")
	}
	return ""
}

func sessionFromCtx(ctx context.Context) *auth.Session {
	v := ctx.Value(sessionKey)
	if v == nil {
		return &auth.Session{}
	}
	if s, ok := v.(*auth.Session); ok {
		return s
	}
	return &auth.Session{}
}

// tenantMiddleware extracts the tenant_id from the session and stores it in
// the request context, making it available to all downstream handlers.
func (s *Server) tenantMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		session := sessionFromCtx(r.Context())
		if session != nil && session.TenantID != nil {
			ctx := context.WithValue(r.Context(), tenantKey, session.TenantID)
			next(w, r.WithContext(ctx))
			return
		}
		next(w, r)
	}
}

// tenantFromCtx extracts the tenant_id from context. Returns nil for global (superadmin) context.
func tenantFromCtx(ctx context.Context) *int64 {
	v, _ := ctx.Value(tenantKey).(*int64)
	return v
}

// sanitizeFilename strips characters that could be used for HTTP header injection
// (quotes, newlines, control chars) from attachment filenames coming from parsed
// e-mails. Only alphanumerics, spaces, dots, hyphens, and underscores are kept.
func sanitizeFilename(name string) string {
	var b strings.Builder
	for _, r := range name {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') ||
			r == '.' || r == '-' || r == '_' || r == ' ' {
			b.WriteRune(r)
		}
	}
	return b.String()
}

// classifyLoginError maps internal login errors to safe audit-log categories.
// Raw error messages must not be stored in audit logs since auditor-role
// users can read them via GET /api/audit and internal details (LDAP hostnames,
// port numbers, etc.) would be exposed.
func classifyLoginError(err error) string {
	if err == nil {
		return ""
	}
	msg := err.Error()
	switch {
	case strings.Contains(msg, "not found"), strings.Contains(msg, "invalid password"),
		strings.Contains(msg, "invalid credentials"):
		return "invalid_password"
	case strings.Contains(msg, "ldap"), strings.Contains(msg, "LDAP"):
		return "ldap_error"
	case strings.Contains(msg, "disabled"), strings.Contains(msg, "inactive"):
		return "account_disabled"
	default:
		return "unknown"
	}
}

// remoteIP returns the real client IP.
// X-Forwarded-For is only trusted when the direct connection comes from a
// configured trusted proxy. Otherwise r.RemoteAddr is used directly to prevent
// clients from spoofing their IP in audit logs and rate-limit counters.
func (s *Server) remoteIP(r *http.Request) string {
	directIP, _, _ := net.SplitHostPort(r.RemoteAddr)
	if directIP == "" {
		directIP = r.RemoteAddr
	}
	if len(s.cfg.TrustedProxies) > 0 && isTrustedProxy(directIP, s.cfg.TrustedProxies) {
		if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
			return strings.TrimSpace(strings.Split(fwd, ",")[0])
		}
	}
	return directIP
}

// isTrustedProxy checks whether ip is in the list of trusted proxy addresses/CIDRs.
func isTrustedProxy(ip string, proxies []string) bool {
	parsed := net.ParseIP(ip)
	for _, p := range proxies {
		if strings.Contains(p, "/") {
			_, cidr, err := net.ParseCIDR(p)
			if err == nil && cidr.Contains(parsed) {
				return true
			}
		} else if p == ip {
			return true
		}
	}
	return false
}
