package admin

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/jackc/pgx/v5"

	"github.com/niels/banking-app/backend/internal/audit"
	"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"
)

type MFAResetRequest struct {
	ID                   string     `json:"id"`
	TargetUserID         string     `json:"target_user_id"`
	TargetUserEmail      string     `json:"target_user_email,omitempty"`
	RequesterAdminUserID string     `json:"requester_admin_user_id"`
	RequesterName        string     `json:"requester_name,omitempty"`
	ReviewerAdminUserID  string     `json:"reviewer_admin_user_id,omitempty"`
	ReviewerName         string     `json:"reviewer_name,omitempty"`
	Status               string     `json:"status"`
	Reason               string     `json:"reason"`
	DecisionNote         string     `json:"decision_note,omitempty"`
	DecidedAt            *time.Time `json:"decided_at,omitempty"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
}

type AdminRoleChangeRequest struct {
	ID                   string     `json:"id"`
	TargetUserID         string     `json:"target_user_id"`
	TargetUserEmail      string     `json:"target_user_email,omitempty"`
	RequesterAdminUserID string     `json:"requester_admin_user_id"`
	RequesterName        string     `json:"requester_name,omitempty"`
	ReviewerAdminUserID  string     `json:"reviewer_admin_user_id,omitempty"`
	ReviewerName         string     `json:"reviewer_name,omitempty"`
	RequestedRole        string     `json:"requested_role"`
	RequestedScopes      []string   `json:"requested_scopes"`
	Status               string     `json:"status"`
	Reason               string     `json:"reason"`
	DecisionNote         string     `json:"decision_note,omitempty"`
	DecidedAt            *time.Time `json:"decided_at,omitempty"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
}

type AdminScopeAssignment struct {
	ID                   string     `json:"id"`
	UserID               string     `json:"user_id"`
	UserEmail            string     `json:"user_email,omitempty"`
	Scope                string     `json:"scope"`
	Status               string     `json:"status"`
	Source               string     `json:"source"`
	GrantedByAdminUserID string     `json:"granted_by_admin_user_id,omitempty"`
	RevokedByAdminUserID string     `json:"revoked_by_admin_user_id,omitempty"`
	Reason               string     `json:"reason,omitempty"`
	ExpiresAt            *time.Time `json:"expires_at,omitempty"`
	RevokedAt            *time.Time `json:"revoked_at,omitempty"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
}

type AdminJITElevationRequest struct {
	ID                   string     `json:"id"`
	AdminUserID          string     `json:"admin_user_id"`
	AdminUserEmail       string     `json:"admin_user_email,omitempty"`
	RequesterAdminUserID string     `json:"requester_admin_user_id"`
	RequesterName        string     `json:"requester_name,omitempty"`
	ReviewerAdminUserID  string     `json:"reviewer_admin_user_id,omitempty"`
	ReviewerName         string     `json:"reviewer_name,omitempty"`
	Scopes               []string   `json:"scopes"`
	Status               string     `json:"status"`
	Reason               string     `json:"reason"`
	DecisionNote         string     `json:"decision_note,omitempty"`
	StartsAt             *time.Time `json:"starts_at,omitempty"`
	ExpiresAt            *time.Time `json:"expires_at,omitempty"`
	RevokedAt            *time.Time `json:"revoked_at,omitempty"`
	DecidedAt            *time.Time `json:"decided_at,omitempty"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
}

type AdminSession struct {
	ID                  string     `json:"id"`
	UserID              string     `json:"user_id"`
	UserEmail           string     `json:"user_email,omitempty"`
	UserAgent           string     `json:"user_agent,omitempty"`
	RemoteIP            string     `json:"remote_ip,omitempty"`
	ExpiresAt           time.Time  `json:"expires_at"`
	RevokedAt           *time.Time `json:"revoked_at,omitempty"`
	RevokeReason        string     `json:"revoke_reason,omitempty"`
	ReplacedBySessionID string     `json:"replaced_by_session_id,omitempty"`
	LastUsedAt          *time.Time `json:"last_used_at,omitempty"`
	CreatedAt           time.Time  `json:"created_at"`
	UpdatedAt           time.Time  `json:"updated_at"`
}

type AuthControlDecisionParams struct {
	RequestID    string
	ActorAdminID string
	Action       string
	DecisionNote string
}

type CreateMFAResetRequestParams struct {
	TargetUserID string
	AdminUserID  string
	Reason       string
}

type CreateRoleChangeRequestParams struct {
	TargetUserID string
	AdminUserID  string
	Role         string
	Scopes       []string
	Reason       string
}

type CreateJITElevationRequestParams struct {
	AdminUserID      string
	RequesterAdminID string
	Scopes           []string
	Reason           string
	ExpiresAt        time.Time
}

type ScopeGrantParams struct {
	UserID       string
	ActorAdminID string
	Scope        string
	Reason       string
	ExpiresAt    *time.Time
}

type roleChangeRequestBody struct {
	TargetUserID string   `json:"target_user_id"`
	Role         string   `json:"role"`
	Scopes       []string `json:"scopes"`
	Reason       string   `json:"reason"`
}

type mfaResetRequestBody struct {
	TargetUserID string `json:"target_user_id"`
	Reason       string `json:"reason"`
}

type authControlDecisionBody struct {
	DecisionNote string `json:"decision_note"`
}

type jitElevationRequestBody struct {
	AdminUserID string   `json:"admin_user_id"`
	Scopes      []string `json:"scopes"`
	Reason      string   `json:"reason"`
	ExpiresAt   string   `json:"expires_at"`
}

type scopeGrantRequestBody struct {
	Scope     string `json:"scope"`
	Reason    string `json:"reason"`
	ExpiresAt string `json:"expires_at"`
}

type sessionRevokeRequestBody struct {
	Reason string `json:"reason"`
}

func (r *Repository) ListMFAResetRequests(ctx context.Context, status string, limit int) ([]MFAResetRequest, error) {
	status, err := normalizeApprovalStatus(status)
	if err != nil {
		return nil, err
	}
	limit = normalizeLimit(limit)
	rows, err := r.db.Query(ctx, mfaResetSelect+`
		WHERE ($1 = '' OR req.status = $1)
		ORDER BY CASE WHEN req.status = 'pending' THEN 0 ELSE 1 END, req.created_at DESC
		LIMIT $2
	`, status, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	requests := []MFAResetRequest{}
	for rows.Next() {
		request, err := scanMFAResetRequest(rows)
		if err != nil {
			return nil, err
		}
		requests = append(requests, request)
	}
	return requests, rows.Err()
}

func (r *Repository) CreateMFAResetRequest(ctx context.Context, params CreateMFAResetRequestParams) (MFAResetRequest, error) {
	if err := normalizeMFAResetCreate(&params); err != nil {
		return MFAResetRequest{}, err
	}
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return MFAResetRequest{}, err
	}
	defer tx.Rollback(ctx)
	if err := requireLiveAdmin(ctx, tx, params.AdminUserID); err != nil {
		return MFAResetRequest{}, err
	}
	if err := requireUserExists(ctx, tx, params.TargetUserID); err != nil {
		return MFAResetRequest{}, err
	}

	var requestID string
	if err := tx.QueryRow(ctx, `
		INSERT INTO mfa_reset_requests (target_user_id, requester_admin_user_id, reason)
		VALUES ($1, $2, $3)
		RETURNING id::text
	`, params.TargetUserID, params.AdminUserID, params.Reason).Scan(&requestID); err != nil {
		return MFAResetRequest{}, err
	}
	request, err := findMFAResetRequest(ctx, tx, requestID, false)
	if err != nil {
		return MFAResetRequest{}, err
	}
	return request, tx.Commit(ctx)
}

