"""Audit event handler for compliance and debugging."""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, UTC
import json
from typing import Any

from ...domain.events import DomainEvent
from ...logging_config import get_logger

logger = get_logger(__name__)


@dataclass
class AuditRecord:
    """Represents an audit log entry.

    Identity fields capture the caller who triggered the action,
    enabling identity-aware queries and compliance reporting.
    """

    event_id: str
    event_type: str
    occurred_at: datetime
    mcp_server_id: str | None
    data: dict[str, Any]
    recorded_at: datetime = field(default_factory=lambda: datetime.now(UTC))
    caller_user_id: str | None = None
    caller_agent_id: str | None = None
    caller_session_id: str | None = None
    caller_principal_type: str | None = None
    task_id: str | None = None
    tenant_id: str | None = None

    @property
    def provider_id(self) -> str | None:
        return self.mcp_server_id

    def to_dict(self) -> dict[str, Any]:
        """Convert audit record to dictionary."""
        return {
            "event_id": self.event_id,
            "event_type": self.event_type,
            "occurred_at": (
                self.occurred_at.isoformat() if isinstance(self.occurred_at, datetime) else str(self.occurred_at)
            ),
            "mcp_server_id": self.mcp_server_id,
            "provider_id": self.mcp_server_id,
            "data": self.data,
            "recorded_at": self.recorded_at.isoformat(),
            "caller_user_id": self.caller_user_id,
            "caller_agent_id": self.caller_agent_id,
            "caller_session_id": self.caller_session_id,
            "caller_principal_type": self.caller_principal_type,
            "task_id": self.task_id,
            "tenant_id": self.tenant_id,
        }

    def to_json(self) -> str:
        """Convert to JSON string."""
        return json.dumps(self.to_dict())


class AuditStore(ABC):
    """Abstract interface for audit log storage."""

    @abstractmethod
    def record(self, audit_record: AuditRecord) -> None:
        """Store an audit record."""
        pass

    @abstractmethod
    def query(
        self,
        mcp_server_id: str | None = None,
        event_type: str | None = None,
        since: datetime | None = None,
        limit: int = 100,
        caller_user_id: str | None = None,
        provider_id: str | None = None,
        task_id: str | None = None,
    ) -> list[AuditRecord]:
        """Query audit records."""
        pass


class InMemoryAuditStore(AuditStore):
    """In-memory audit store for testing and development."""

    def __init__(self, max_records: int = 10000):
        self._records: list[AuditRecord] = []
        self._max_records = max_records

    def record(self, audit_record: AuditRecord) -> None:
        """Store an audit record."""
        self._records.append(audit_record)
        # Trim old records if over limit
        if len(self._records) > self._max_records:
            self._records = self._records[-self._max_records :]

    def query(
        self,
        mcp_server_id: str | None = None,
        event_type: str | None = None,
        since: datetime | None = None,
        limit: int = 100,
        caller_user_id: str | None = None,
        provider_id: str | None = None,
        task_id: str | None = None,
    ) -> list[AuditRecord]:
        """Query audit records with optional filters.

        When ``task_id`` is supplied, all lifecycle records for that task are
        returned, letting callers reconstruct the full async trail for a task.
        """
        if mcp_server_id is None:
            mcp_server_id = provider_id

        results: list[AuditRecord] = []
        for record in reversed(self._records):  # Most recent first
            if len(results) >= limit:
                break

            # Apply filters
            if mcp_server_id and record.mcp_server_id != mcp_server_id:
                continue
            if event_type and record.event_type != event_type:
                continue
            if since and record.recorded_at < since:
                continue
            if caller_user_id and record.caller_user_id != caller_user_id:
                continue
            if task_id and record.task_id != task_id:
                continue

            results.append(record)

        return results

    def clear(self) -> None:
        """Clear all records (for testing)."""
        self._records.clear()

    @property
    def count(self) -> int:
        """Get number of stored records."""
        return len(self._records)


