"""Event emission helpers for trace contexts."""

from __future__ import annotations

import asyncio
import logging
from collections.abc import Awaitable, Callable
from contextvars import ContextVar
from typing import Protocol

from .events import Checkpoint, LLMResponseEvent, Session, TraceEvent
from .scorer import get_importance_scorer

logger = logging.getLogger("agent_debugger")

__all__ = ["EventBufferLike", "EventEmitter"]


class EventBufferLike(Protocol):
    """Protocol for publish-capable event buffers."""

    async def publish(self, session_id: str, event: TraceEvent) -> None:
        """Publish an event for live consumers."""
        ...


class EventEmitter:
    """Emit events with scoring, persistence, and live publishing."""

    def __init__(
        self,
        *,
        session_id: str,
        session: Session,
        event_store: list[TraceEvent | Checkpoint],
        event_lock: asyncio.Lock,
        event_sequence: ContextVar[int],
        event_buffer: EventBufferLike | None,
        event_persister: Callable[[TraceEvent], Awaitable[None]] | None,
        session_update_hook: Callable[[Session], Awaitable[None]] | None,
        score_on_emit: bool = True,
    ) -> None:
        self._session_id = session_id
        self._session = session
        self._event_store = event_store
        self._event_lock = event_lock
        self._event_sequence = event_sequence
        self._event_buffer = event_buffer
        self._event_persister = event_persister
        self._session_update_hook = session_update_hook
        self._score_on_emit = score_on_emit

        # Cache config.enabled at initialization to avoid global lookup on every emit
        from agent_debugger_sdk.config import get_config as _get_config

        self._config_enabled = _get_config().enabled

    def update_config_cache(self) -> None:
        """Update cached config.enabled if config has changed."""
        from agent_debugger_sdk.config import get_config as _get_config

        self._config_enabled = _get_config().enabled

    @staticmethod
    async def _safe_call(
        target: object | None,
        args: object,
        *,
        label: str,
        context: str,
        is_method: bool = False,
    ) -> None:
        """Invoke *target* (callback or method) swallowing exceptions."""
        if target is None:
            return
        try:
            if is_method:
                await getattr(target, "publish")(*args)
            else:
                await target(args)
        except Exception as e:
            logger.warning(
                "Failed to %s %s: %s",
                label,
                context,
                type(e).__name__,
                exc_info=True,
            )

    async def emit(self, event: TraceEvent) -> None:
        """Emit an event to storage, hooks, and live consumers."""
        if not self._config_enabled:
            return

        # Use event_lock to ensure atomic sequence increment under concurrent async access
        async with self._event_lock:
            current_seq = self._event_sequence.get() + 1
            self._event_sequence.set(current_seq)
            self._event_store.append(event)

        event.metadata["sequence"] = current_seq
        if self._score_on_emit:
            event.importance = get_importance_scorer().score(event)

        if isinstance(event, LLMResponseEvent):
            usage = event.usage
            self._session.total_tokens += usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
            self._session.total_cost_usd += event.cost_usd
            self._session.llm_calls += 1

        # Collect callbacks to run in parallel
        callbacks = []
        if self._event_persister is not None:
            callbacks.append(
                self._safe_call(
                    self._event_persister,
                    event,
                    label="persist event",
                    context=event.id,
                )
            )
        if self._session_update_hook is not None:
            callbacks.append(
                self._safe_call(
                    self._session_update_hook,
                    self._session,
                    label="update session",
                    context=self._session_id,
                )
            )
        if self._event_buffer is not None:
            callbacks.append(
                self._safe_call(
                    self._event_buffer,
                    (self._session_id, event),
                    label="publish event to buffer",
                    context=event.id,
                    is_method=True,
                )
            )

        if callbacks:
            await asyncio.gather(*callbacks, return_exceptions=True)