func (r *Repository) DecideMFAResetRequest(ctx context.Context, params AuthControlDecisionParams) (MFAResetRequest, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return MFAResetRequest{}, err
	}
	defer tx.Rollback(ctx)
	request, err := findMFAResetRequest(ctx, tx, params.RequestID, true)
	if err != nil {
		return MFAResetRequest{}, err
	}
	note, err := validateApprovalDecision(request.Status, request.RequesterAdminUserID, params.ActorAdminID, params.Action, params.DecisionNote)
	if err != nil {
		return MFAResetRequest{}, err
	}
	if err := requireLiveAdmin(ctx, tx, params.ActorAdminID); err != nil {
		return MFAResetRequest{}, err
	}

	switch strings.ToLower(strings.TrimSpace(params.Action)) {
	case "approve":
		if _, err := tx.Exec(ctx, `
			UPDATE user_mfa_settings
			SET totp_secret_ciphertext = NULL,
				totp_enabled = false,
				enabled_at = NULL,
				updated_at = now()
			WHERE user_id = $1
		`, request.TargetUserID); err != nil {
			return MFAResetRequest{}, err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE user_mfa_recovery_codes
			SET replaced_at = now()
			WHERE user_id = $1
				AND used_at IS NULL
				AND replaced_at IS NULL
		`, request.TargetUserID); err != nil {
			return MFAResetRequest{}, err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE auth_sessions
			SET revoked_at = now(),
				revoke_reason = 'mfa_reset',
				updated_at = now()
			WHERE user_id = $1
				AND revoked_at IS NULL
		`, request.TargetUserID); err != nil {
			return MFAResetRequest{}, err
		}
		if err := insertSessionSecurityEvent(ctx, tx, request.TargetUserID, request.TargetUserEmail, params.ActorAdminID, "mfa_reset", note); err != nil {
			return MFAResetRequest{}, err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE mfa_reset_requests
			SET status = 'approved', reviewer_admin_user_id = $1, decision_note = NULLIF($2, ''), decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return MFAResetRequest{}, err
		}
	case "reject":
		if _, err := tx.Exec(ctx, `
			UPDATE mfa_reset_requests
			SET status = 'rejected', reviewer_admin_user_id = $1, decision_note = $2, decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return MFAResetRequest{}, err
		}
	case "cancel":
		if _, err := tx.Exec(ctx, `
			UPDATE mfa_reset_requests
			SET status = 'canceled', decision_note = $1, decided_at = now()
			WHERE id = $2
		`, note, request.ID); err != nil {
			return MFAResetRequest{}, err
		}
	}
	updated, err := findMFAResetRequest(ctx, tx, request.ID, false)
	if err != nil {
		return MFAResetRequest{}, err
	}
	return updated, tx.Commit(ctx)
}

func (r *Repository) ListRoleChangeRequests(ctx context.Context, status string, limit int) ([]AdminRoleChangeRequest, error) {
	status, err := normalizeApprovalStatus(status)
	if err != nil {
		return nil, err
	}
	limit = normalizeLimit(limit)
	rows, err := r.db.Query(ctx, roleChangeSelect+`
		WHERE ($1 = '' OR req.status = $1)
		ORDER BY CASE WHEN req.status = 'pending' THEN 0 ELSE 1 END, req.created_at DESC
		LIMIT $2
	`, status, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	requests := []AdminRoleChangeRequest{}
	for rows.Next() {
		request, err := scanRoleChangeRequest(rows)
		if err != nil {
			return nil, err
		}
		requests = append(requests, request)
	}
	return requests, rows.Err()
}

func (r *Repository) CreateRoleChangeRequest(ctx context.Context, params CreateRoleChangeRequestParams) (AdminRoleChangeRequest, error) {
	scopes, err := normalizeRoleChangeCreate(&params)
	if err != nil {
		return AdminRoleChangeRequest{}, err
	}
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return AdminRoleChangeRequest{}, err
	}
	defer tx.Rollback(ctx)
	if err := requireLiveAdmin(ctx, tx, params.AdminUserID); err != nil {
		return AdminRoleChangeRequest{}, err
	}
	if err := requireUserExists(ctx, tx, params.TargetUserID); err != nil {
		return AdminRoleChangeRequest{}, err
	}

	var requestID string
	if err := tx.QueryRow(ctx, `
		INSERT INTO admin_role_change_requests (
			target_user_id, requester_admin_user_id, requested_role, requested_scopes, reason
		)
		VALUES ($1, $2, $3, $4, $5)
		RETURNING id::text
	`, params.TargetUserID, params.AdminUserID, params.Role, scopes, params.Reason).Scan(&requestID); err != nil {
		return AdminRoleChangeRequest{}, err
	}
	request, err := findRoleChangeRequest(ctx, tx, requestID, false)
	if err != nil {
		return AdminRoleChangeRequest{}, err
	}
	return request, tx.Commit(ctx)
}

func (r *Repository) DecideRoleChangeRequest(ctx context.Context, params AuthControlDecisionParams) (AdminRoleChangeRequest, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return AdminRoleChangeRequest{}, err
	}
	defer tx.Rollback(ctx)
	request, err := findRoleChangeRequest(ctx, tx, params.RequestID, true)
	if err != nil {
		return AdminRoleChangeRequest{}, err
	}
	note, err := validateApprovalDecision(request.Status, request.RequesterAdminUserID, params.ActorAdminID, params.Action, params.DecisionNote)
	if err != nil {
		return AdminRoleChangeRequest{}, err
	}
	if err := requireLiveAdmin(ctx, tx, params.ActorAdminID); err != nil {
		return AdminRoleChangeRequest{}, err
	}

	switch strings.ToLower(strings.TrimSpace(params.Action)) {
	case "approve":
		if _, err := tx.Exec(ctx, `UPDATE users SET role = $1 WHERE id = $2`, request.RequestedRole, request.TargetUserID); err != nil {
			return AdminRoleChangeRequest{}, err
		}
		if err := applyAdminScopeSet(ctx, tx, request.TargetUserID, params.ActorAdminID, request.RequestedRole, request.RequestedScopes, "role_change:"+request.ID); err != nil {
			return AdminRoleChangeRequest{}, err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE admin_role_change_requests
			SET status = 'approved', reviewer_admin_user_id = $1, decision_note = NULLIF($2, ''), decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return AdminRoleChangeRequest{}, err
		}
	case "reject":
		if _, err := tx.Exec(ctx, `
			UPDATE admin_role_change_requests
			SET status = 'rejected', reviewer_admin_user_id = $1, decision_note = $2, decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return AdminRoleChangeRequest{}, err
		}
	case "cancel":
		if _, err := tx.Exec(ctx, `
			UPDATE admin_role_change_requests
			SET status = 'canceled', decision_note = $1, decided_at = now()
			WHERE id = $2
		`, note, request.ID); err != nil {
			return AdminRoleChangeRequest{}, err
		}
	}
	updated, err := findRoleChangeRequest(ctx, tx, request.ID, false)
	if err != nil {
		return AdminRoleChangeRequest{}, err
	}
	return updated, tx.Commit(ctx)
}

