"""Lightweight in-process event bus for inter-module communication.

Usage:
    from core.event_bus import bus

    # Subscribe
    bus.on("voice.user_said", my_handler)       # sync or async callable
    bus.on("motion.action_done", other_handler)

    # Publish
    await bus.emit("voice.user_said", text="hello")
"""

from __future__ import annotations

import asyncio
import threading
from collections import defaultdict
from typing import Any, Callable

from Project.Sanad.core.logger import get_logger

log = get_logger("event_bus", to_console=False)


class EventBus:
    def __init__(self):
        self._lock = threading.Lock()
        self._listeners: dict[str, list[Callable]] = defaultdict(list)

    def on(self, event: str, callback: Callable):
        with self._lock:
            self._listeners[event].append(callback)
            log.debug("Subscribed %s → %s", event, callback.__qualname__)

    def off(self, event: str, callback: Callable):
        with self._lock:
            try:
                self._listeners[event].remove(callback)
            except ValueError:
                pass

    async def emit(self, event: str, **kwargs: Any):
        with self._lock:
            handlers = list(self._listeners.get(event, []))
        for handler in handlers:
            try:
                result = handler(**kwargs)
                if asyncio.iscoroutine(result):
                    await result
            except Exception:
                log.exception("Handler %s for event '%s' failed", handler.__qualname__, event)

    def emit_sync(self, event: str, **kwargs: Any):
        """Fire-and-forget from a sync context.

        Async handlers are scheduled on the running event loop if one exists.
        Otherwise they are dropped with a warning (the original silent-no-op
        bug — at least now it's logged).
        """
        with self._lock:
            handlers = list(self._listeners.get(event, []))
        for handler in handlers:
            try:
                if asyncio.iscoroutinefunction(handler):
                    try:
                        loop = asyncio.get_running_loop()
                        loop.create_task(handler(**kwargs))
                    except RuntimeError:
                        log.warning(
                            "Async handler %s for '%s' dropped — no running loop",
                            handler.__qualname__, event,
                        )
                    continue
                result = handler(**kwargs)
                if asyncio.iscoroutine(result):
                    # Sync handler returned a coroutine — schedule it
                    try:
                        loop = asyncio.get_running_loop()
                        loop.create_task(result)
                    except RuntimeError:
                        result.close()
                        log.warning(
                            "Coroutine result from %s for '%s' dropped — no running loop",
                            handler.__qualname__, event,
                        )
            except Exception:
                log.exception("Handler %s for event '%s' failed", handler.__qualname__, event)


bus = EventBus()
