from __future__ import annotations

from collections.abc import Mapping
from uuid import uuid4
from typing import Any

from sqlalchemy.orm import Session

from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.change_sequence import record_change
from govoplan_core.core.access import AuditEvent, AuditRecordRef, AuditRecorder, CAPABILITY_AUDIT_RECORDER
from govoplan_core.core.events import (
    EventActorRef,
    EventObjectRef,
    EventTenantRef,
    PlatformEvent,
    current_event_trace,
    normalize_trace_id,
    emit_platform_event,
)
from govoplan_core.core.institutional import GovernedContextEnvelope
from govoplan_core.core.runtime import get_registry
from govoplan_core.privacy.retention import sanitize_audit_details_for_policy
from govoplan_core.security.redaction import redact_secret_values


AUDIT_MODULE_ID = "audit"
AUDIT_TENANT_EVENTS_COLLECTION = "audit.admin.tenant_events"
AUDIT_SYSTEM_EVENTS_COLLECTION = "audit.admin.system_events"

_ACTION_MODULE_PREFIXES = {
    "api_key": "access",
    "configuration_change": "access",
    "configuration_package": "access",
    "group": "access",
    "idm": "idm",
    "profile": "access",
    "role": "access",
    "system_access": "access",
    "system_account": "access",
    "system_memberships": "access",
    "system_role": "access",
    "user": "access",
    "tenant": "tenancy",
    "privacy_retention": "policy",
    "retention_policy": "policy",
    "files": "files",
    "mail": "mail",
    "campaign": "campaigns",
    "report": "campaigns",
}


def _optional_text(value: object | None) -> str | None:
    if value is None:
        return None
    candidate = str(value).strip()
    return candidate or None


def _compact_trace(trace: Mapping[str, object] | None) -> dict[str, str]:
    if not isinstance(trace, Mapping):
        return {}
    compact: dict[str, str] = {}
    correlation_id = _optional_text(trace.get("correlation_id"))
    causation_id = _optional_text(trace.get("causation_id"))
    clean_correlation_id = normalize_trace_id(correlation_id)
    clean_causation_id = normalize_trace_id(causation_id)
    if clean_correlation_id:
        compact["correlation_id"] = clean_correlation_id
    if clean_causation_id:
        compact["causation_id"] = clean_causation_id
    return compact


def _sanitize_details(value: Any) -> Any:
    return redact_secret_values(value)


def audit_operation_context(
    *,
    module_id: object | None = None,
    request_id: object | None = None,
    run_id: object | None = None,
    outcome: object | None = None,
    trace: Mapping[str, object] | None = None,
    **details: Any,
) -> dict[str, Any]:
    """Build concise operational audit details for admin and lifecycle actions."""

    payload: dict[str, Any] = {}
    for key, value in (
        ("module_id", module_id),
        ("request_id", request_id),
        ("run_id", run_id),
        ("outcome", outcome),
    ):
        text = _optional_text(value)
        if text is not None:
            payload[key] = text
    for key, value in details.items():
        if value is not None:
            payload[key] = _sanitize_details(value)
    compact_trace = _compact_trace(trace)
    if compact_trace:
        existing = payload.get("_trace")
        payload["_trace"] = {**existing, **compact_trace} if isinstance(existing, dict) else compact_trace
    return payload


def _trace_details(
    details: dict[str, Any],
    *,
    correlation_id: str | None = None,
    causation_id: str | None = None,
) -> tuple[dict[str, Any], dict[str, str]]:
    active_trace = current_event_trace()
    trace: dict[str, str] = {}
    clean_correlation_id = normalize_trace_id(correlation_id)
    clean_causation_id = normalize_trace_id(causation_id)
    if clean_correlation_id or active_trace:
        trace["correlation_id"] = clean_correlation_id or active_trace.correlation_id
    if clean_causation_id or (active_trace and active_trace.causation_id):
        trace["causation_id"] = clean_causation_id or str(active_trace.causation_id)
    if not trace:
        return details, {}
    merged = dict(details)
    existing = merged.get("_trace")
    merged["_trace"] = {**existing, **trace} if isinstance(existing, dict) else trace
    return merged, trace


def _restore_trace_after_policy(details: dict[str, Any], trace: dict[str, str]) -> dict[str, Any]:
    if not trace:
        return details
    if details.get("_trace") == trace:
        return details
    restored = dict(details)
    existing = restored.get("_trace")
    restored["_trace"] = {**existing, **trace} if isinstance(existing, dict) else trace
    return restored


def _module_id_for_audit_action(action: str) -> str:
    prefix = action.split(".", 1)[0].strip().casefold()
    return _ACTION_MODULE_PREFIXES.get(prefix, prefix or AUDIT_MODULE_ID)


def _audit_recorder() -> AuditRecorder:
    registry = get_registry()
    if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_AUDIT_RECORDER):
        return _NullAuditRecorder()
    capability = registry.require_capability(CAPABILITY_AUDIT_RECORDER)
    if not isinstance(capability, AuditRecorder):
        raise RuntimeError("Audit recorder capability is invalid")
    return capability