func (r *Repository) ListAdminScopes(ctx context.Context, userID string) ([]AdminScopeAssignment, error) {
	if err := domain.ValidateUUID("user_id", userID); err != nil {
		return nil, err
	}
	rows, err := r.db.Query(ctx, adminScopeSelect+`
		WHERE asa.user_id = $1
		ORDER BY CASE WHEN asa.status = 'active' THEN 0 ELSE 1 END, asa.scope
	`, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	assignments := []AdminScopeAssignment{}
	for rows.Next() {
		assignment, err := scanAdminScopeAssignment(rows)
		if err != nil {
			return nil, err
		}
		assignments = append(assignments, assignment)
	}
	return assignments, rows.Err()
}

func (r *Repository) GrantAdminScope(ctx context.Context, params ScopeGrantParams) (AdminScopeAssignment, error) {
	scope, err := normalizeScopeGrant(&params)
	if err != nil {
		return AdminScopeAssignment{}, err
	}
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return AdminScopeAssignment{}, err
	}
	defer tx.Rollback(ctx)
	if err := requireLiveAdmin(ctx, tx, params.ActorAdminID); err != nil {
		return AdminScopeAssignment{}, err
	}
	if err := requireTargetAdmin(ctx, tx, params.UserID); err != nil {
		return AdminScopeAssignment{}, err
	}
	if _, err := tx.Exec(ctx, `
		INSERT INTO admin_scope_assignments (
			user_id, scope, status, source, granted_by_admin_user_id, reason, expires_at
		)
		VALUES ($1, $2, 'active', 'manual', $3, $4, $5)
		ON CONFLICT DO NOTHING
	`, params.UserID, scope, params.ActorAdminID, params.Reason, params.ExpiresAt); err != nil {
		return AdminScopeAssignment{}, err
	}
	assignment, err := findActiveAdminScope(ctx, tx, params.UserID, scope)
	if err != nil {
		return AdminScopeAssignment{}, err
	}
	return assignment, tx.Commit(ctx)
}

func (r *Repository) RevokeAdminScope(ctx context.Context, userID, scope, actorAdminID, reason string) (AdminScopeAssignment, error) {
	userID = strings.TrimSpace(userID)
	actorAdminID = strings.TrimSpace(actorAdminID)
	scopes, err := security.NormalizeScopes([]string{scope})
	if err != nil {
		return AdminScopeAssignment{}, err
	}
	if err := domain.ValidateUUID("user_id", userID); err != nil {
		return AdminScopeAssignment{}, err
	}
	if err := domain.ValidateUUID("actor_admin_id", actorAdminID); err != nil {
		return AdminScopeAssignment{}, err
	}
	reason = strings.TrimSpace(reason)
	if len(reason) < 8 || len(reason) > 500 {
		return AdminScopeAssignment{}, fmt.Errorf("%w: reason must be between 8 and 500 characters", domain.ErrValidation)
	}
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return AdminScopeAssignment{}, err
	}
	defer tx.Rollback(ctx)
	if err := requireLiveAdmin(ctx, tx, actorAdminID); err != nil {
		return AdminScopeAssignment{}, err
	}
	if _, err := tx.Exec(ctx, `
		UPDATE admin_scope_assignments
		SET status = 'revoked',
			revoked_by_admin_user_id = $3,
			revoked_at = now(),
			reason = $4
		WHERE user_id = $1
			AND scope = $2
			AND status = 'active'
	`, userID, scopes[0], actorAdminID, reason); err != nil {
		return AdminScopeAssignment{}, err
	}
	assignment, err := findLatestAdminScope(ctx, tx, userID, scopes[0])
	if err != nil {
		return AdminScopeAssignment{}, err
	}
	return assignment, tx.Commit(ctx)
}

func (r *Repository) ListJITElevationRequests(ctx context.Context, status string, limit int) ([]AdminJITElevationRequest, error) {
	status, err := normalizeJITStatus(status)
	if err != nil {
		return nil, err
	}
	limit = normalizeLimit(limit)
	rows, err := r.db.Query(ctx, jitSelect+`
		WHERE ($1 = '' OR req.status = $1)
		ORDER BY CASE WHEN req.status = 'pending' THEN 0 ELSE 1 END, req.created_at DESC
		LIMIT $2
	`, status, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	requests := []AdminJITElevationRequest{}
	for rows.Next() {
		request, err := scanJITRequest(rows)
		if err != nil {
			return nil, err
		}
		requests = append(requests, request)
	}
	return requests, rows.Err()
}

func (r *Repository) CreateJITElevationRequest(ctx context.Context, params CreateJITElevationRequestParams) (AdminJITElevationRequest, error) {
	scopes, err := normalizeJITCreate(&params)
	if err != nil {
		return AdminJITElevationRequest{}, err
	}
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return AdminJITElevationRequest{}, err
	}
	defer tx.Rollback(ctx)
	if err := requireLiveAdmin(ctx, tx, params.RequesterAdminID); err != nil {
		return AdminJITElevationRequest{}, err
	}
	if err := requireTargetAdmin(ctx, tx, params.AdminUserID); err != nil {
		return AdminJITElevationRequest{}, err
	}
	var requestID string
	if err := tx.QueryRow(ctx, `
		INSERT INTO admin_jit_elevation_requests (
			admin_user_id, requester_admin_user_id, scopes, reason, expires_at
		)
		VALUES ($1, $2, $3, $4, $5)
		RETURNING id::text
	`, params.AdminUserID, params.RequesterAdminID, scopes, params.Reason, params.ExpiresAt).Scan(&requestID); err != nil {
		return AdminJITElevationRequest{}, err
	}
	request, err := findJITRequest(ctx, tx, requestID, false)
	if err != nil {
		return AdminJITElevationRequest{}, err
	}
	return request, tx.Commit(ctx)
}

