"""
FastPluggyEventBus — lightweight, thread-safe, string-keyed event bus.
"""
import asyncio
import inspect
import threading
import traceback
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Any, Callable

from loguru import logger

from .event import Event


# ---------------------------------------------------------------------------
# Internal subscription record
# ---------------------------------------------------------------------------

@dataclass
class _Sub:
    fn: Callable
    priority: int = 0
    once: bool = False
    predicate: Callable[[Event], bool] | None = None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _fn_label(fn: Callable) -> str:
    """Human-readable label for a listener callable (module.qualname)."""
    module = getattr(fn, '__module__', '') or ''
    qualname = getattr(fn, '__qualname__', None) or repr(fn)
    return f"{module}.{qualname}" if module else qualname


# ---------------------------------------------------------------------------
# Bus
# ---------------------------------------------------------------------------

class FastPluggyEventBus:
    """
    Publish/subscribe event bus for FastPluggy.

    Basic usage::

        # Subscribe (decorator form)
        @fast_pluggy.events.on("user.login")
        def handle_login(event: Event):
            print(event.payload["user_id"])

        # Subscribe (direct call)
        fast_pluggy.events.on("user.login", handle_login)

        # Emit from a route handler
        request.app.state.events.emit(
            "user.login", source="auth_user", user_id=42, ip="1.2.3.4"
        )

    Features:
    - Thread-safe (``threading.RLock``)
    - Priority ordering (higher int → runs first)
    - One-shot listeners (``once=True``)
    - Per-listener predicate filtering
    - Namespace subscriptions: ``on_namespace("user", fn)`` catches all ``"user.*"``
    - Temporary subscriptions via ``subscribed()`` context manager
    - Async listeners supported (fire-and-forget in a running loop; blocking otherwise)
    - Listener crashes are isolated — error is logged and subsequent listeners still run
    """

    def __init__(self) -> None:
        self._lock = threading.RLock()
        self._listeners: dict[str, list[_Sub]] = {}
        self._ns_listeners: list[tuple[str, _Sub]] = []   # (prefix, sub)
        self._error_handler: Callable[[Exception, Event], None] = self._default_error_handler
        self.events_dispatched: int = 0

    # ------------------------------------------------------------------
    # Configuration
    # ------------------------------------------------------------------

    def on_error(self, handler: Callable[[Exception, Event], None]) -> None:
        """Override the default error handler for listener exceptions."""
        with self._lock:
            self._error_handler = handler

    # ------------------------------------------------------------------
    # Subscription API
    # ------------------------------------------------------------------

    def on(
        self,
        event_name: str,
        listener: Callable | None = None,
        *,
        priority: int = 0,
        once: bool = False,
        predicate: Callable[[Event], bool] | None = None,
    ) -> Callable:
        """
        Subscribe a listener to *event_name*.

        Can be used as a direct call or as a decorator::

            fast_pluggy.events.on("user.login", my_handler)

            @fast_pluggy.events.on("user.login")
            def my_handler(event): ...

        Parameters:
            event_name: Exact topic string.
            listener:   Callable ``(Event) -> None``. If omitted, returns a decorator.
            priority:   Higher runs first (default 0).
            once:       Auto-unsubscribe after the first successful invocation.
            predicate:  Called before the listener; skip if it returns ``False``.
        """
        def _register(fn: Callable) -> Callable:
            sub = _Sub(fn, priority=priority, once=once, predicate=predicate)
            with self._lock:
                arr = self._listeners.setdefault(event_name, [])
                arr.append(sub)
                arr.sort(key=lambda s: s.priority, reverse=True)
            return fn

        if listener is not None:
            _register(listener)
            return listener

        return _register

    def off(self, event_name: str, listener: Callable) -> None:
        """Remove *listener* from *event_name*. No-op if not registered."""
        with self._lock:
            if event_name not in self._listeners:
                return
            self._listeners[event_name] = [
                s for s in self._listeners[event_name] if s.fn is not listener
            ]
            if not self._listeners[event_name]:
                del self._listeners[event_name]

    def on_namespace(
        self,
        prefix: str,
        listener: Callable,
        *,
        priority: int = 0,
        once: bool = False,
        predicate: Callable[[Event], bool] | None = None,
    ) -> None:
        """
        Subscribe *listener* to all events whose name starts with ``prefix + "."``.

        Example — catch ``"user.login"``, ``"user.logout"``, ``"user.register"``::

            fast_pluggy.events.on_namespace("user", audit_handler)
        """
        sub = _Sub(listener, priority=priority, once=once, predicate=predicate)
        with self._lock:
            self._ns_listeners.append((prefix, sub))
            self._ns_listeners.sort(key=lambda t: t[1].priority, reverse=True)

    def off_namespace(self, prefix: str, listener: Callable) -> None:
        """Remove a namespace listener. No-op if not registered."""
        with self._lock:
            self._ns_listeners = [
                (p, s) for p, s in self._ns_listeners
                if not (p == prefix and s.fn is listener)
            ]

    @contextmanager
    def subscribed(
        self,
        event_name: str,
        listener: Callable,
        *,
        priority: int = 0,
        once: bool = False,
        predicate: Callable[[Event], bool] | None = None,
    ):
        """
        Context manager that subscribes *listener* for the duration of the block,
        then automatically removes it. Useful in tests::

            with fast_pluggy.events.subscribed("user.login", my_spy):
                do_login()
            # my_spy is no longer registered
        """
        self.on(event_name, listener, priority=priority, once=once, predicate=predicate)
        try:
            yield
        finally:
            self.off(event_name, listener)

    # ------------------------------------------------------------------
    # Emission
    # ------------------------------------------------------------------

    def emit(self, event_name: str, *, source: str = "", **payload: Any) -> None:
        """
        Build an :class:`Event` and synchronously dispatch it to all listeners.

        Async listeners are handled gracefully:
        - Running event loop (e.g. uvicorn): scheduled as a fire-and-forget task.
        - No running loop (worker thread): ``asyncio.run()`` — blocks until done.

        A listener exception never blocks subsequent listeners; it is passed to the
        error handler (default: log + traceback).

        Example::

            request.app.state.events.emit(
                "user.login", source="auth_user", user_id=42, ip="1.2.3.4"
            )
        """
        self.events_dispatched += 1
        event = Event(name=event_name, source=source, payload=payload)

        # Snapshot listeners under lock to avoid holding the lock during dispatch
        with self._lock:
            exact = list(self._listeners.get(event_name, []))
            ns = [
                (p, sub) for p, sub in self._ns_listeners
                if event_name.startswith(p + ".") or event_name == p
            ]

        once_exact: list[Callable] = []
        once_ns: list[tuple[str, Callable]] = []

        for sub in exact:
            if sub.predicate and not self._safe_predicate(sub.predicate, event):
                continue
            self._call(sub.fn, event)
            if sub.once:
                once_exact.append(sub.fn)

        for prefix, sub in ns:
            if sub.predicate and not self._safe_predicate(sub.predicate, event):
                continue
            self._call(sub.fn, event)
            if sub.once:
                once_ns.append((prefix, sub.fn))

        # Clean up once-only listeners
        if once_exact or once_ns:
            with self._lock:
                for fn in once_exact:
                    if event_name in self._listeners:
                        self._listeners[event_name] = [
                            s for s in self._listeners[event_name] if s.fn is not fn
                        ]
                        if not self._listeners[event_name]:
                            del self._listeners[event_name]
                for prefix, fn in once_ns:
                    self._ns_listeners = [
                        (p, s) for p, s in self._ns_listeners
                        if not (p == prefix and s.fn is fn)
                    ]

    # ------------------------------------------------------------------
    # Introspection
    # ------------------------------------------------------------------

    def listeners(self, event_name: str) -> list[Callable]:
        """Return all registered callables for *event_name* (exact match only)."""
        with self._lock:
            return [s.fn for s in self._listeners.get(event_name, [])]

    def topics(self) -> list[str]:
        """Return all exact event names that have at least one listener."""
        with self._lock:
            return list(self._listeners.keys())

    def listener_count(self) -> int:
        """Total number of registered listeners (exact + namespace)."""
        with self._lock:
            return sum(len(v) for v in self._listeners.values()) + len(self._ns_listeners)

    def topic_info(self) -> list[dict]:
        """Return per-topic subscriber details for the admin UI."""
        with self._lock:
            return [
                {
                    "topic": name,
                    "count": len(subs),
                    "listeners": [_fn_label(s.fn) for s in subs],
                    "flags": [
                        *( ["once"] * sum(1 for s in subs if s.once) ),
                        *( ["predicate"] * sum(1 for s in subs if s.predicate) ),
                    ],
                }
                for name, subs in sorted(self._listeners.items())
            ]

    def namespace_info(self) -> list[dict]:
        """Return per-prefix subscriber details for the admin UI."""
        grouped: dict[str, list[_Sub]] = {}
        with self._lock:
            for prefix, sub in self._ns_listeners:
                grouped.setdefault(prefix, []).append(sub)
        return [
            {
                "prefix": prefix,
                "count": len(subs),
                "listeners": [_fn_label(s.fn) for s in subs],
            }
            for prefix, subs in sorted(grouped.items())
        ]

    # ------------------------------------------------------------------
    # Internals
    # ------------------------------------------------------------------

    def _call(self, fn: Callable, event: Event) -> None:
        try:
            result = fn(event)
            if inspect.isawaitable(result):
                self._run_async(result)
        except Exception as exc:
            self._error_handler(exc, event)

    @staticmethod
    def _run_async(coro) -> None:
        """Schedule or run a coroutine depending on whether a loop is active."""
        try:
            loop = asyncio.get_running_loop()
            loop.create_task(coro)
        except RuntimeError:
            asyncio.run(coro)

    @staticmethod
    def _safe_predicate(pred: Callable[[Event], bool], event: Event) -> bool:
        try:
            return bool(pred(event))
        except Exception:
            return False

    @staticmethod
    def _default_error_handler(exc: Exception, event: Event) -> None:
        logger.error(f"Event listener error for '{event.name}': {exc}")
        traceback.print_exc()
