"""In-memory event bus for cross-domain side effects.

Provides a typed, async pub/sub layer that decouples domain events from their
consumers. Handlers are registered via the ``@on()`` decorator on the global
``event_bus`` singleton. Events are fired-and-forgotten via ``asyncio.create_task``.

Errors in individual handlers are logged and isolated — they never propagate to
the emitter or affect other handlers.
"""

from __future__ import annotations

import asyncio
import logging
from datetime import datetime, timezone
from typing import Any, Callable, Coroutine

from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Event type constants
# ---------------------------------------------------------------------------


class EventType:
    """Supported event type constants."""

    # Task lifecycle
    TASK_CREATED = "task.created"
    TASK_STATUS_CHANGED = "task.status_changed"
    TASK_PROGRESS_UPDATED = "task.progress_updated"
    TASK_COMPLETED = "task.completed"
    TASK_FAILED = "task.failed"
    TASK_TIMEOUT = "task.timeout"

    # Test events
    TEST_PASSED = "test.passed"
    TEST_FAILED = "test.failed"
    TEST_MAX_RETRIES_EXHAUSTED = "test.max_retries_exhausted"

    # Feature events
    FEATURE_COMPLETED = "feature.completed"

    # Lead events
    LEAD_CAPTURED = "lead.captured"

    ALL = [
        TASK_CREATED,
        TASK_STATUS_CHANGED,
        TASK_PROGRESS_UPDATED,
        TASK_COMPLETED,
        TASK_FAILED,
        TASK_TIMEOUT,
        TEST_PASSED,
        TEST_FAILED,
        TEST_MAX_RETRIES_EXHAUSTED,
        FEATURE_COMPLETED,
        LEAD_CAPTURED,
    ]


# ---------------------------------------------------------------------------
# Pydantic event models
# ---------------------------------------------------------------------------


class BaseEvent(BaseModel):
    """Base for all event payloads."""

    event_type: str
    project_id: str
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class TaskCreatedEvent(BaseEvent):
    """Emitted when a new task is created."""

    event_type: str = EventType.TASK_CREATED
    task_id: int
    title: str = ""
    priority: str = ""
    status: str = ""
    feature_id: int | None = None
    task_data: dict = Field(default_factory=dict)


class TaskStatusChangedEvent(BaseEvent):
    """Emitted on any task status transition."""

    event_type: str = EventType.TASK_STATUS_CHANGED
    task_id: int
    old_status: str
    new_status: str
    feature_id: int | None = None
    task_data: dict = Field(default_factory=dict)


class TaskProgressUpdatedEvent(BaseEvent):
    """Emitted when a task's progress value changes."""

    event_type: str = EventType.TASK_PROGRESS_UPDATED
    task_id: int
    old_progress: float
    new_progress: float
    task_data: dict = Field(default_factory=dict)


class TaskCompletedEvent(BaseEvent):
    """Emitted when a task reaches status 'done'."""

    event_type: str = EventType.TASK_COMPLETED
    task_id: int
    feature_id: int | None = None
    pr_url: str = ""
    lines_added: int | None = None
    lines_deleted: int | None = None
    tokens_used: int | None = None
    work_log_summary: str = ""
    work_log_files_changed: str = ""
    work_log_test_result: str = ""
    task_data: dict = Field(default_factory=dict)


class TaskFailedEvent(BaseEvent):
    """Emitted when a task is cancelled."""

    event_type: str = EventType.TASK_FAILED
    task_id: int
    feature_id: int | None = None
    task_data: dict = Field(default_factory=dict)


class TaskTimeoutEvent(BaseEvent):
    """Emitted when a task times out."""

    event_type: str = EventType.TASK_TIMEOUT
    task_id: int
    feature_id: int | None = None
    task_data: dict = Field(default_factory=dict)


