"""Append-only audit events (PostgreSQL audit_events table)."""

from __future__ import annotations

import enum
import logging
import uuid
from typing import Any, Mapping, Optional, Sequence

from sqlalchemy import select
from sqlalchemy.exc import InvalidRequestError, ProgrammingError
from sqlalchemy.orm import object_session
from sqlalchemy.ext.asyncio import AsyncSession

from src.initiative_db.engine import get_session_factory, init_engine, is_postgres_enabled

logger = logging.getLogger(__name__)


class AuditAction(str, enum.Enum):
    create = "create"
    read = "read"
    update = "update"
    delete = "delete"
    login = "login"
    logout = "logout"
    login_failed = "login_failed"


def _audit_table_missing_error(exc: BaseException) -> bool:
    parts: list[str] = [str(exc)]
    orig = getattr(exc, "orig", None)
    if orig is not None:
        parts.append(str(orig))
    chain = getattr(exc, "__cause__", None)
    if chain is not None:
        parts.append(str(chain))
    blob = " ".join(parts).lower()
    return "audit_events" in blob and "does not exist" in blob


async def record_audit(
    session: AsyncSession,
    *,
    actor_user_id: Optional[uuid.UUID],
    actor_email: str,
    actor_role: str,
    action: AuditAction,
    entity_type: str,
    entity_id: Optional[str] = None,
    before: Optional[dict[str, Any]] = None,
    after: Optional[dict[str, Any]] = None,
    metadata: Optional[dict[str, Any]] = None,
    request_id: Optional[uuid.UUID] = None,
) -> None:
    """
    Insert one audit row in a SAVEPOINT so missing ``audit_events`` (migration not applied)
    does not abort the surrounding business transaction.
    """
    from src.initiative_db.models import AuditEvent

    row = AuditEvent(
        actor_user_id=actor_user_id,
        actor_email=actor_email,
        actor_role=actor_role,
        action=action.value,
        entity_type=entity_type,
        entity_id=entity_id,
        before=before,
        after=after,
        metadata_=metadata or {},
        request_id=request_id,
    )
    try:
        async with session.begin_nested():
            session.add(row)
            await session.flush()
    except ProgrammingError as e:
        if _audit_table_missing_error(e):
            # If the ORM row is still tracked, drop it so outer commit() does not retry the INSERT.
            if object_session(row) is session:
                try:
                    session.expunge(row)
                except InvalidRequestError:
                    pass
            logger.warning(
                "audit_events table missing — apply be0/migrations/008_audit_events.sql; skipping audit (%s)",
                action.value,
            )
            return
        raise


async def persist_audit_standalone(
    *,
    actor_user_id: Optional[uuid.UUID],
    actor_email: str,
    actor_role: str,
    action: AuditAction,
    entity_type: str,
    entity_id: Optional[str] = None,
    before: Optional[dict[str, Any]] = None,
    after: Optional[dict[str, Any]] = None,
    metadata: Optional[dict[str, Any]] = None,
    request_id: Optional[uuid.UUID] = None,
) -> None:
    """
    Commit a single audit row in its own transaction (e.g. failed login where the
    main request transaction rolls back).
    """
    if not is_postgres_enabled():
        return
    from src.initiative_db.models import AuditEvent

    await init_engine()
    factory = get_session_factory()
    async with factory() as session:
        session.add(
            AuditEvent(
                actor_user_id=actor_user_id,
                actor_email=actor_email,
                actor_role=actor_role,
                action=action.value,
                entity_type=entity_type,
                entity_id=entity_id,
                before=before,
                after=after,
                metadata_=metadata or {},
                request_id=request_id,
            )
        )
        try:
            await session.commit()
        except ProgrammingError as e:
            if _audit_table_missing_error(e):
                logger.warning(
                    "audit_events table missing — apply migration 008; skipping standalone audit (%s)",
                    action.value,
                )
                await session.rollback()
                return
            await session.rollback()
            raise


async def resolve_actor_fields(session: AsyncSession, user_id: uuid.UUID) -> tuple[str, str]:
    """Return ``(email, roles_csv)`` for denormalized audit columns."""
    from src.initiative_db.models import User, UserRoleRow

    user = await session.get(User, user_id)
    if user is None:
        return "unknown@invalid", "none"
    stmt = select(UserRoleRow.role).where(UserRoleRow.user_id == user_id)
    roles = (await session.execute(stmt)).scalars().all()
    r = ",".join(sorted({str(x) for x in roles})) if roles else "none"
    return user.email, r


def roles_from_jwt_list(roles: Sequence[str] | None) -> str:
    if not roles:
        return "none"
    return ",".join(sorted({str(x) for x in roles}))


def jwt_payload_actor_email(payload: Mapping[str, Any] | None) -> tuple[str, str]:
    """Extract ``(email, roles_csv)`` from JWT payload claims."""
    if not payload:
        return "<anonymous>", "none"
    email = str(payload.get("email") or "<unknown>")
    raw = payload.get("roles")
    if isinstance(raw, list):
        return email, roles_from_jwt_list([str(x) for x in raw])
    return email, "none"


jwt_payload_actor_email_role = jwt_payload_actor_email