func (r *Repository) DecideJITElevationRequest(ctx context.Context, params AuthControlDecisionParams) (AdminJITElevationRequest, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return AdminJITElevationRequest{}, err
	}
	defer tx.Rollback(ctx)
	request, err := findJITRequest(ctx, tx, params.RequestID, true)
	if err != nil {
		return AdminJITElevationRequest{}, err
	}
	note, err := validateJITDecision(request, params)
	if err != nil {
		return AdminJITElevationRequest{}, err
	}
	if err := requireLiveAdmin(ctx, tx, params.ActorAdminID); err != nil {
		return AdminJITElevationRequest{}, err
	}
	switch strings.ToLower(strings.TrimSpace(params.Action)) {
	case "approve":
		if _, err := tx.Exec(ctx, `
			UPDATE admin_jit_elevation_requests
			SET status = 'approved',
				reviewer_admin_user_id = $1,
				decision_note = NULLIF($2, ''),
				starts_at = now(),
				decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return AdminJITElevationRequest{}, err
		}
	case "reject":
		if _, err := tx.Exec(ctx, `
			UPDATE admin_jit_elevation_requests
			SET status = 'rejected', reviewer_admin_user_id = $1, decision_note = $2, decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return AdminJITElevationRequest{}, err
		}
	case "cancel":
		if _, err := tx.Exec(ctx, `
			UPDATE admin_jit_elevation_requests
			SET status = 'canceled', decision_note = $1, decided_at = now()
			WHERE id = $2
		`, note, request.ID); err != nil {
			return AdminJITElevationRequest{}, err
		}
	case "revoke":
		if _, err := tx.Exec(ctx, `
			UPDATE admin_jit_elevation_requests
			SET status = 'revoked', reviewer_admin_user_id = $1, decision_note = $2, revoked_at = now(), decided_at = now()
			WHERE id = $3
		`, params.ActorAdminID, note, request.ID); err != nil {
			return AdminJITElevationRequest{}, err
		}
	}
	updated, err := findJITRequest(ctx, tx, request.ID, false)
	if err != nil {
		return AdminJITElevationRequest{}, err
	}
	return updated, tx.Commit(ctx)
}

func (r *Repository) ListUserSessions(ctx context.Context, userID string, limit int) ([]AdminSession, error) {
	if err := domain.ValidateUUID("user_id", userID); err != nil {
		return nil, err
	}
	limit = normalizeLimit(limit)
	rows, err := r.db.Query(ctx, adminSessionSelect+`
		WHERE s.user_id = $1
			AND s.expires_at > now() - interval '30 days'
		ORDER BY s.created_at DESC
		LIMIT $2
	`, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	sessions := []AdminSession{}
	for rows.Next() {
		session, err := scanAdminSession(rows)
		if err != nil {
			return nil, err
		}
		sessions = append(sessions, session)
	}
	return sessions, rows.Err()
}

func (r *Repository) RevokeUserSession(ctx context.Context, actorAdminID, userID, sessionID, reason string) (AdminSession, error) {
	if err := normalizeSessionRevocation(actorAdminID, userID, sessionID, &reason); err != nil {
		return AdminSession{}, err
	}
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return AdminSession{}, err
	}
	defer tx.Rollback(ctx)
	if err := requireLiveAdmin(ctx, tx, actorAdminID); err != nil {
		return AdminSession{}, err
	}
	if _, err := tx.Exec(ctx, `
		UPDATE auth_sessions
		SET revoked_at = now(),
			revoke_reason = $3,
			updated_at = now()
		WHERE id = $1
			AND user_id = $2
			AND revoked_at IS NULL
	`, sessionID, userID, reason); err != nil {
		return AdminSession{}, err
	}
	session, err := findAdminSession(ctx, tx, sessionID)
	if err != nil {
		return AdminSession{}, err
	}
	if session.RevokedAt == nil {
		return AdminSession{}, domain.ErrNotFound
	}
	if err := insertSessionSecurityEvent(ctx, tx, userID, session.UserEmail, actorAdminID, "admin_session_revoke", reason); err != nil {
		return AdminSession{}, err
	}
	return session, tx.Commit(ctx)
}

func (h *Handler) MFAResetRequests(w http.ResponseWriter, r *http.Request) {
	requests, err := h.repo.ListMFAResetRequests(r.Context(), strings.TrimSpace(r.URL.Query().Get("status")), queryLimit(r))
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"mfa_reset_requests": requests})
}