class AuditEventHandler:
    """
    Event handler that records all events for audit trail.

    Records every domain event with full details for:
    - Compliance requirements
    - Debugging and troubleshooting
    - Historical analysis
    """

    def __init__(
        self,
        store: AuditStore | None = None,
        include_event_types: list[str] | None = None,
        exclude_event_types: list[str] | None = None,
    ):
        """
        Initialize the audit handler.

        Args:
            store: Audit store to use (defaults to in-memory)
            include_event_types: Only record these event types (None = all)
            exclude_event_types: Exclude these event types
        """
        self._store = store or InMemoryAuditStore()
        self._include = set(include_event_types) if include_event_types else None
        self._exclude = set(exclude_event_types) if exclude_event_types else set()

    def handle(self, event: DomainEvent) -> None:
        """Handle a domain event by recording it."""
        event_type = type(event).__name__

        # Check filters
        if self._include is not None and event_type not in self._include:
            return
        if event_type in self._exclude:
            return

        # Extract mcp_server_id if available
        mcp_server_id = getattr(event, "mcp_server_id", None)

        # Extract task lifecycle keys (task_id/tenant_id) from events that carry
        # them, so the full async trail is reconstructable per task_id (#321).
        task_id = getattr(event, "task_id", None)
        tenant_id = getattr(event, "tenant_id", None)

        # Extract identity_context from events that carry it
        # (ToolInvocationRequested/Completed/Failed have identity_context field)
        identity_context = getattr(event, "identity_context", None)
        caller_user_id: str | None = None
        caller_agent_id: str | None = None
        caller_session_id: str | None = None
        caller_principal_type: str | None = None

        if isinstance(identity_context, dict):
            caller_user_id = identity_context.get("user_id")
            caller_agent_id = identity_context.get("agent_id")
            caller_session_id = identity_context.get("session_id")
            caller_principal_type = identity_context.get("principal_type")

        # Create audit record
        record = AuditRecord(
            event_id=event.event_id,
            event_type=event_type,
            occurred_at=datetime.fromtimestamp(event.occurred_at, tz=UTC),
            mcp_server_id=mcp_server_id,
            data=event.to_dict(),
            caller_user_id=caller_user_id,
            caller_agent_id=caller_agent_id,
            caller_session_id=caller_session_id,
            caller_principal_type=caller_principal_type,
            task_id=task_id if isinstance(task_id, str) else None,
            tenant_id=tenant_id if isinstance(tenant_id, str) else None,
        )

        try:
            self._store.record(record)
        except Exception as e:  # noqa: BLE001 -- fault-barrier: audit store failure must not crash event handler
            logger.error(f"Failed to record audit event: {e}")

    @property
    def store(self) -> AuditStore:
        """Get the audit store."""
        return self._store

    def query(
        self,
        mcp_server_id: str | None = None,
        event_type: str | None = None,
        since: datetime | None = None,
        limit: int = 100,
        caller_user_id: str | None = None,
        provider_id: str | None = None,
        task_id: str | None = None,
    ) -> list[AuditRecord]:
        """Query audit records.

        Pass ``task_id`` to reconstruct the full lifecycle trail
        (created -> ... -> completed/failed/cancelled) for one task.
        """
        if mcp_server_id is None:
            mcp_server_id = provider_id
        return self._store.query(
            mcp_server_id=mcp_server_id,
            event_type=event_type,
            since=since,
            limit=limit,
            caller_user_id=caller_user_id,
            task_id=task_id,
        )


# --- Global audit handler instance ---

_audit_handler: AuditEventHandler | None = None


def get_audit_handler() -> AuditEventHandler:
    """Get or create the global audit handler instance."""
    global _audit_handler
    if _audit_handler is None:
        _audit_handler = AuditEventHandler()
    return _audit_handler


def reset_audit_handler() -> None:
    """Reset the global audit handler (for testing)."""
    global _audit_handler
    _audit_handler = None
