package users

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/mail"
	"strings"
	"time"

	"github.com/niels/banking-app/backend/internal/audit"
	"github.com/niels/banking-app/backend/internal/authguard"
	"github.com/niels/banking-app/backend/internal/domain"
	"github.com/niels/banking-app/backend/internal/httpapi/middleware"
	"github.com/niels/banking-app/backend/internal/platform/httputil"
	"github.com/niels/banking-app/backend/internal/respond"
	"github.com/niels/banking-app/backend/internal/security"
)

const refreshTokenCookieName = "banking_refresh_token"
const csrfTokenCookieName = "banking_csrf_token"

type Handler struct {
	repo       *Repository
	audit      *audit.Repository
	tokens     *security.TokenManager
	secretBox  *security.SecretBox
	authGuard  *authguard.Repository
	issuer     string
	refreshTTL time.Duration
	stepUpTTL  time.Duration
	log        *slog.Logger
}

type registerRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
	FullName string `json:"full_name"`
}

type loginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type mfaCodeRequest struct {
	Code         string `json:"code"`
	RecoveryCode string `json:"recovery_code"`
}

type refreshRequest struct {
	RefreshToken string `json:"refresh_token"`
}

type passkeyRegistrationRequest struct {
	Challenge      string   `json:"challenge"`
	CredentialID   string   `json:"credential_id"`
	PublicKey      string   `json:"public_key"`
	Nickname       string   `json:"nickname"`
	Transports     []string `json:"transports"`
	AAGUID         string   `json:"aaguid"`
	SignCount      int64    `json:"sign_count"`
	BackupEligible bool     `json:"backup_eligible"`
	BackupState    bool     `json:"backup_state"`
}

type tokenResponse struct {
	AccessToken           string      `json:"access_token"`
	RefreshToken          string      `json:"refresh_token,omitempty"`
	RefreshTokenExpiresAt *time.Time  `json:"refresh_token_expires_at,omitempty"`
	SessionID             string      `json:"session_id,omitempty"`
	TokenType             string      `json:"token_type"`
	User                  domain.User `json:"user"`
}

func NewHandler(repo *Repository, auditRepo *audit.Repository, tokens *security.TokenManager, secretBox *security.SecretBox, issuer string, refreshTTL, stepUpTTL time.Duration, authGuard *authguard.Repository, log *slog.Logger) *Handler {
	return &Handler{
		repo:       repo,
		audit:      auditRepo,
		tokens:     tokens,
		secretBox:  secretBox,
		authGuard:  authGuard,
		issuer:     issuer,
		refreshTTL: refreshTTL,
		stepUpTTL:  stepUpTTL,
		log:        log,
	}
}

func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
	var req registerRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}

	if err := validateRegistration(req); err != nil {
		respond.Error(w, err)
		return
	}

	hash, err := security.HashPassword(req.Password)
	if err != nil {
		respond.Error(w, err)
		return
	}

	user, err := h.repo.Create(r.Context(), CreateParams{
		Email:        req.Email,
		FullName:     strings.TrimSpace(req.FullName),
		PasswordHash: hash,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}

	h.recordAudit(r, &user.ID, "user.registered", "user", user.ID, map[string]string{"email": user.Email})

	response, err := h.issueTokenResponse(r, user)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if err := setSessionCookies(w, r, response.RefreshToken, *response.RefreshTokenExpiresAt); err != nil {
		respond.Error(w, err)
		return
	}
	respond.Created(w, response)
}

