# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
EventEmitter — Structured tool execution events.

Extracted from streaming_executor.py (v1.4.0). The full StreamingExecutor
(parallel async agent loop) was removed because it duplicated the existing
agent.chat_stream() path and depended on the deleted SkillBus.

What's kept: the event bus itself — EventEmitter, ExecutionEvent, EventType,
ToolStatus — which are wired into the agent's tool_context so tools can
optionally emit progress events during long-running operations.

Usage inside a tool handler:

    def my_tool(input_data, tool_context):
        emitter = tool_context.get("emitter")
        for i, item in enumerate(items):
            if emitter:
                emitter.tool_progress(
                    tool_name="my_tool",
                    progress_percent=(i / len(items)) * 100,
                    status_message=f"Processing item {i+1}/{len(items)}",
                )
            process(item)
        return result

The agent automatically emits TOOL_START and TOOL_COMPLETE/TOOL_ERROR for
every tool call — tools don't need to emit those themselves.

Clients that want progress events subscribe a callback before the request:

    agent.set_tool_event_callback(user_id, lambda evt: send_to_websocket(evt))
"""

from __future__ import annotations

import logging
import threading
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from queue import Full, Queue
from typing import Any, Callable, Dict, List, Optional

logger = logging.getLogger(__name__)


# ============================================================
# UTILITIES
# ============================================================


def _utcnow() -> datetime:
    """Return current UTC time as a timezone-aware datetime."""
    return datetime.now(timezone.utc)


# ============================================================
# ENUMS
# ============================================================


class EventType(str, Enum):
    """Types of tool execution events emitted by EventEmitter."""

    # Thinking / reasoning — emitted by tools or the agent to signal
    # that work is in progress before a result is available.
    THINKING = "thinking"

    # Tool lifecycle — TOOL_START and TOOL_COMPLETE are emitted
    # automatically by the agent for every tool call. Tools emit
    # TOOL_PROGRESS and TOOL_OUTPUT themselves when useful.
    TOOL_START = "tool_start"
    TOOL_PROGRESS = "tool_progress"
    TOOL_OUTPUT = "tool_output"  # Intermediate output (e.g. log lines)
    TOOL_COMPLETE = "tool_complete"
    TOOL_ERROR = "tool_error"
    TOOL_RETRY = "tool_retry"

    # Response events — emitted by tools that produce streaming text output
    PARTIAL_RESPONSE = "partial_response"
    RESPONSE = "response"

    # General progress and status — for tools with indeterminate progress
    PROGRESS = "progress"
    STATUS = "status"

    # Diagnostics
    DEBUG = "debug"
    WARNING = "warning"


class ToolStatus(str, Enum):
    """Lifecycle status of a single tool execution."""

    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    RETRYING = "retrying"
    SKIPPED = "skipped"


# ============================================================
# EVENT DATACLASS
# ============================================================


@dataclass
class ExecutionEvent:
    """
    A single event from tool execution.

    Emitted by EventEmitter as execution progresses — from TOOL_START
    through optional TOOL_PROGRESS updates to TOOL_COMPLETE or TOOL_ERROR.

    Fields are type-dependent: most are None except for those relevant
    to the specific event type.
    """

    # Identity
    type: EventType
    execution_id: str
    timestamp: datetime = field(default_factory=_utcnow)
    sequence: int = 0

    # Content (type-dependent)
    content: Optional[str] = None
    data: Optional[Dict[str, Any]] = None

    # Tool-specific
    tool_name: Optional[str] = None
    tool_id: Optional[str] = None
    tool_input: Optional[Dict[str, Any]] = None
    result: Optional[Any] = None
    error: Optional[str] = None

    # Progress tracking — for TOOL_PROGRESS and PROGRESS events
    progress_percent: Optional[float] = None
    status_message: Optional[str] = None

    # Timing — elapsed_ms set on TOOL_COMPLETE/TOOL_ERROR
    elapsed_ms: Optional[float] = None

    # Free-form metadata
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to a JSON-serialisable dictionary."""
        return {
            "type": self.type.value,
            "execution_id": self.execution_id,
            "timestamp": self.timestamp.isoformat(),
            "sequence": self.sequence,
            "content": self.content,
            "data": self.data,
            "tool_name": self.tool_name,
            "tool_id": self.tool_id,
            "tool_input": self.tool_input,
            "result": self.result,
            "error": self.error,
            "progress_percent": self.progress_percent,
            "status_message": self.status_message,
            "elapsed_ms": self.elapsed_ms,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "ExecutionEvent":
        """Reconstruct from a dictionary (e.g. after JSON deserialisation)."""
        data = data.copy()
        data["type"] = EventType(data["type"])
        if isinstance(data.get("timestamp"), str):
            data["timestamp"] = datetime.fromisoformat(data["timestamp"])
        return cls(**data)


# ============================================================
# EVENT EMITTER
# ============================================================


class EventEmitter:
    """
    Structured event bus for a single tool-execution context.

    One emitter is created per agent request and placed in tool_context
    so any tool handler can emit progress events without needing a direct
    reference to the agent or the streaming layer.

    Supports:
    - Sync callbacks (fire-and-forget, called inline on emit())
    - A single sync Queue for non-async consumers
    - Event history (optional, disabled by default)

    Thread-safe: emit() acquires a lock before mutating sequence counter.

    Example — subscribe a WebSocket sender before the request:

        emitter = EventEmitter(execution_id="req-42")
        emitter.subscribe(lambda evt: ws.send(evt.to_dict()))
        tool_context["emitter"] = emitter
    """

    def __init__(self, execution_id: str):
        self.execution_id = execution_id
        self._sequence = 0
        self._lock = threading.Lock()

        # Subscribers
        self._callbacks: List[Callable[[ExecutionEvent], None]] = []
        self._sync_queue: Optional[Queue] = None

        # Optional history
        self._history: List[ExecutionEvent] = []
        self._keep_history = False
        self._max_history = 1000

    # ── History ──────────────────────────────────────────────────────────────

    def enable_history(self, max_events: int = 1000):
        """Enable in-memory event history (disabled by default)."""
        self._keep_history = True
        self._max_history = max_events

    def get_history(self) -> List[ExecutionEvent]:
        """Return a copy of the event history."""
        return list(self._history)

    # ── Subscriptions ────────────────────────────────────────────────────────

    def subscribe(self, callback: Callable[[ExecutionEvent], None]):
        """Register a sync callback. Called inline on every emit()."""
        self._callbacks.append(callback)

    def unsubscribe(self, callback: Callable[[ExecutionEvent], None]):
        """Remove a previously registered callback."""
        if callback in self._callbacks:
            self._callbacks.remove(callback)

    def create_sync_queue(self) -> Queue:
        """
        Create (or return) a sync Queue that receives every emitted event.

        Useful for non-async consumers (e.g. CLI progress bars, test assertions).
        Only one sync queue per emitter — subsequent calls return the same queue.
        """
        if self._sync_queue is None:
            self._sync_queue = Queue()
        return self._sync_queue

    # ── Core emit ────────────────────────────────────────────────────────────

    def emit(self, event: ExecutionEvent):
        """
        Emit an event to all subscribers.

        Assigns sequence number and execution_id, then fires callbacks
        and pushes to the sync queue if one exists.
        Errors in individual callbacks are logged but do not propagate.
        """
        with self._lock:
            event.sequence = self._sequence
            event.execution_id = self.execution_id
            self._sequence += 1

        if self._keep_history:
            self._history.append(event)
            if len(self._history) > self._max_history:
                self._history.pop(0)

        for callback in self._callbacks:
            try:
                callback(event)
            except Exception as e:
                logger.warning(f"EventEmitter callback error: {e}")

        if self._sync_queue is not None:
            try:
                self._sync_queue.put_nowait(event)
            except (Full, AttributeError) as e:
                logger.debug(f"EventEmitter sync queue full or unavailable: {e}")

    def emit_event(
        self,
        event_type: EventType,
        content: Optional[str] = None,
        **kwargs,
    ) -> ExecutionEvent:
        """Create an ExecutionEvent and emit it. Returns the event."""
        event = ExecutionEvent(
            type=event_type,
            execution_id=self.execution_id,
            content=content,
            **kwargs,
        )
        self.emit(event)
        return event

    # ── Typed convenience methods ─────────────────────────────────────────────

    def thinking(self, content: str, **kwargs) -> ExecutionEvent:
        """Emit a THINKING event — signals reasoning/work in progress."""
        return self.emit_event(EventType.THINKING, content=content, **kwargs)

    def tool_start(
        self,
        tool_name: str,
        tool_input: Dict[str, Any],
        tool_id: Optional[str] = None,
        **kwargs,
    ) -> ExecutionEvent:
        """Emit TOOL_START. Called automatically by the agent before each tool."""
        return self.emit_event(
            EventType.TOOL_START,
            tool_name=tool_name,
            tool_id=tool_id or str(uuid.uuid4())[:8],
            tool_input=tool_input,
            **kwargs,
        )

    def tool_progress(
        self,
        tool_name: str,
        progress_percent: float,
        status_message: Optional[str] = None,
        tool_id: Optional[str] = None,
        **kwargs,
    ) -> ExecutionEvent:
        """
        Emit TOOL_PROGRESS. Call this from inside a tool handler to report
        progress during long-running operations.

            emitter = tool_context.get("emitter")
            if emitter:
                emitter.tool_progress("export_csv", 50.0, "Halfway through rows")
        """
        return self.emit_event(
            EventType.TOOL_PROGRESS,
            tool_name=tool_name,
            tool_id=tool_id,
            progress_percent=progress_percent,
            status_message=status_message,
            **kwargs,
        )

    def tool_complete(
        self,
        tool_name: str,
        result: Any,
        elapsed_ms: float,
        tool_id: Optional[str] = None,
        **kwargs,
    ) -> ExecutionEvent:
        """Emit TOOL_COMPLETE. Called automatically by the agent after success."""
        return self.emit_event(
            EventType.TOOL_COMPLETE,
            tool_name=tool_name,
            tool_id=tool_id,
            result=result,
            elapsed_ms=elapsed_ms,
            **kwargs,
        )

    def tool_error(
        self,
        tool_name: str,
        error: str,
        elapsed_ms: Optional[float] = None,
        tool_id: Optional[str] = None,
        **kwargs,
    ) -> ExecutionEvent:
        """Emit TOOL_ERROR. Called automatically by the agent on tool failure."""
        return self.emit_event(
            EventType.TOOL_ERROR,
            tool_name=tool_name,
            tool_id=tool_id,
            error=error,
            elapsed_ms=elapsed_ms,
            **kwargs,
        )

    def tool_retry(
        self,
        tool_name: str,
        attempt: int,
        reason: str,
        tool_id: Optional[str] = None,
        **kwargs,
    ) -> ExecutionEvent:
        """Emit TOOL_RETRY when self-correction retries a tool."""
        return self.emit_event(
            EventType.TOOL_RETRY,
            tool_name=tool_name,
            tool_id=tool_id,
            data={"attempt": attempt, "reason": reason},
            **kwargs,
        )

    def progress(
        self,
        percent: float,
        message: Optional[str] = None,
        **kwargs,
    ) -> ExecutionEvent:
        """Emit a general PROGRESS event (not tool-specific)."""
        return self.emit_event(
            EventType.PROGRESS,
            progress_percent=percent,
            status_message=message,
            **kwargs,
        )

    def status(self, message: str, **kwargs) -> ExecutionEvent:
        """Emit a STATUS event — a human-readable status update."""
        return self.emit_event(EventType.STATUS, status_message=message, **kwargs)

    def response(self, content: str, **kwargs) -> ExecutionEvent:
        """Emit a RESPONSE event — final output from a tool."""
        return self.emit_event(EventType.RESPONSE, content=content, **kwargs)

    def partial_response(self, content: str, **kwargs) -> ExecutionEvent:
        """Emit PARTIAL_RESPONSE — a streaming text chunk from a tool."""
        return self.emit_event(EventType.PARTIAL_RESPONSE, content=content, **kwargs)

    def warning(self, content: str, **kwargs) -> ExecutionEvent:
        """Emit a WARNING event."""
        return self.emit_event(EventType.WARNING, content=content, **kwargs)

    def debug(self, content: str, **kwargs) -> ExecutionEvent:
        """Emit a DEBUG event."""
        return self.emit_event(EventType.DEBUG, content=content, **kwargs)


# ============================================================
# EXPORTS
# ============================================================

__all__ = [
    "EventEmitter",
    "ExecutionEvent",
    "EventType",
    "ToolStatus",
]
