"""Helpers for recording admin audit-log events.

All admin-sensitive code paths should call :func:`record_admin_event` with a
well-known ``action`` string and optional actor/target/details. The helper is
intentionally best-effort: failures to persist an audit row must never break
the underlying business flow, because that would turn observability into a
denial-of-service vector. Errors are logged and swallowed.

Action strings (keep this list in sync with the frontend filter dropdown):

- ``auth.login``                        — successful local sign-in
- ``auth.password_reset_completed``     — user completed forgot-password flow
- ``admin.user.role_changed``           — admin promoted or demoted a user
- ``admin.user.active_changed``         — admin enabled or disabled a user
- ``admin.user.password_reset``         — admin directly reset a user's password
"""

from __future__ import annotations

import json
import logging
from typing import Any, Mapping, Optional

from fastapi import Request
from models.audit_log import AdminAuditLog
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

# Canonical action strings. Exposed for routers and tests.
ACTION_LOGIN = "auth.login"
ACTION_PASSWORD_RESET_COMPLETED = "auth.password_reset_completed"
ACTION_ROLE_CHANGED = "admin.user.role_changed"
ACTION_ACTIVE_CHANGED = "admin.user.active_changed"
ACTION_ADMIN_PASSWORD_RESET = "admin.user.password_reset"
ACTION_CACHE_TOGGLED = "admin.cache.toggled"
ACTION_CACHE_CLEARED = "admin.cache.cleared"

KNOWN_ACTIONS: tuple[str, ...] = (
    ACTION_LOGIN,
    ACTION_PASSWORD_RESET_COMPLETED,
    ACTION_ROLE_CHANGED,
    ACTION_ACTIVE_CHANGED,
    ACTION_ADMIN_PASSWORD_RESET,
    ACTION_CACHE_TOGGLED,
    ACTION_CACHE_CLEARED,
)


def _client_ip(request: Optional[Request]) -> Optional[str]:
    """Best-effort client IP extraction (respects X-Forwarded-For)."""
    if request is None:
        return None
    xff = request.headers.get("x-forwarded-for")
    if xff:
        # First entry is the originating client per convention.
        return xff.split(",")[0].strip()[:64] or None
    client = request.client
    if client and client.host:
        return client.host[:64]
    return None


def _user_agent(request: Optional[Request]) -> Optional[str]:
    if request is None:
        return None
    ua = request.headers.get("user-agent")
    return ua[:512] if ua else None


def _serialize_details(details: Optional[Mapping[str, Any]]) -> Optional[str]:
    if not details:
        return None
    try:
        return json.dumps(dict(details), default=str, ensure_ascii=False)
    except (TypeError, ValueError) as exc:
        logger.warning("[audit_log] Failed to serialize details: %s", exc)
        return None


async def record_admin_event(
    db: AsyncSession,
    *,
    action: str,
    admin_user_id: Optional[str] = None,
    admin_email: Optional[str] = None,
    target_user_id: Optional[str] = None,
    target_email: Optional[str] = None,
    details: Optional[Mapping[str, Any]] = None,
    request: Optional[Request] = None,
) -> None:
    """Persist one admin audit-log row.

    This function never raises: audit-log failures must not break the caller.
    It also never commits the parent transaction; it issues its own commit so
    the event survives even if the caller later rolls back (e.g. business
    logic decides to fail after the event was interesting to record).
    """
    try:
        entry = AdminAuditLog(
            action=action,
            admin_user_id=admin_user_id,
            admin_email=(admin_email or "").strip().lower() or None,
            target_user_id=target_user_id,
            target_email=(target_email or "").strip().lower() or None,
            details=_serialize_details(details),
            ip_address=_client_ip(request),
            user_agent=_user_agent(request),
        )
        db.add(entry)
        await db.commit()
    except Exception:  # noqa: BLE001
        logger.exception("[audit_log] Failed to record event action=%s", action)
        try:
            await db.rollback()
        except Exception:  # noqa: BLE001
            logger.debug("[audit_log] Rollback after failed insert also failed")