func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
	var req loginRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}

	identifier := strings.ToLower(strings.TrimSpace(req.Email))
	remoteIP := httputil.RemoteIP(r)
	userAgent := r.UserAgent()
	if h.authGuard != nil {
		if err := h.authGuard.CheckLoginAllowed(r.Context(), identifier, remoteIP, userAgent); err != nil {
			respond.Error(w, err)
			return
		}
	}

	user, err := h.repo.FindByEmail(r.Context(), req.Email)
	if err != nil || !security.VerifyPassword(user.PasswordHash, req.Password) {
		userID := ""
		if err == nil {
			userID = user.ID
		}
		if h.authGuard != nil {
			if guardErr := h.authGuard.RecordLoginFailure(r.Context(), userID, identifier, remoteIP, userAgent, "invalid_credentials"); guardErr != nil {
				h.log.Error("login failure tracking failed", "error", guardErr)
			}
		}
		h.recordAudit(r, nil, "user.login_failed", "user", identifier, nil)
		respond.Error(w, domain.ErrInvalidCredentials)
		return
	}

	var securityResult authguard.LoginSecurityResult
	if h.authGuard != nil {
		securityResult, err = h.authGuard.RecordLoginSuccessAndDetect(r.Context(), user.ID, identifier, remoteIP, userAgent)
		if err != nil {
			h.log.Error("login success tracking failed", "error", err)
		}
	}
	auditMetadata := map[string]any{}
	if securityResult.Suspicious {
		auditMetadata["suspicious"] = true
		auditMetadata["reasons"] = securityResult.Reasons
	}
	h.recordAudit(r, &user.ID, "user.login_succeeded", "user", user.ID, auditMetadata)

	response, err := h.issueTokenResponse(r, user)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if err := setSessionCookies(w, r, response.RefreshToken, *response.RefreshTokenExpiresAt); err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, response)
}

func (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) {
	refreshToken, err := refreshTokenFromRequest(r)
	if err != nil {
		respond.Error(w, err)
		return
	}

	nextRefreshToken, err := security.GenerateRefreshToken()
	if err != nil {
		respond.Error(w, err)
		return
	}
	expiresAt := time.Now().UTC().Add(h.refreshTTL)
	rotated, err := h.repo.RotateSession(
		r.Context(),
		security.RefreshTokenHash(refreshToken),
		security.RefreshTokenHash(nextRefreshToken),
		r.UserAgent(),
		httputil.RemoteIP(r),
		expiresAt,
	)
	if err != nil {
		respond.Error(w, err)
		return
	}

	scopes, err := h.repo.EffectiveScopes(r.Context(), rotated.User)
	if err != nil {
		respond.Error(w, err)
		return
	}
	accessToken, err := h.tokens.GenerateWithScopes(rotated.User, scopes, time.Time{})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &rotated.User.ID, "auth.session.refreshed", "auth_session", rotated.Session.ID, nil)
	response := tokenResponse{
		AccessToken:           accessToken,
		RefreshToken:          nextRefreshToken,
		RefreshTokenExpiresAt: &rotated.Session.ExpiresAt,
		SessionID:             rotated.Session.ID,
		TokenType:             "Bearer",
		User:                  rotated.User,
	}
	if err := setSessionCookies(w, r, response.RefreshToken, *response.RefreshTokenExpiresAt); err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, response)
}

func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
	refreshToken, err := refreshTokenFromRequest(r)
	if err != nil {
		clearRefreshCookie(w, r)
		respond.NoContent(w)
		return
	}
	if err := h.repo.RevokeSessionByRefreshTokenHash(r.Context(), security.RefreshTokenHash(refreshToken), "logout"); err != nil && !errors.Is(err, domain.ErrNotFound) {
		respond.Error(w, err)
		return
	}
	clearRefreshCookie(w, r)
	respond.NoContent(w)
}

func (h *Handler) ListSessions(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	sessions, err := h.repo.ListSessions(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"sessions": sessions})
}

func (h *Handler) RevokeSession(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	sessionID := r.PathValue("id")
	if err := domain.ValidateUUID("id", sessionID); err != nil {
		respond.Error(w, err)
		return
	}
	if err := h.repo.RevokeSession(r.Context(), claims.Subject, sessionID, "user_revoked"); err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &claims.Subject, "auth.session.revoked", "auth_session", sessionID, nil)
	respond.NoContent(w)
}