class _NullAuditRecorder:
    def record_event(self, session: object, event: AuditEvent) -> AuditRecordRef:
        del session
        return AuditRecordRef(
            id=str(uuid4()),
            scope=event.scope,
            tenant_id=event.tenant_id,
            user_id=event.user_id,
            api_key_id=event.api_key_id,
            action=event.action,
            object_type=event.resource_type,
            object_id=event.resource_id,
            details=dict(event.details or {}),
        )


def _publish_audit_platform_event(
    session: Session,
    item: AuditRecordRef,
) -> None:
    trace = _compact_trace(item.details.get("_trace") if isinstance(item.details, Mapping) else None)
    raw_context = (
        item.details.get("_institutional_context")
        if isinstance(item.details, Mapping)
        else None
    )
    institutional_context = (
        GovernedContextEnvelope.from_mapping(raw_context)
        if isinstance(raw_context, Mapping)
        else None
    )
    emit_platform_event(
        session,
        PlatformEvent(
            type=item.action,
            module_id=_module_id_for_audit_action(item.action),
            payload={
                "audit_log_id": item.id,
                "scope": item.scope,
                "details": dict(item.details or {}),
            },
            correlation_id=trace.get("correlation_id"),
            causation_id=trace.get("causation_id"),
            actor=EventActorRef(type="user", id=item.user_id) if item.user_id else (
                EventActorRef(type="api_key", id=item.api_key_id) if item.api_key_id else None
            ),
            tenant=EventTenantRef(id=item.tenant_id) if item.tenant_id else None,
            resource=EventObjectRef(type=item.object_type, id=item.object_id) if item.object_type else None,
            classification="internal",
            institutional_context=institutional_context,
        )
    )


def audit_event(
    session: Session,
    *,
    tenant_id: str | None,
    action: str,
    scope: str = "tenant",
    user_id: str | None = None,
    api_key_id: str | None = None,
    object_type: str | None = None,
    object_id: str | None = None,
    details: dict[str, Any] | None = None,
    correlation_id: str | None = None,
    causation_id: str | None = None,
    institutional_context: GovernedContextEnvelope | None = None,
    commit: bool = False,
) -> AuditRecordRef:
    """Persist one audit event.

    The function deliberately accepts primitive IDs so it can be used from API
    handlers, CLI commands and worker code without coupling the lower-level
    services to FastAPI request objects.
    """

    if scope not in {"tenant", "system"}:
        raise ValueError(f"Unsupported audit scope: {scope}")

    if (
        institutional_context is not None
        and tenant_id is not None
        and institutional_context.tenant_id != tenant_id
    ):
        raise ValueError("Audit institutional context belongs to another tenant")
    raw_details = dict(details or {})
    if institutional_context is not None:
        raw_details["_institutional_context"] = institutional_context.to_dict()
    traced_details, trace = _trace_details(
        _sanitize_details(raw_details),
        correlation_id=correlation_id,
        causation_id=causation_id,
    )
    stored_details = _restore_trace_after_policy(
        sanitize_audit_details_for_policy(session, traced_details),
        trace,
    )

    item = _audit_recorder().record_event(session, AuditEvent(
        event_type=action,
        action=action,
        scope=scope,
        tenant_id=tenant_id,
        user_id=user_id,
        api_key_id=api_key_id,
        resource_type=object_type,
        resource_id=object_id,
        institutional_context=institutional_context,
        details=stored_details,
    ))
    record_change(
        session,
        module_id=AUDIT_MODULE_ID,
        collection=AUDIT_SYSTEM_EVENTS_COLLECTION if scope == "system" else AUDIT_TENANT_EVENTS_COLLECTION,
        resource_type="audit_log",
        resource_id=item.id,
        operation="created",
        tenant_id=None if scope == "system" else tenant_id,
        actor_type="user" if user_id else ("api_key" if api_key_id else None),
        actor_id=user_id or api_key_id,
        payload={"scope": scope, "action": action, "object_type": object_type, "object_id": object_id},
    )
    _publish_audit_platform_event(session, item)
    if commit:
        session.commit()
    return item


def audit_from_principal(
    session: Session,
    principal: ApiPrincipal,
    *,
    action: str,
    scope: str = "tenant",
    object_type: str | None = None,
    object_id: str | None = None,
    details: dict[str, Any] | None = None,
    correlation_id: str | None = None,
    causation_id: str | None = None,
    institutional_context: GovernedContextEnvelope | None = None,
    commit: bool = False,
) -> AuditRecordRef:
    return audit_event(
        session,
        tenant_id=principal.tenant_id,
        user_id=principal.user.id,
        api_key_id=principal.api_key.id if principal.api_key else None,
        action=action,
        scope=scope,
        object_type=object_type,
        object_id=object_id,
        details=details,
        correlation_id=correlation_id,
        causation_id=causation_id,
        institutional_context=institutional_context,
        commit=commit,
    )
