"""Flight recorder for automatic log capture during handler execution.

The flight recorder transparently captures all log output during a handler's
``process()`` call and attaches it to the ``AdapterResponse.execution_logs``
field. Handlers are unaware of the flight recorder — they just use standard
``logging.getLogger(__name__)`` calls.
"""

from __future__ import annotations

import logging
import uuid
from contextvars import ContextVar
from datetime import datetime, timezone
from typing import Any

from entr_adapter_core.schemas import (
    AdapterResponse,
    DocumentPayload,
    StatusEnum,
)

# ContextVar to scope log capture per-request. Each concurrent /process request
# gets a unique token; FlightRecorderHandler only captures logs whose current
# context token matches the handler's token. This prevents concurrent requests
# from leaking logs into each other's execution_logs.
_recorder_token: ContextVar[str | None] = ContextVar('_recorder_token', default=None)

# Standard LogRecord attributes to exclude when extracting extra context
_STANDARD_ATTRS = frozenset({
    "name", "msg", "args", "created", "relativeCreated", "exc_info",
    "exc_text", "stack_info", "lineno", "funcName", "pathname",
    "filename", "module", "thread", "threadName", "process",
    "processName", "levelname", "levelno", "msecs", "message",
    "asctime", "taskName",
})

server_logger = logging.getLogger("entr_adapter_core.dispatch")


class FlightRecorderHandler(logging.Handler):
    """A logging handler that captures log records to an in-memory list.

    Each record is stored as a dict with keys: ``timestamp``, ``level``,
    ``logger_name``, ``message``, and ``context``. When the entry limit is
    exceeded, the oldest entries are dropped and a truncation marker is
    appended to the output.

    When ``token`` is set, only logs emitted within the matching request
    context are captured. This isolates concurrent /process requests so
    each response only contains its own logs.
    """

    def __init__(self, max_entries: int = 5000, token: str | None = None) -> None:
        super().__init__()
        self._max_entries = max_entries
        self._records: list[dict[str, Any]] = []
        self._dropped_count = 0
        self._token = token

    def emit(self, record: logging.LogRecord) -> None:
        """Capture a log record as a formatted dict.

        If a token was set, only capture logs from the matching request context.
        Logs emitted outside any request context (token is None) are skipped
        when filtering is active, since they belong to unrelated operations.
        """
        try:
            # Only capture logs from the matching request context
            if self._token is not None:
                current_token = _recorder_token.get(None)
                if current_token != self._token:
                    return

            # Extract extra context (non-standard attributes)
            context: dict[str, Any] | None = None
            extra_keys = set(record.__dict__.keys()) - _STANDARD_ATTRS
            if extra_keys:
                context = {k: record.__dict__[k] for k in extra_keys}

            entry = {
                "timestamp": datetime.fromtimestamp(
                    record.created, tz=timezone.utc
                ).isoformat(),
                "level": record.levelname,
                "logger_name": record.name,
                "message": self.format(record),
                "context": context,
            }

            if len(self._records) >= self._max_entries:
                self._records.pop(0)
                self._dropped_count += 1

            self._records.append(entry)
        except Exception:
            self.handleError(record)

    def get_logs(self) -> list[dict[str, Any]]:
        """Return a copy of captured log entries.

        If entries were dropped due to the limit, a truncation marker is
        appended as the final entry.
        """
        logs = list(self._records)
        if self._dropped_count > 0:
            logs.append({
                "truncated": True,
                "dropped_count": self._dropped_count,
                "message": "Log buffer exceeded limit, oldest entries dropped",
                "level": "WARNING",
                "timestamp": datetime.now(tz=timezone.utc).isoformat(),
            })
        return logs

    def clear(self) -> None:
        """Reset captured records and dropped count."""
        self._records.clear()
        self._dropped_count = 0


class FlightRecorder:
    """Async context manager that captures log output during a block.

    Attaches a ``FlightRecorderHandler`` to a logger on entry and removes
    it on exit (even if an exception occurs). Captured logs are available
    via ``get_logs()`` after the block completes.

    Args:
        max_entries: Maximum number of log entries to retain (default 1000).
        logger_name: Logger to attach to. If None, attaches to the root logger
            to capture all log output.
        token: Optional request-scoping token. When set, only logs emitted
            within the matching ``_recorder_token`` context are captured.
    """

    def __init__(
        self,
        max_entries: int = 5000,
        logger_name: str | None = None,
        token: str | None = None,
    ) -> None:
        self._max_entries = max_entries
        self._logger = logging.getLogger(logger_name)
        self._handler: FlightRecorderHandler | None = None
        self._original_level: int = logging.NOTSET
        self._token = token

    async def __aenter__(self) -> FlightRecorder:
        self._handler = FlightRecorderHandler(
            max_entries=self._max_entries, token=self._token
        )
        self._handler.setLevel(logging.DEBUG)
        # Save and lower logger level so INFO/DEBUG messages from handlers
        # are not filtered before reaching our handler. Without this, a
        # root logger at WARNING (the production default) would silently
        # drop handler INFO/DEBUG logs, making execution_logs incomplete.
        self._original_level = self._logger.level
        if self._logger.level > logging.DEBUG:
            self._logger.setLevel(logging.DEBUG)
        self._logger.addHandler(self._handler)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> bool:
        if self._handler is not None:
            self._logger.removeHandler(self._handler)
        self._logger.setLevel(self._original_level)
        return False  # Do not suppress exceptions

    def get_logs(self) -> list[dict[str, Any]]:
        """Return captured log entries from the handler."""
        if self._handler is None:
            return []
        return self._handler.get_logs()


async def execute_with_flight_recorder(
    handler,
    payload: DocumentPayload,
    max_entries: int = 5000,
) -> AdapterResponse:
    """Execute a handler's process() with automatic log capture.

    This is the primary integration point called by the dispatcher.
    Wraps the handler call in a FlightRecorder, captures all logs,
    and attaches them to the response.

    On unhandled exceptions, the full traceback is logged INSIDE the
    flight recorder context so it appears in ``execution_logs`` for
    diagnostics. The ``failure_reason`` includes the exception message.

    Args:
        handler: The handler instance to execute.
        payload: The document payload to process.
        max_entries: Maximum flight recorder log entries (default 5000).

    Returns:
        The ``AdapterResponse`` with ``execution_logs`` populated.
    """
    # Generate a unique token for this request and set it in the current
    # async context. The FlightRecorderHandler will only capture logs
    # emitted within this context, isolating concurrent requests.
    token = uuid.uuid4().hex
    _recorder_token.set(token)

    async with FlightRecorder(max_entries=max_entries, token=token) as recorder:
        try:
            response = await handler.process(payload)
        except Exception as exc:
            # Log the full traceback INSIDE the recorder so it appears
            # in execution_logs — this is valuable for diagnostics
            server_logger.exception(
                "Unhandled exception in handler for adapter_identifier: %s",
                payload.adapter_identifier,
                exc_info=exc,
            )
            response = AdapterResponse(
                document_id=payload.document_id,
                status=StatusEnum.FAILED,
                failure_reason=str(exc),
            )

        response.execution_logs = recorder.get_logs()
        return response