func (h *Handler) Me(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}

	user, err := h.repo.FindByID(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}

	respond.JSON(w, http.StatusOK, user)
}

func (h *Handler) MFAStatus(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	settings, err := h.repo.GetMFASettings(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	recoveryCount, err := h.repo.ActiveRecoveryCodeCount(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{
		"totp_enabled":               settings.TOTPEnabled,
		"enabled_at":                 settings.EnabledAt,
		"recovery_codes_remaining":   recoveryCount,
		"recovery_codes_configured":  recoveryCount > 0,
		"passkeys_supported_backend": true,
	})
}

func (h *Handler) SetupTOTP(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	secret, err := security.GenerateTOTPSecret()
	if err != nil {
		respond.Error(w, err)
		return
	}
	ciphertext, err := h.secretBox.Encrypt(secret)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if _, err := h.repo.UpsertTOTPSetup(r.Context(), claims.Subject, ciphertext); err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &claims.Subject, "security.mfa_totp.setup", "user", claims.Subject, nil)
	respond.JSON(w, http.StatusOK, map[string]any{
		"secret":      secret,
		"otpauth_url": security.TOTPAuthURL(h.issuer, claims.Email, secret),
		"enabled":     false,
	})
}

func (h *Handler) EnableTOTP(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req mfaCodeRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	settings, err := h.repo.GetMFASettings(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	secret, err := h.decryptTOTPSecret(settings)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if !security.VerifyTOTP(secret, req.Code, time.Now().UTC()) {
		respond.Error(w, domain.ErrInvalidCredentials)
		return
	}
	settings, err = h.repo.EnableTOTP(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &claims.Subject, "security.mfa_totp.enabled", "user", claims.Subject, nil)
	respond.JSON(w, http.StatusOK, map[string]any{
		"totp_enabled": settings.TOTPEnabled,
		"enabled_at":   settings.EnabledAt,
	})
}

func (h *Handler) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	settings, err := h.repo.GetMFASettings(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	if !settings.TOTPEnabled {
		respond.Error(w, fmt.Errorf("%w: TOTP MFA must be enabled before recovery codes are generated", domain.ErrValidation))
		return
	}
	codes, err := security.GenerateRecoveryCodes(10)
	if err != nil {
		respond.Error(w, err)
		return
	}
	hashes := make([]string, 0, len(codes))
	for _, code := range codes {
		hashes = append(hashes, security.RecoveryCodeHash(code))
	}
	if err := h.repo.ReplaceRecoveryCodes(r.Context(), claims.Subject, hashes); err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &claims.Subject, "security.mfa_recovery_codes.generated", "user", claims.Subject, map[string]any{
		"count": len(codes),
	})
	respond.JSON(w, http.StatusOK, map[string]any{
		"recovery_codes": codes,
		"count":          len(codes),
	})
}

func (h *Handler) StepUp(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req mfaCodeRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	settings, err := h.repo.GetMFASettings(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	code := strings.TrimSpace(req.Code)
	if code == "" {
		code = strings.TrimSpace(req.RecoveryCode)
	}
	method := ""
	if settings.TOTPEnabled && code != "" {
		secret, err := h.decryptTOTPSecret(settings)
		if err != nil {
			respond.Error(w, err)
			return
		}
		if security.VerifyTOTP(secret, code, time.Now().UTC()) {
			method = "totp"
		}
	}
	if method == "" && code != "" {
		if err := h.repo.UseRecoveryCode(r.Context(), claims.Subject, security.RecoveryCodeHash(code), httputil.RemoteIP(r), r.UserAgent()); err != nil {
			respond.Error(w, domain.ErrInvalidCredentials)
			return
		}
		method = "recovery_code"
	}
	if method == "" {
		respond.Error(w, fmt.Errorf("%w: TOTP code or recovery_code is required", domain.ErrValidation))
		return
	}
	user, err := h.repo.FindByID(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	scopes, err := h.repo.EffectiveScopes(r.Context(), user)
	if err != nil {
		respond.Error(w, err)
		return
	}
	stepUpUntil := time.Now().UTC().Add(h.stepUpTTL)
	token, err := h.tokens.GenerateWithScopes(user, scopes, stepUpUntil)
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &claims.Subject, "security.step_up.succeeded", "user", claims.Subject, map[string]any{
		"step_up_until": stepUpUntil,
		"method":        method,
	})
	respond.JSON(w, http.StatusOK, map[string]any{
		"access_token":  token,
		"token_type":    "Bearer",
		"step_up_until": stepUpUntil,
		"user":          user,
	})
}

func (h *Handler) Passkeys(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	passkeys, err := h.repo.ListPasskeys(r.Context(), claims.Subject)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"passkeys": passkeys})
}

