"""Observability and event emission for pathway execution.

This module handles:
- Event listeners and emission
- Tool wrapping for observability
- Event creation helpers
"""

import contextlib
import logging
import time
from collections.abc import Callable
from uuid import uuid4

from core.common import JsonObject
from core.pathways.context import Context
from core.pathways.models.execution import PathwayEvent

logger = logging.getLogger(__name__)


class VMEventEmitter:
    """Event emission system for pathway execution (VM-internal)."""

    def __init__(self) -> None:
        self._listeners: list[Callable[[PathwayEvent], None]] = []

    def add_listener(self, listener: Callable[[PathwayEvent], None]) -> None:
        """Add an event listener."""
        self._listeners.append(listener)

    def emit_event(self, event: PathwayEvent) -> None:
        """Emit an event to all listeners."""
        for listener in self._listeners:
            with contextlib.suppress(Exception):
                listener(event)

    def emit(
        self,
        event_type: str,
        *,
        pathway_id: str | None = None,
        node_id: str | None = None,
        error: str | None = None,
        **data: object,
    ) -> None:
        """Emit an event with common error handling."""
        with contextlib.suppress(Exception):
            self.emit_event(
                PathwayEvent(
                    event_type=event_type,
                    pathway_id=pathway_id,
                    node_id=node_id,
                    error=error,
                    data=data,
                ),
            )


def wrap_tools_for_observability(ctx: Context, emitter: VMEventEmitter) -> None:
    """Wrap ctx.tools handlers to emit tool call lifecycle PathwayEvents.

    Important: we preserve `.stream` on handlers so EventSourceNode keeps working.

    NOTE: We wrap handlers IN-PLACE in the original dict (not a copy) so that
    dynamically registered tools added later to the same dict are still visible.

    Args:
        ctx: Execution context with tools to wrap
        emitter: Event emitter for tool call events
    """
    if ctx.run._tools_wrapped:
        return

    # IMPORTANT: Do NOT copy - we need to preserve the reference to the original
    # tools dict so dynamically registered tools are visible.
    tools = ctx.tools or {}

    def _emit(
        event_type: str,
        *,
        tool_name: str,
        call_id: str,
        ok: bool | None = None,
        duration_ms: float | None = None,
        error: str | None = None,
        args: JsonObject | None = None,
        result: JsonObject | None = None,
    ) -> None:
        with contextlib.suppress(Exception):
            emitter.emit_event(
                PathwayEvent(
                    event_type=event_type,
                    pathway_id=getattr(ctx, "pathway_id", None) or None,
                    node_id=ctx.run._current_node_id or None,
                    data={
                        "execution_id": getattr(ctx, "execution_id", "") or "",
                        "tool_name": tool_name,
                        "call_id": call_id,
                        "ok": ok,
                        "duration_ms": duration_ms,
                        "args": args or {},
                        "result": result,
                    },
                    error=error,
                ),
            )

    for name, handler in tools.items():
        if not callable(handler):
            continue
        if getattr(handler, "_albus_wrapped", False):
            continue

        async def _wrapped(
            call_args: JsonObject,
            ctx: Context,
            *,
            _name: str = name,
            _handler: object = handler,
        ) -> JsonObject:
            call_id = f"tc_{uuid4().hex[:10]}"
            _emit(
                "tool_called",
                tool_name=_name,
                call_id=call_id,
                args=call_args or {},
            )
            started_at = time.time()
            try:
                out = await _handler(call_args, ctx)
                dur = (time.time() - started_at) * 1000
                _emit(
                    "tool_completed",
                    tool_name=_name,
                    call_id=call_id,
                    ok=True,
                    duration_ms=dur,
                    result=out,
                )
                return out
            except Exception as e:
                dur = (time.time() - started_at) * 1000
                _emit(
                    "tool_failed",
                    tool_name=_name,
                    call_id=call_id,
                    ok=False,
                    duration_ms=dur,
                    error=str(e),
                )
                raise

        # Preserve streaming attribute if present.
        if hasattr(handler, "stream"):
            _wrapped.stream = handler.stream
        _wrapped._albus_wrapped = True
        tools[name] = _wrapped

    # NOTE: We modified tools in-place (no copy), so no need to reassign ctx.tools
    ctx.run._tools_wrapped = True


# Backward-compatible alias
EventEmitter = VMEventEmitter