class FeatureCompletedEvent(BaseEvent):
    """Emitted when a feature is marked done via complete_feature."""

    event_type: str = EventType.FEATURE_COMPLETED
    feature_id: int
    feature_name: str = ""
    branch: str = ""
    pr_url: str = ""
    project_path: str = ""
    feature_data: dict = Field(default_factory=dict)


class LeadCapturedEvent(BaseEvent):
    """Emitted when a lead is submitted via the public capture endpoint."""

    event_type: str = EventType.LEAD_CAPTURED
    lead_name: str = ""
    lead_email: str = ""
    lead_company: str = ""
    source_ip: str = ""
    lead_data: dict = Field(default_factory=dict)


# Type alias for async handler functions
Handler = Callable[[BaseEvent], Coroutine[Any, Any, None]]


# ---------------------------------------------------------------------------
# EventBus
# ---------------------------------------------------------------------------


class EventBus:
    """In-memory async event bus with fire-and-forget dispatch."""

    def __init__(self) -> None:
        self._handlers: dict[str, list[Handler]] = {}

    def subscribe(self, event_type: str, handler: Handler) -> None:
        """Register *handler* to be called when *event_type* is emitted."""
        self._handlers.setdefault(event_type, []).append(handler)

    def emit(self, event: BaseEvent) -> None:
        """Fire-and-forget: schedule handler dispatch as a background task.

        If there are no handlers for the event type, this is a no-op (no task
        is created).
        """
        handlers = self._handlers.get(event.event_type)
        if not handlers:
            return
        # Snapshot the handler list so mutations during dispatch are safe
        snapshot = list(handlers)
        try:
            asyncio.create_task(
                self._dispatch(event, snapshot),
                name=f"event-{event.event_type}",
            )
        except RuntimeError:
            # No running event loop (e.g. during shutdown). Log and skip.
            logger.debug("No event loop available to dispatch %s", event.event_type)

    async def _dispatch(self, event: BaseEvent, handlers: list[Handler]) -> None:
        """Run *handlers* sequentially; catch+log errors per handler."""
        for handler in handlers:
            try:
                await handler(event)
            except Exception:
                logger.exception(
                    "Event handler %s failed for %s",
                    getattr(handler, "__qualname__", handler),
                    event.event_type,
                )

    def handler_count(self, event_type: str | None = None) -> int:
        """Return the number of registered handlers.

        If *event_type* is given, return the count for that type only.
        Otherwise return the total across all types.
        """
        if event_type is not None:
            return len(self._handlers.get(event_type, []))
        return sum(len(h) for h in self._handlers.values())

    @property
    def registered_event_types(self) -> set[str]:
        """Return the set of event types that have at least one handler."""
        return {k for k, v in self._handlers.items() if v}

    def clear(self) -> None:
        """Remove all registered handlers."""
        self._handlers.clear()


# ---------------------------------------------------------------------------
# Global singleton + decorator
# ---------------------------------------------------------------------------

event_bus = EventBus()


def on(event_type: str):
    """Decorator that subscribes the decorated async function to *event_type*."""

    def decorator(fn: Handler) -> Handler:
        event_bus.subscribe(event_type, fn)
        return fn

    return decorator


# ---------------------------------------------------------------------------
# Test double
# ---------------------------------------------------------------------------


class CollectingEventBus(EventBus):
    """Test double that records emitted events without dispatching.

    Overrides :meth:`emit` to synchronously append to :attr:`events` instead
    of creating asyncio tasks. No handler dispatch occurs.
    """

    def __init__(self) -> None:
        super().__init__()
        self.events: list[BaseEvent] = []

    def emit(self, event: BaseEvent) -> None:  # type: ignore[override]
        """Record the event synchronously — no asyncio task, no dispatch."""
        self.events.append(event)

    def reset(self) -> None:
        """Clear recorded events."""
        self.events.clear()

    def events_of_type(self, event_type: str) -> list[BaseEvent]:
        """Return recorded events matching *event_type*."""
        return [e for e in self.events if e.event_type == event_type]
