"""ReactiveEventBus — in-process event bus backed by RxPY.

Uses an RxPY :class:`rx.subject.Subject` as the hot observable backbone.
Every :meth:`emit` call:

1. Pushes the event into the reactive stream (for advanced RxPY operators).
2. Dispatches synchronously to any type-specific handlers registered via
   :meth:`subscribe`.

This dual-dispatch model means plain callback-based subscribers work without
any RxPY knowledge, while subscribers that want filtering, buffering, or
debouncing can operate directly on :attr:`stream`.

Based on:
    - docs/specification.md §Event-Driven Architecture
    - Forgejo issue #473
"""

from __future__ import annotations

from collections import deque
from collections.abc import Callable

import structlog
from rx import operators as ops
from rx.core.observable.observable import Observable
from rx.subject.subject import Subject

from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType

_logger = structlog.get_logger(__name__)


def _identity(event: DomainEvent) -> DomainEvent:
    return event


class ReactiveEventBus:
    """In-process event bus using RxPY for real-time distribution.

    Thread safety: This implementation is designed for single-threaded use
    within the main application event loop. The DI container registers it as
    a Singleton. If concurrent access from multiple threads is required,
    callers must provide external synchronization.

    Attributes:
        _subject: Hot RxPY Subject — the raw observable stream.
        _stream: Read-only Observable view over ``_subject``.
        _subscriptions: Per-event-type handler lists.
        _audit_log: Volatile in-memory log of all emitted events.
    """

    def __init__(self, max_audit_log_size: int | None = None) -> None:
        if max_audit_log_size is not None and max_audit_log_size < 1:
            raise ValueError("max_audit_log_size must be >= 1 when provided, or None")

        self._subject: Subject = Subject()
        self._stream: Observable = self._subject.pipe(ops.map(_identity))
        self._subscriptions: dict[EventType, list[Callable[[DomainEvent], None]]] = {}
        self._audit_log: deque[DomainEvent] = deque(maxlen=max_audit_log_size)

    # ------------------------------------------------------------------
    # Public API (satisfies EventBus protocol)
    # ------------------------------------------------------------------

    @property
    def audit_log(self) -> list[DomainEvent]:
        """Return a snapshot of the in-memory event log.

        Every call to :meth:`emit` appends the event to this log,
        guaranteeing a complete in-process audit trail.

        .. note::

            This is a **volatile, in-memory** log intended for runtime
            inspection and testing.  It does **not** replace the durable
            ``audit_log`` SQLite table described in the specification
            (§Persistence Schema).  Durable persistence is wired
            separately via the audit service layer.

            If ``max_audit_log_size`` was configured at construction,
            oldest events are evicted FIFO when the cap is reached.

        Returns:
            A *copy* of the internal audit list to prevent external
            mutation.
        """
        return list(self._audit_log)

    def clear_audit_log(self) -> None:
        """Clear all retained in-memory audit events."""
        self._audit_log.clear()

    def emit(self, event: DomainEvent) -> None:
        """Publish *event* to the reactive stream and all type handlers.

        Execution order follows the specification (§Event System):

        1. Push into the RxPY subject for real-time subscribers.
        2. Append to the in-memory :attr:`audit_log`.
        3. Dispatch to per-type handlers registered via :meth:`subscribe`.

        Args:
            event: The :class:`DomainEvent` to publish.

        Raises:
            TypeError: If *event* is not a :class:`DomainEvent`.
        """
        if not isinstance(event, DomainEvent):
            raise TypeError(
                f"event must be a DomainEvent, got {type(event).__name__!r}"
            )

        try:
            self._subject.on_next(event)
        except Exception as exc:
            _logger.warning(
                "reactive_stream_error",
                event_type=event.event_type.value,
                error_type=type(exc).__name__,
                error=str(exc),
            )

        self._audit_log.append(event.model_copy(deep=True))

        for handler in list(self._subscriptions.get(event.event_type, ())):
            try:
                handler(event)
            except Exception as exc:
                _logger.warning(
                    "event_handler_failed",
                    event_type=event.event_type.value,
                    handler=getattr(handler, "__qualname__", repr(handler)),
                    error_type=type(exc).__name__,
                    error=str(exc),
                )

    def subscribe(
        self,
        event_type: EventType,
        handler: Callable[[DomainEvent], None],
    ) -> None:
        """Register *handler* for events of *event_type*.

        Handlers are called synchronously inside :meth:`emit` in the order
        they were registered.

        Args:
            event_type: The :class:`EventType` to listen for.
            handler: Callable invoked for each matching :class:`DomainEvent`.

        Raises:
            TypeError: If *event_type* is not an :class:`EventType`.
            TypeError: If *handler* is not callable.
        """
        if not isinstance(event_type, EventType):
            raise TypeError(
                f"event_type must be an EventType, got {type(event_type).__name__!r}"
            )
        if not callable(handler):
            raise TypeError("handler must be callable")
        self._subscriptions.setdefault(event_type, []).append(handler)

    @property
    def stream(self) -> Observable:
        """Read-only observable stream for advanced RxPY operators.

        Returns:
            A read-only :class:`~rx.core.observable.observable.Observable`
            view over the internal Subject.
        """
        return self._stream


__all__ = ["ReactiveEventBus"]