func (h *Handler) PasskeyRegistrationOptions(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	challenge, err := randomWebAuthnChallenge()
	if err != nil {
		respond.Error(w, err)
		return
	}
	expiresAt := time.Now().UTC().Add(10 * time.Minute)
	if err := h.repo.CreateWebAuthnChallenge(r.Context(), claims.Subject, security.ChallengeHash(challenge), "registration", expiresAt); err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{
		"challenge":  challenge,
		"expires_at": expiresAt,
		"rp_id":      h.issuer,
		"user": map[string]string{
			"id":    claims.Subject,
			"email": claims.Email,
		},
	})
}

func (h *Handler) RegisterPasskey(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req passkeyRegistrationRequest
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	if err := h.repo.ConsumeWebAuthnChallenge(r.Context(), claims.Subject, security.ChallengeHash(req.Challenge), "registration"); err != nil {
		respond.Error(w, err)
		return
	}
	passkey, err := h.repo.CreatePasskey(r.Context(), CreatePasskeyParams{
		UserID:         claims.Subject,
		CredentialID:   req.CredentialID,
		PublicKey:      req.PublicKey,
		Nickname:       req.Nickname,
		Transports:     req.Transports,
		AAGUID:         req.AAGUID,
		SignCount:      req.SignCount,
		BackupEligible: req.BackupEligible,
		BackupState:    req.BackupState,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &claims.Subject, "security.passkey.registered", "webauthn_credential", passkey.ID, map[string]any{
		"credential_id": passkey.CredentialID,
		"transports":    passkey.Transports,
	})
	respond.Created(w, passkey)
}

func (h *Handler) DeletePasskey(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	passkeyID := strings.TrimSpace(r.PathValue("id"))
	if err := domain.ValidateUUID("id", passkeyID); err != nil {
		respond.Error(w, err)
		return
	}
	if err := h.repo.RevokePasskey(r.Context(), claims.Subject, passkeyID); err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAudit(r, &claims.Subject, "security.passkey.revoked", "webauthn_credential", passkeyID, nil)
	respond.NoContent(w)
}

func (h *Handler) issueTokenResponse(r *http.Request, user domain.User) (tokenResponse, error) {
	scopes, err := h.repo.EffectiveScopes(r.Context(), user)
	if err != nil {
		return tokenResponse{}, err
	}
	accessToken, err := h.tokens.GenerateWithScopes(user, scopes, time.Time{})
	if err != nil {
		return tokenResponse{}, err
	}
	refreshToken, err := security.GenerateRefreshToken()
	if err != nil {
		return tokenResponse{}, err
	}
	expiresAt := time.Now().UTC().Add(h.refreshTTL)
	session, err := h.repo.CreateSession(r.Context(), CreateSessionParams{
		UserID:           user.ID,
		RefreshTokenHash: security.RefreshTokenHash(refreshToken),
		UserAgent:        r.UserAgent(),
		RemoteIP:         httputil.RemoteIP(r),
		ExpiresAt:        expiresAt,
	})
	if err != nil {
		return tokenResponse{}, err
	}
	return tokenResponse{
		AccessToken:           accessToken,
		RefreshToken:          refreshToken,
		RefreshTokenExpiresAt: &session.ExpiresAt,
		SessionID:             session.ID,
		TokenType:             "Bearer",
		User:                  user,
	}, nil
}

func refreshTokenFromRequest(r *http.Request) (string, error) {
	var req refreshRequest
	if r.Body != nil {
		decoder := json.NewDecoder(r.Body)
		if err := decoder.Decode(&req); err == nil {
			if token := strings.TrimSpace(req.RefreshToken); token != "" {
				return token, nil
			}
		} else if !errors.Is(err, io.EOF) {
			return "", fmt.Errorf("%w: invalid JSON body", domain.ErrValidation)
		}
	}

	cookie, err := r.Cookie(refreshTokenCookieName)
	if err == nil && strings.TrimSpace(cookie.Value) != "" {
		return strings.TrimSpace(cookie.Value), nil
	}
	return "", fmt.Errorf("%w: refresh_token is required", domain.ErrValidation)
}

func setSessionCookies(w http.ResponseWriter, r *http.Request, token string, expiresAt time.Time) error {
	secure := secureCookie(r)
	http.SetCookie(w, &http.Cookie{
		Name:     refreshTokenCookieName,
		Value:    token,
		Path:     "/v1/auth",
		Expires:  expiresAt,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   secure,
	})
	csrfToken, err := security.RandomHex(32)
	if err != nil {
		return err
	}
	http.SetCookie(w, &http.Cookie{
		Name:     csrfTokenCookieName,
		Value:    csrfToken,
		Path:     "/",
		Expires:  expiresAt,
		HttpOnly: false,
		SameSite: http.SameSiteLaxMode,
		Secure:   secure,
	})
	return nil
}

func clearRefreshCookie(w http.ResponseWriter, r *http.Request) {
	secure := secureCookie(r)
	http.SetCookie(w, &http.Cookie{
		Name:     refreshTokenCookieName,
		Value:    "",
		Path:     "/v1/auth",
		MaxAge:   -1,
		Expires:  time.Unix(0, 0),
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   secure,
	})
	http.SetCookie(w, &http.Cookie{
		Name:     csrfTokenCookieName,
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		Expires:  time.Unix(0, 0),
		HttpOnly: false,
		SameSite: http.SameSiteLaxMode,
		Secure:   secure,
	})
}

func secureCookie(r *http.Request) bool {
	return r.TLS != nil ||
		strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") ||
		strings.EqualFold(r.Header.Get("X-Forwarded-Ssl"), "on")
}

func (h *Handler) decryptTOTPSecret(settings MFASettings) (string, error) {
	if settings.TOTPSecretCiphertext == "" {
		return "", fmt.Errorf("%w: TOTP setup is required", domain.ErrValidation)
	}
	return h.secretBox.Decrypt(settings.TOTPSecretCiphertext)
}

func randomWebAuthnChallenge() (string, error) {
	raw := make([]byte, 32)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}

func validateRegistration(req registerRequest) error {
	if _, err := mail.ParseAddress(strings.TrimSpace(req.Email)); err != nil {
		return fmt.Errorf("%w: email must be valid", domain.ErrValidation)
	}
	if len(req.Password) < 12 {
		return fmt.Errorf("%w: password must be at least 12 characters", domain.ErrValidation)
	}
	if strings.TrimSpace(req.FullName) == "" {
		return fmt.Errorf("%w: full_name is required", domain.ErrValidation)
	}
	return nil
}

func (h *Handler) recordAudit(r *http.Request, actorID *string, eventType, targetType, targetID string, metadata any) {
	err := h.audit.Record(r.Context(), audit.Event{
		ActorUserID: actorID,
		EventType:   eventType,
		TargetType:  targetType,
		TargetID:    targetID,
		Metadata:    metadata,
		RemoteIP:    httputil.RemoteIP(r),
		UserAgent:   r.UserAgent(),
	})
	if err != nil {
		h.log.Error("audit record failed", "error", err)
	}
}