func (h *Handler) CreateMFAResetRequest(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req mfaResetRequestBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	request, err := h.repo.CreateMFAResetRequest(r.Context(), CreateMFAResetRequestParams{
		TargetUserID: req.TargetUserID,
		AdminUserID:  claims.Subject,
		Reason:       req.Reason,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.mfa_reset_request.created", "mfa_reset_request", request.ID, request)
	respond.Created(w, request)
}

func (h *Handler) ApproveMFAResetRequest(w http.ResponseWriter, r *http.Request) {
	h.decideMFAResetRequest(w, r, "approve")
}

func (h *Handler) RejectMFAResetRequest(w http.ResponseWriter, r *http.Request) {
	h.decideMFAResetRequest(w, r, "reject")
}

func (h *Handler) CancelMFAResetRequest(w http.ResponseWriter, r *http.Request) {
	h.decideMFAResetRequest(w, r, "cancel")
}

func (h *Handler) decideMFAResetRequest(w http.ResponseWriter, r *http.Request, action string) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req authControlDecisionBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	request, err := h.repo.DecideMFAResetRequest(r.Context(), AuthControlDecisionParams{
		RequestID:    strings.TrimSpace(r.PathValue("id")),
		ActorAdminID: claims.Subject,
		Action:       action,
		DecisionNote: req.DecisionNote,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.mfa_reset_request."+decisionEvent(action), "mfa_reset_request", request.ID, request)
	respond.JSON(w, http.StatusOK, request)
}

func (h *Handler) RoleChangeRequests(w http.ResponseWriter, r *http.Request) {
	requests, err := h.repo.ListRoleChangeRequests(r.Context(), strings.TrimSpace(r.URL.Query().Get("status")), queryLimit(r))
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"admin_role_change_requests": requests})
}

func (h *Handler) CreateRoleChangeRequest(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req roleChangeRequestBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	request, err := h.repo.CreateRoleChangeRequest(r.Context(), CreateRoleChangeRequestParams{
		TargetUserID: req.TargetUserID,
		AdminUserID:  claims.Subject,
		Role:         req.Role,
		Scopes:       req.Scopes,
		Reason:       req.Reason,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.role_change_request.created", "admin_role_change_request", request.ID, request)
	respond.Created(w, request)
}

func (h *Handler) ApproveRoleChangeRequest(w http.ResponseWriter, r *http.Request) {
	h.decideRoleChangeRequest(w, r, "approve")
}

func (h *Handler) RejectRoleChangeRequest(w http.ResponseWriter, r *http.Request) {
	h.decideRoleChangeRequest(w, r, "reject")
}

func (h *Handler) CancelRoleChangeRequest(w http.ResponseWriter, r *http.Request) {
	h.decideRoleChangeRequest(w, r, "cancel")
}

func (h *Handler) decideRoleChangeRequest(w http.ResponseWriter, r *http.Request, action string) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req authControlDecisionBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	request, err := h.repo.DecideRoleChangeRequest(r.Context(), AuthControlDecisionParams{
		RequestID:    strings.TrimSpace(r.PathValue("id")),
		ActorAdminID: claims.Subject,
		Action:       action,
		DecisionNote: req.DecisionNote,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.role_change_request."+decisionEvent(action), "admin_role_change_request", request.ID, request)
	respond.JSON(w, http.StatusOK, request)
}

func (h *Handler) AdminScopes(w http.ResponseWriter, r *http.Request) {
	userID := strings.TrimSpace(r.PathValue("id"))
	assignments, err := h.repo.ListAdminScopes(r.Context(), userID)
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"admin_scope_assignments": assignments})
}

func (h *Handler) GrantAdminScope(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req scopeGrantRequestBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	expiresAt, err := parseOptionalRFC3339("expires_at", req.ExpiresAt)
	if err != nil {
		respond.Error(w, err)
		return
	}
	assignment, err := h.repo.GrantAdminScope(r.Context(), ScopeGrantParams{
		UserID:       strings.TrimSpace(r.PathValue("id")),
		ActorAdminID: claims.Subject,
		Scope:        req.Scope,
		Reason:       req.Reason,
		ExpiresAt:    expiresAt,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.scope.granted", "admin_scope_assignment", assignment.ID, assignment)
	respond.Created(w, assignment)
}

func (h *Handler) RevokeAdminScope(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req scopeGrantRequestBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	assignment, err := h.repo.RevokeAdminScope(
		r.Context(),
		strings.TrimSpace(r.PathValue("id")),
		strings.TrimSpace(r.PathValue("scope")),
		claims.Subject,
		req.Reason,
	)
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.scope.revoked", "admin_scope_assignment", assignment.ID, assignment)
	respond.JSON(w, http.StatusOK, assignment)
}

func (h *Handler) JITElevationRequests(w http.ResponseWriter, r *http.Request) {
	requests, err := h.repo.ListJITElevationRequests(r.Context(), strings.TrimSpace(r.URL.Query().Get("status")), queryLimit(r))
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"admin_jit_elevation_requests": requests})
}

func (h *Handler) CreateJITElevationRequest(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req jitElevationRequestBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	adminUserID := strings.TrimSpace(req.AdminUserID)
	if adminUserID == "" {
		adminUserID = claims.Subject
	}
	expiresAt, err := parseRequiredRFC3339("expires_at", req.ExpiresAt)
	if err != nil {
		respond.Error(w, err)
		return
	}
	request, err := h.repo.CreateJITElevationRequest(r.Context(), CreateJITElevationRequestParams{
		AdminUserID:      adminUserID,
		RequesterAdminID: claims.Subject,
		Scopes:           req.Scopes,
		Reason:           req.Reason,
		ExpiresAt:        expiresAt,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.jit_elevation_request.created", "admin_jit_elevation_request", request.ID, request)
	respond.Created(w, request)
}

func (h *Handler) ApproveJITElevationRequest(w http.ResponseWriter, r *http.Request) {
	h.decideJITElevationRequest(w, r, "approve")
}

func (h *Handler) RejectJITElevationRequest(w http.ResponseWriter, r *http.Request) {
	h.decideJITElevationRequest(w, r, "reject")
}

func (h *Handler) CancelJITElevationRequest(w http.ResponseWriter, r *http.Request) {
	h.decideJITElevationRequest(w, r, "cancel")
}

func (h *Handler) RevokeJITElevationRequest(w http.ResponseWriter, r *http.Request) {
	h.decideJITElevationRequest(w, r, "revoke")
}

func (h *Handler) decideJITElevationRequest(w http.ResponseWriter, r *http.Request, action string) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req authControlDecisionBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	request, err := h.repo.DecideJITElevationRequest(r.Context(), AuthControlDecisionParams{
		RequestID:    strings.TrimSpace(r.PathValue("id")),
		ActorAdminID: claims.Subject,
		Action:       action,
		DecisionNote: req.DecisionNote,
	})
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.jit_elevation_request."+decisionEvent(action), "admin_jit_elevation_request", request.ID, request)
	respond.JSON(w, http.StatusOK, request)
}

func (h *Handler) UserSessions(w http.ResponseWriter, r *http.Request) {
	sessions, err := h.repo.ListUserSessions(r.Context(), strings.TrimSpace(r.PathValue("id")), queryLimit(r))
	if err != nil {
		respond.Error(w, err)
		return
	}
	respond.JSON(w, http.StatusOK, map[string]any{"sessions": sessions})
}

func (h *Handler) RevokeUserSession(w http.ResponseWriter, r *http.Request) {
	claims, ok := middleware.CurrentClaims(r)
	if !ok {
		respond.Error(w, domain.ErrUnauthorized)
		return
	}
	var req sessionRevokeRequestBody
	if err := respond.DecodeJSON(r, &req); err != nil {
		respond.Error(w, err)
		return
	}
	session, err := h.repo.RevokeUserSession(
		r.Context(),
		claims.Subject,
		strings.TrimSpace(r.PathValue("id")),
		strings.TrimSpace(r.PathValue("sessionID")),
		req.Reason,
	)
	if err != nil {
		respond.Error(w, err)
		return
	}
	h.recordAdminControlAudit(r, claims.Subject, "admin.auth_session.revoked", "auth_session", session.ID, session)
	respond.JSON(w, http.StatusOK, session)
}

const mfaResetSelect = `
	SELECT req.id::text, req.target_user_id::text, target.email,
		req.requester_admin_user_id::text, requester.full_name,
		COALESCE(req.reviewer_admin_user_id::text, ''), COALESCE(reviewer.full_name, ''),
		req.status, req.reason, COALESCE(req.decision_note, ''),
		req.decided_at, req.created_at, req.updated_at
	FROM mfa_reset_requests req
	JOIN users target ON target.id = req.target_user_id
	JOIN users requester ON requester.id = req.requester_admin_user_id
	LEFT JOIN users reviewer ON reviewer.id = req.reviewer_admin_user_id
`

const roleChangeSelect = `
	SELECT req.id::text, req.target_user_id::text, target.email,
		req.requester_admin_user_id::text, requester.full_name,
		COALESCE(req.reviewer_admin_user_id::text, ''), COALESCE(reviewer.full_name, ''),
		req.requested_role, req.requested_scopes, req.status, req.reason, COALESCE(req.decision_note, ''),
		req.decided_at, req.created_at, req.updated_at
	FROM admin_role_change_requests req
	JOIN users target ON target.id = req.target_user_id
	JOIN users requester ON requester.id = req.requester_admin_user_id
	LEFT JOIN users reviewer ON reviewer.id = req.reviewer_admin_user_id
`

const adminScopeSelect = `
	SELECT asa.id::text, asa.user_id::text, u.email, asa.scope, asa.status, asa.source,
		COALESCE(asa.granted_by_admin_user_id::text, ''), COALESCE(asa.revoked_by_admin_user_id::text, ''),
		asa.reason, asa.expires_at, asa.revoked_at, asa.created_at, asa.updated_at
	FROM admin_scope_assignments asa
	JOIN users u ON u.id = asa.user_id
`

const jitSelect = `
	SELECT req.id::text, req.admin_user_id::text, admin_user.email,
		req.requester_admin_user_id::text, requester.full_name,
		COALESCE(req.reviewer_admin_user_id::text, ''), COALESCE(reviewer.full_name, ''),
		req.scopes, req.status, req.reason, COALESCE(req.decision_note, ''),
		req.starts_at, req.expires_at, req.revoked_at, req.decided_at, req.created_at, req.updated_at
	FROM admin_jit_elevation_requests req
	JOIN users admin_user ON admin_user.id = req.admin_user_id
	JOIN users requester ON requester.id = req.requester_admin_user_id
	LEFT JOIN users reviewer ON reviewer.id = req.reviewer_admin_user_id
`

const adminSessionSelect = `
	SELECT s.id::text, s.user_id::text, u.email, COALESCE(s.user_agent, ''), COALESCE(s.remote_ip, ''),
		s.expires_at, s.revoked_at, COALESCE(s.revoke_reason, ''), COALESCE(s.replaced_by_session_id::text, ''),
		s.last_used_at, s.created_at, s.updated_at
	FROM auth_sessions s
	JOIN users u ON u.id = s.user_id
`

func findMFAResetRequest(ctx context.Context, tx pgx.Tx, requestID string, forUpdate bool) (MFAResetRequest, error) {
	if err := domain.ValidateUUID("id", strings.TrimSpace(requestID)); err != nil {
		return MFAResetRequest{}, err
	}
	query := mfaResetSelect + ` WHERE req.id = $1`
	if forUpdate {
		query += ` FOR UPDATE OF req`
	}
	request, err := scanMFAResetRequest(tx.QueryRow(ctx, query, requestID))
	if errors.Is(err, pgx.ErrNoRows) {
		return MFAResetRequest{}, domain.ErrNotFound
	}
	return request, err
}

func findRoleChangeRequest(ctx context.Context, tx pgx.Tx, requestID string, forUpdate bool) (AdminRoleChangeRequest, error) {
	if err := domain.ValidateUUID("id", strings.TrimSpace(requestID)); err != nil {
		return AdminRoleChangeRequest{}, err
	}
	query := roleChangeSelect + ` WHERE req.id = $1`
	if forUpdate {
		query += ` FOR UPDATE OF req`
	}
	request, err := scanRoleChangeRequest(tx.QueryRow(ctx, query, requestID))
	if errors.Is(err, pgx.ErrNoRows) {
		return AdminRoleChangeRequest{}, domain.ErrNotFound
	}
	return request, err
}

func findJITRequest(ctx context.Context, tx pgx.Tx, requestID string, forUpdate bool) (AdminJITElevationRequest, error) {
	if err := domain.ValidateUUID("id", strings.TrimSpace(requestID)); err != nil {
		return AdminJITElevationRequest{}, err
	}
	query := jitSelect + ` WHERE req.id = $1`
	if forUpdate {
		query += ` FOR UPDATE OF req`
	}
	request, err := scanJITRequest(tx.QueryRow(ctx, query, requestID))
	if errors.Is(err, pgx.ErrNoRows) {
		return AdminJITElevationRequest{}, domain.ErrNotFound
	}
	return request, err
}

func findActiveAdminScope(ctx context.Context, tx pgx.Tx, userID, scope string) (AdminScopeAssignment, error) {
	assignment, err := scanAdminScopeAssignment(tx.QueryRow(ctx, adminScopeSelect+`
		WHERE asa.user_id = $1 AND asa.scope = $2 AND asa.status = 'active'
	`, userID, scope))
	if errors.Is(err, pgx.ErrNoRows) {
		return AdminScopeAssignment{}, domain.ErrNotFound
	}
	return assignment, err
}

func findLatestAdminScope(ctx context.Context, tx pgx.Tx, userID, scope string) (AdminScopeAssignment, error) {
	assignment, err := scanAdminScopeAssignment(tx.QueryRow(ctx, adminScopeSelect+`
		WHERE asa.user_id = $1 AND asa.scope = $2
		ORDER BY asa.updated_at DESC
		LIMIT 1
	`, userID, scope))
	if errors.Is(err, pgx.ErrNoRows) {
		return AdminScopeAssignment{}, domain.ErrNotFound
	}
	return assignment, err
}

func findAdminSession(ctx context.Context, tx pgx.Tx, sessionID string) (AdminSession, error) {
	session, err := scanAdminSession(tx.QueryRow(ctx, adminSessionSelect+` WHERE s.id = $1`, sessionID))
	if errors.Is(err, pgx.ErrNoRows) {
		return AdminSession{}, domain.ErrNotFound
	}
	return session, err
}

func scanMFAResetRequest(row scanner) (MFAResetRequest, error) {
	var request MFAResetRequest
	var decidedAt sql.NullTime
	err := row.Scan(
		&request.ID,
		&request.TargetUserID,
		&request.TargetUserEmail,
		&request.RequesterAdminUserID,
		&request.RequesterName,
		&request.ReviewerAdminUserID,
		&request.ReviewerName,
		&request.Status,
		&request.Reason,
		&request.DecisionNote,
		&decidedAt,
		&request.CreatedAt,
		&request.UpdatedAt,
	)
	if decidedAt.Valid {
		request.DecidedAt = &decidedAt.Time
	}
	return request, err
}

func scanRoleChangeRequest(row scanner) (AdminRoleChangeRequest, error) {
	var request AdminRoleChangeRequest
	var decidedAt sql.NullTime
	err := row.Scan(
		&request.ID,
		&request.TargetUserID,
		&request.TargetUserEmail,
		&request.RequesterAdminUserID,
		&request.RequesterName,
		&request.ReviewerAdminUserID,
		&request.ReviewerName,
		&request.RequestedRole,
		&request.RequestedScopes,
		&request.Status,
		&request.Reason,
		&request.DecisionNote,
		&decidedAt,
		&request.CreatedAt,
		&request.UpdatedAt,
	)
	if decidedAt.Valid {
		request.DecidedAt = &decidedAt.Time
	}
	return request, err
}

func scanAdminScopeAssignment(row scanner) (AdminScopeAssignment, error) {
	var assignment AdminScopeAssignment
	var expiresAt sql.NullTime
	var revokedAt sql.NullTime
	err := row.Scan(
		&assignment.ID,
		&assignment.UserID,
		&assignment.UserEmail,
		&assignment.Scope,
		&assignment.Status,
		&assignment.Source,
		&assignment.GrantedByAdminUserID,
		&assignment.RevokedByAdminUserID,
		&assignment.Reason,
		&expiresAt,
		&revokedAt,
		&assignment.CreatedAt,
		&assignment.UpdatedAt,
	)
	if expiresAt.Valid {
		assignment.ExpiresAt = &expiresAt.Time
	}
	if revokedAt.Valid {
		assignment.RevokedAt = &revokedAt.Time
	}
	return assignment, err
}

func scanJITRequest(row scanner) (AdminJITElevationRequest, error) {
	var request AdminJITElevationRequest
	var startsAt sql.NullTime
	var expiresAt sql.NullTime
	var revokedAt sql.NullTime
	var decidedAt sql.NullTime
	err := row.Scan(
		&request.ID,
		&request.AdminUserID,
		&request.AdminUserEmail,
		&request.RequesterAdminUserID,
		&request.RequesterName,
		&request.ReviewerAdminUserID,
		&request.ReviewerName,
		&request.Scopes,
		&request.Status,
		&request.Reason,
		&request.DecisionNote,
		&startsAt,
		&expiresAt,
		&revokedAt,
		&decidedAt,
		&request.CreatedAt,
		&request.UpdatedAt,
	)
	if startsAt.Valid {
		request.StartsAt = &startsAt.Time
	}
	if expiresAt.Valid {
		request.ExpiresAt = &expiresAt.Time
	}
	if revokedAt.Valid {
		request.RevokedAt = &revokedAt.Time
	}
	if decidedAt.Valid {
		request.DecidedAt = &decidedAt.Time
	}
	return request, err
}

func scanAdminSession(row scanner) (AdminSession, error) {
	var session AdminSession
	var revokedAt sql.NullTime
	var lastUsedAt sql.NullTime
	err := row.Scan(
		&session.ID,
		&session.UserID,
		&session.UserEmail,
		&session.UserAgent,
		&session.RemoteIP,
		&session.ExpiresAt,
		&revokedAt,
		&session.RevokeReason,
		&session.ReplacedBySessionID,
		&lastUsedAt,
		&session.CreatedAt,
		&session.UpdatedAt,
	)
	if revokedAt.Valid {
		session.RevokedAt = &revokedAt.Time
	}
	if lastUsedAt.Valid {
		session.LastUsedAt = &lastUsedAt.Time
	}
	return session, err
}

func normalizeMFAResetCreate(params *CreateMFAResetRequestParams) error {
	params.TargetUserID = strings.TrimSpace(params.TargetUserID)
	params.AdminUserID = strings.TrimSpace(params.AdminUserID)
	params.Reason = strings.TrimSpace(params.Reason)
	if err := domain.ValidateUUID("target_user_id", params.TargetUserID); err != nil {
		return err
	}
	if err := domain.ValidateUUID("admin_user_id", params.AdminUserID); err != nil {
		return err
	}
	if len(params.Reason) < 12 || len(params.Reason) > 500 {
		return fmt.Errorf("%w: reason must be between 12 and 500 characters", domain.ErrValidation)
	}
	return nil
}

func normalizeRoleChangeCreate(params *CreateRoleChangeRequestParams) ([]string, error) {
	params.TargetUserID = strings.TrimSpace(params.TargetUserID)
	params.AdminUserID = strings.TrimSpace(params.AdminUserID)
	params.Role = strings.ToLower(strings.TrimSpace(params.Role))
	params.Reason = strings.TrimSpace(params.Reason)
	if err := domain.ValidateUUID("target_user_id", params.TargetUserID); err != nil {
		return nil, err
	}
	if err := domain.ValidateUUID("admin_user_id", params.AdminUserID); err != nil {
		return nil, err
	}
	if params.Role != "admin" && params.Role != "customer" {
		return nil, fmt.Errorf("%w: role must be admin or customer", domain.ErrValidation)
	}
	if len(params.Reason) < 12 || len(params.Reason) > 500 {
		return nil, fmt.Errorf("%w: reason must be between 12 and 500 characters", domain.ErrValidation)
	}
	scopes, err := security.NormalizeScopes(params.Scopes)
	if err != nil {
		return nil, err
	}
	if params.Role == "customer" && len(scopes) > 0 {
		return nil, fmt.Errorf("%w: customer role cannot have admin scope assignments", domain.ErrValidation)
	}
	return scopes, nil
}

func normalizeScopeGrant(params *ScopeGrantParams) (string, error) {
	params.UserID = strings.TrimSpace(params.UserID)
	params.ActorAdminID = strings.TrimSpace(params.ActorAdminID)
	params.Reason = strings.TrimSpace(params.Reason)
	if err := domain.ValidateUUID("user_id", params.UserID); err != nil {
		return "", err
	}
	if err := domain.ValidateUUID("actor_admin_id", params.ActorAdminID); err != nil {
		return "", err
	}
	scopes, err := security.NormalizeScopes([]string{params.Scope})
	if err != nil {
		return "", err
	}
	if len(params.Reason) < 8 || len(params.Reason) > 500 {
		return "", fmt.Errorf("%w: reason must be between 8 and 500 characters", domain.ErrValidation)
	}
	if params.ExpiresAt != nil && !params.ExpiresAt.After(time.Now().UTC()) {
		return "", fmt.Errorf("%w: expires_at must be in the future", domain.ErrValidation)
	}
	return scopes[0], nil
}

func normalizeJITCreate(params *CreateJITElevationRequestParams) ([]string, error) {
	params.AdminUserID = strings.TrimSpace(params.AdminUserID)
	params.RequesterAdminID = strings.TrimSpace(params.RequesterAdminID)
	params.Reason = strings.TrimSpace(params.Reason)
	if err := domain.ValidateUUID("admin_user_id", params.AdminUserID); err != nil {
		return nil, err
	}
	if err := domain.ValidateUUID("requester_admin_id", params.RequesterAdminID); err != nil {
		return nil, err
	}
	if len(params.Reason) < 12 || len(params.Reason) > 500 {
		return nil, fmt.Errorf("%w: reason must be between 12 and 500 characters", domain.ErrValidation)
	}
	scopes, err := security.NormalizeScopes(params.Scopes)
	if err != nil {
		return nil, err
	}
	if len(scopes) == 0 {
		return nil, fmt.Errorf("%w: at least one scope is required", domain.ErrValidation)
	}
	now := time.Now().UTC()
	if !params.ExpiresAt.After(now) {
		return nil, fmt.Errorf("%w: expires_at must be in the future", domain.ErrValidation)
	}
	if params.ExpiresAt.After(now.Add(8 * time.Hour)) {
		return nil, fmt.Errorf("%w: JIT elevation cannot exceed 8 hours", domain.ErrValidation)
	}
	return scopes, nil
}

func normalizeSessionRevocation(actorAdminID, userID, sessionID string, reason *string) error {
	if err := domain.ValidateUUID("actor_admin_id", strings.TrimSpace(actorAdminID)); err != nil {
		return err
	}
	if err := domain.ValidateUUID("user_id", strings.TrimSpace(userID)); err != nil {
		return err
	}
	if err := domain.ValidateUUID("session_id", strings.TrimSpace(sessionID)); err != nil {
		return err
	}
	*reason = strings.TrimSpace(*reason)
	if len(*reason) < 8 || len(*reason) > 500 {
		return fmt.Errorf("%w: reason must be between 8 and 500 characters", domain.ErrValidation)
	}
	return nil
}

func normalizeApprovalStatus(status string) (string, error) {
	status = strings.ToLower(strings.TrimSpace(status))
	if status == "" || status == "pending" || status == "approved" || status == "rejected" || status == "canceled" {
		return status, nil
	}
	return "", fmt.Errorf("%w: invalid request status", domain.ErrValidation)
}

func normalizeJITStatus(status string) (string, error) {
	status = strings.ToLower(strings.TrimSpace(status))
	switch status {
	case "", "pending", "approved", "rejected", "canceled", "revoked", "expired":
		return status, nil
	default:
		return "", fmt.Errorf("%w: invalid JIT request status", domain.ErrValidation)
	}
}

func validateApprovalDecision(status, requesterID, actorID, action, rawNote string) (string, error) {
	action = strings.ToLower(strings.TrimSpace(action))
	note := strings.TrimSpace(rawNote)
	if status != "pending" {
		return "", fmt.Errorf("%w: only pending requests can be decided", domain.ErrValidation)
	}
	if action == "approve" || action == "reject" {
		if strings.TrimSpace(actorID) == strings.TrimSpace(requesterID) {
			return "", fmt.Errorf("%w: requester cannot approve or reject their own request", domain.ErrForbidden)
		}
	}
	if action == "cancel" && strings.TrimSpace(actorID) != strings.TrimSpace(requesterID) {
		return "", fmt.Errorf("%w: only the requester can cancel this request", domain.ErrForbidden)
	}
	if action != "approve" && action != "reject" && action != "cancel" {
		return "", fmt.Errorf("%w: action must be approve, reject or cancel", domain.ErrValidation)
	}
	if (action == "reject" || action == "cancel") && (len(note) < 8 || len(note) > 500) {
		return "", fmt.Errorf("%w: decision_note must be between 8 and 500 characters", domain.ErrValidation)
	}
	if action == "approve" && len(note) > 500 {
		return "", fmt.Errorf("%w: decision_note must be 500 characters or fewer", domain.ErrValidation)
	}
	return note, nil
}

func validateJITDecision(request AdminJITElevationRequest, params AuthControlDecisionParams) (string, error) {
	action := strings.ToLower(strings.TrimSpace(params.Action))
	note := strings.TrimSpace(params.DecisionNote)
	if action == "approve" || action == "reject" {
		return validateApprovalDecision(request.Status, request.RequesterAdminUserID, params.ActorAdminID, action, note)
	}
	if action == "cancel" {
		return validateApprovalDecision(request.Status, request.RequesterAdminUserID, params.ActorAdminID, action, note)
	}
	if action == "revoke" {
		if request.Status != "approved" {
			return "", fmt.Errorf("%w: only approved JIT elevations can be revoked", domain.ErrValidation)
		}
		if len(note) < 8 || len(note) > 500 {
			return "", fmt.Errorf("%w: decision_note must be between 8 and 500 characters", domain.ErrValidation)
		}
		return note, nil
	}
	return "", fmt.Errorf("%w: action must be approve, reject, cancel or revoke", domain.ErrValidation)
}

func requireUserExists(ctx context.Context, tx pgx.Tx, userID string) error {
	var exists bool
	if err := tx.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM users WHERE id = $1)`, userID).Scan(&exists); err != nil {
		return err
	}
	if !exists {
		return domain.ErrNotFound
	}
	return nil
}

func requireTargetAdmin(ctx context.Context, tx pgx.Tx, userID string) error {
	var role string
	err := tx.QueryRow(ctx, `SELECT role FROM users WHERE id = $1 FOR SHARE`, userID).Scan(&role)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.ErrNotFound
	}
	if err != nil {
		return err
	}
	if role != "admin" {
		return fmt.Errorf("%w: target user must have admin role", domain.ErrValidation)
	}
	return nil
}

func applyAdminScopeSet(ctx context.Context, tx pgx.Tx, userID, actorAdminID, role string, scopes []string, reason string) error {
	if role != "admin" {
		_, err := tx.Exec(ctx, `
			UPDATE admin_scope_assignments
			SET status = 'revoked',
				revoked_by_admin_user_id = $2,
				revoked_at = now()
			WHERE user_id = $1
				AND status = 'active'
		`, userID, actorAdminID)
		return err
	}
	if _, err := tx.Exec(ctx, `
		UPDATE admin_scope_assignments
		SET status = 'revoked',
			revoked_by_admin_user_id = $2,
			revoked_at = now()
		WHERE user_id = $1
			AND status = 'active'
			AND NOT (scope = ANY($3::text[]))
	`, userID, actorAdminID, scopes); err != nil {
		return err
	}
	for _, scope := range scopes {
		if _, err := tx.Exec(ctx, `
			INSERT INTO admin_scope_assignments (user_id, scope, status, source, granted_by_admin_user_id, reason)
			VALUES ($1, $2, 'active', 'role_change', $3, $4)
			ON CONFLICT DO NOTHING
		`, userID, scope, actorAdminID, reason); err != nil {
			return err
		}
	}
	return nil
}

func insertSessionSecurityEvent(ctx context.Context, tx pgx.Tx, userID, email, actorAdminID, reason, note string) error {
	if strings.TrimSpace(email) == "" {
		email = userID
	}
	_, err := tx.Exec(ctx, `
		INSERT INTO security_events (user_id, event_type, severity, identifier, remote_ip, user_agent, details)
		VALUES (
			$1, 'session_remote_revoked', 'high', $2, 'admin_action', '',
			jsonb_build_object('actor_admin_user_id', $3, 'reason', $4, 'note', $5)
		)
	`, userID, email, actorAdminID, reason, note)
	return err
}

func parseRequiredRFC3339(field, raw string) (time.Time, error) {
	parsed, err := parseOptionalRFC3339(field, raw)
	if err != nil {
		return time.Time{}, err
	}
	if parsed == nil {
		return time.Time{}, fmt.Errorf("%w: %s is required", domain.ErrValidation, field)
	}
	return *parsed, nil
}

func parseOptionalRFC3339(field, raw string) (*time.Time, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	parsed, err := time.Parse(time.RFC3339, raw)
	if err != nil {
		return nil, fmt.Errorf("%w: %s must be RFC3339", domain.ErrValidation, field)
	}
	parsed = parsed.UTC()
	return &parsed, nil
}

func decisionEvent(action string) string {
	switch strings.ToLower(strings.TrimSpace(action)) {
	case "approve":
		return "approved"
	case "reject":
		return "rejected"
	case "cancel":
		return "canceled"
	case "revoke":
		return "revoked"
	default:
		return "updated"
	}
}

func (h *Handler) recordAdminControlAudit(r *http.Request, actorID, eventType, targetType, targetID string, metadata any) {
	if h.audit == nil {
		return
	}
	if 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(),
	}); err != nil && h.log != nil {
		h.log.Error("audit record failed", "error", err)
	}
}
