# #region EventBusModule [C:5] [TYPE Module] [SEMANTICS event,bus,log,buffer,subscriber,flush]
# @BRIEF Task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time
#        WebSocket observers — extracted from the monolithic TaskManager.
# @LAYER Core
# @RELATION DEPENDS_ON -> [LogEntry]
# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]
# @RELATION DEPENDS_ON -> [should_log_task_level]
# @RATIONALE Extracted from TaskManager to satisfy INV_7. EventBus owns log buffering, batch
#            persistence flushing, and pub/sub subscription management — a cohesive unit with
#            independent threading and I/O patterns.
# @REJECTED Keeping log/event concerns inside TaskManager was rejected — the background flusher
#            thread, buffer locks, and subscriber queues are architecturally distinct from task
#            lifecycle state machines and the in-memory task registry.
# @PRE TaskLogPersistenceService is initialized.
# @POST Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers.
# @SIDE_EFFECT Writes task logs to persistence, mutates in-memory buffers, pushes log entries
#              into subscriber queues, runs background flusher thread.
# @INVARIANT Buffered logs are retried on persistence failure; every subscriber receives only
#            task-scoped events.
# @DATA_CONTRACT Input: LogEntry -> Output: persisted log + subscriber notification

import asyncio
import threading
from typing import Any

from ..cot_logger import seed_trace_id
from ..logger import belief_scope, logger, should_log_task_level
from .models import LogEntry, LogFilter, LogStats
from .persistence import TaskLogPersistenceService


# #region EventBus [C:5] [TYPE Class] [SEMANTICS event,bus,log,buffer,subscriber,flush]
# @BRIEF Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out
#        for real-time observers.
# @RELATION DEPENDS_ON -> [LogEntry]
# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]
# @RELATION DEPENDS_ON -> [should_log_task_level]
# @PRE asyncio loop is available for thread-safe queue operations.
# @POST Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers.
# @INVARIANT Buffered logs are retried on persistence failure.
# @RATIONALE Extracted as standalone pub/sub event bus to isolate threading (background flusher
#            thread, buffer locks) and subscription management from task lifecycle and registry.
# @REJECTED Keeping log buffering inside TaskManager was rejected — the background flusher
#            thread and subscriber queues are architecturally distinct from task state machines
#            and should be independently stoppable/testable.
class EventBus:
    """Task-scoped log buffering, persistence flushes, and subscriber fan-out."""

    LOG_FLUSH_INTERVAL = 2.0

    # #region __init__ [TYPE Function]
    # @BRIEF Initialize empty log buffer, subscriber registry, status subscribers, and start background flusher.
    def __init__(self, log_persistence_service: TaskLogPersistenceService, loop: asyncio.AbstractEventLoop):
        self.log_persistence_service = log_persistence_service
        self.loop = loop
        self.subscribers: dict[str, list[asyncio.Queue]] = {}
        # Per-task status subscribers (same pattern as log subscribers)
        self._status_subscribers: dict[str, list[asyncio.Queue]] = {}
        # Global task event subscribers (all tasks)
        self._task_event_subscribers: list[asyncio.Queue] = []
        # Maintenance event subscribers
        self._maintenance_subscribers: list[asyncio.Queue] = []
        self._log_buffer: dict[str, list[LogEntry]] = {}
        self._log_buffer_lock = threading.Lock()
        self._flusher_stop_event = threading.Event()
        self._flusher_thread = threading.Thread(
            target=self._flusher_loop, daemon=True
        )
        self._flusher_thread.start()
    # #endregion __init__

    # #region stop [TYPE Function]
    # @BRIEF Signal the flusher thread to stop and wait for it.
    def stop(self) -> None:
        self._flusher_stop_event.set()
        self._flusher_thread.join(timeout=2)
    # #endregion stop

    # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]
    # @BRIEF Background thread that periodically flushes log buffer to database.
    def _flusher_loop(self):
        seed_trace_id()
        while not self._flusher_stop_event.is_set():
            self._flush_logs()
            self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)
    # #endregion _flusher_loop

    # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]
    # @BRIEF Flush all buffered logs to the database.
    # @RELATION CALLS -> [EXT:method:TaskLogPersistenceService.add_logs]
    def _flush_logs(self):
        seed_trace_id()
        with self._log_buffer_lock:
            task_ids = list(self._log_buffer.keys())
        for task_id in task_ids:
            with self._log_buffer_lock:
                logs = self._log_buffer.pop(task_id, [])
            if logs:
                try:
                    self.log_persistence_service.add_logs(task_id, logs)
                    logger.debug(f"Flushed {len(logs)} logs for task {task_id}")
                except Exception as e:
                    logger.error(f"Failed to flush logs for task {task_id}: {e}")
                    with self._log_buffer_lock:
                        if task_id not in self._log_buffer:
                            self._log_buffer[task_id] = []
                        self._log_buffer[task_id].extend(logs)
    # #endregion _flush_logs

    # #region flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]
    # @BRIEF Flush logs for a specific task immediately.
    # @PRE task_id exists.
    # @POST Task's buffered logs are written to database.
    # @RELATION CALLS -> [EXT:method:TaskLogPersistenceService.add_logs]
    def flush_task_logs(self, task_id: str):
        with belief_scope("EventBus.flush_task_logs"):
            with self._log_buffer_lock:
                logs = self._log_buffer.pop(task_id, [])
            if logs:
                try:
                    self.log_persistence_service.add_logs(task_id, logs)
                except Exception as e:
                    logger.error(f"Failed to flush logs for task {task_id}: {e}")
    # #endregion flush_task_logs

    # #region add_log [C:3] [TYPE Function] [SEMANTICS log,buffer,notify,subscriber]
    # @BRIEF Adds a log entry to a task buffer and notifies subscribers.
    # @PRE Task exists.
    # @POST Log added to buffer and pushed to queues (if level meets task_log_level filter).
    # @RELATION CALLS -> [should_log_task_level]
    def add_log(
        self,
        task_id: str,
        level: str,
        message: str,
        source: str = "system",
        metadata: dict[str, Any] | None = None,
        context: dict[str, Any] | None = None,
        task_logs_list: list[LogEntry] | None = None,
    ):
        # Filter logs based on task_log_level configuration
        if not should_log_task_level(level):
            return
        log_entry = LogEntry(
            level=level,
            message=message,
            source=source,
            metadata=metadata,
            context=context,
        )
        # Add to in-memory logs (for backward compatibility with legacy JSON field)
        if task_logs_list is not None:
            task_logs_list.append(log_entry)
        # Add to buffer for batch persistence
        with self._log_buffer_lock:
            if task_id not in self._log_buffer:
                self._log_buffer[task_id] = []
            self._log_buffer[task_id].append(log_entry)
        # Notify subscribers (for real-time WebSocket updates)
        if task_id in self.subscribers:
            for queue in self.subscribers[task_id]:
                self.loop.call_soon_threadsafe(queue.put_nowait, log_entry)
    # #endregion add_log

    # #region subscribe_logs [C:2] [TYPE Function] [SEMANTICS subscribe,queue,realtime]
    # @BRIEF Subscribes to real-time logs for a task.
    # @POST Returns an asyncio.Queue for log entries.
    async def subscribe_logs(self, task_id: str) -> asyncio.Queue:
        queue = asyncio.Queue()
        if task_id not in self.subscribers:
            self.subscribers[task_id] = []
        self.subscribers[task_id].append(queue)
        return queue
    # #endregion subscribe_logs

    # #region unsubscribe_logs [C:2] [TYPE Function] [SEMANTICS unsubscribe,queue,remove]
    # @BRIEF Unsubscribes from real-time logs for a task.
    # @POST Queue removed from subscribers.
    def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):
        if task_id in self.subscribers:
            if queue in self.subscribers[task_id]:
                self.subscribers[task_id].remove(queue)
            if not self.subscribers[task_id]:
                del self.subscribers[task_id]
    # #endregion unsubscribe_logs

    # ── Task Status Subscribers ──

    # #region subscribe_status [C:2] [TYPE Function] [SEMANTICS subscribe,status,realtime]
    # @BRIEF Subscribes to real-time status updates for a specific task.
    # @POST Returns an asyncio.Queue for Task status dicts.
    async def subscribe_status(self, task_id: str) -> asyncio.Queue:
        queue = asyncio.Queue()
        if task_id not in self._status_subscribers:
            self._status_subscribers[task_id] = []
        self._status_subscribers[task_id].append(queue)
        return queue
    # #endregion subscribe_status

    # #region unsubscribe_status [C:2] [TYPE Function] [SEMANTICS unsubscribe,status,remove]
    # @BRIEF Unsubscribes from real-time status updates for a task.
    # @POST Queue removed from status subscribers.
    def unsubscribe_status(self, task_id: str, queue: asyncio.Queue):
        if task_id in self._status_subscribers:
            if queue in self._status_subscribers[task_id]:
                self._status_subscribers[task_id].remove(queue)
            if not self._status_subscribers[task_id]:
                del self._status_subscribers[task_id]
    # #endregion unsubscribe_status

    # #region broadcast_status [C:2] [TYPE Function] [SEMANTICS broadcast,status,notify]
    # @BRIEF Broadcast a task status update to all per-task and global subscribers.
    # @POST Status event pushed to all matching queues.
    def broadcast_status(self, task_id: str, task_dict: dict):
        from ..logger import logger as _lg
        event = {"type": "task_status", "task_id": task_id, "task": task_dict}
        # Per-task subscribers
        if task_id in self._status_subscribers:
            for queue in self._status_subscribers[task_id]:
                try:
                    self.loop.call_soon_threadsafe(queue.put_nowait, event)
                except Exception as _e:
                    _lg.warning("Failed to send status to per-task subscriber", extra={"task_id": task_id, "error": str(_e)})
        # Global task event subscribers
        for queue in self._task_event_subscribers:
            try:
                self.loop.call_soon_threadsafe(queue.put_nowait, event)
            except Exception as _e:
                _lg.warning("Failed to send status to global subscriber", extra={"task_id": task_id, "error": str(_e)})
    # #endregion broadcast_status

    # ── Maintenance Event Subscribers ──

    # #region subscribe_maintenance_events [C:2] [TYPE Function] [SEMANTICS subscribe,maintenance,events]
    # @BRIEF Subscribes to maintenance event updates (created/ended/banner changes).
    # @POST Returns an asyncio.Queue for maintenance event dicts.
    async def subscribe_maintenance_events(self) -> asyncio.Queue:
        queue = asyncio.Queue()
        self._maintenance_subscribers.append(queue)
        return queue
    # #endregion subscribe_maintenance_events

    # #region unsubscribe_maintenance_events [C:2] [TYPE Function] [SEMANTICS unsubscribe,maintenance,events]
    # @BRIEF Unsubscribes from maintenance events.
    # @POST Queue removed from maintenance subscribers.
    def unsubscribe_maintenance_events(self, queue: asyncio.Queue):
        if queue in self._maintenance_subscribers:
            self._maintenance_subscribers.remove(queue)
    # #endregion unsubscribe_maintenance_events

    # #region broadcast_maintenance_event [C:2] [TYPE Function] [SEMANTICS broadcast,maintenance,notify]
    # @BRIEF Broadcast a maintenance event to all subscribers.
    # @POST Event pushed to all maintenance subscriber queues.
    def broadcast_maintenance_event(self, event: dict):
        from ..logger import logger as _lg
        for queue in self._maintenance_subscribers:
            try:
                self.loop.call_soon_threadsafe(queue.put_nowait, event)
            except Exception as _e:
                _lg.warning("Failed to send maintenance event to subscriber", extra={"event_type": event.get("type"), "error": str(_e)})
    # #endregion broadcast_maintenance_event

    # ── Global Task Event Subscribers ──

    # #region subscribe_task_events [C:2] [TYPE Function] [SEMANTICS subscribe,global,events]
    # @BRIEF Subscribes to ALL task events (status changes) globally.
    # @POST Returns an asyncio.Queue for task event dicts.
    async def subscribe_task_events(self) -> asyncio.Queue:
        queue = asyncio.Queue()
        self._task_event_subscribers.append(queue)
        return queue
    # #endregion subscribe_task_events

    # #region unsubscribe_task_events [C:2] [TYPE Function] [SEMANTICS unsubscribe,global,events]
    # @BRIEF Unsubscribes from global task events.
    # @POST Queue removed from global subscribers.
    def unsubscribe_task_events(self, queue: asyncio.Queue):
        if queue in self._task_event_subscribers:
            self._task_event_subscribers.remove(queue)
    # #endregion unsubscribe_task_events

    # #region get_task_logs [C:3] [TYPE Function] [SEMANTICS logs,read,persistence,backfill]
    # @BRIEF Retrieves logs for a specific task (from memory for running, persistence for completed).
    # @RELATION CALLS -> [EXT:method:TaskLogPersistenceService.get_logs]
    def get_task_logs(
        self, task_id: str, log_filter: LogFilter | None = None, task_status=None, task_logs: list | None = None
    ) -> list[LogEntry]:
        from .models import TaskStatus
        # For completed tasks, fetch from persistence
        if task_status in [TaskStatus.SUCCESS, TaskStatus.FAILED]:
            if log_filter is None:
                log_filter = LogFilter()
            task_logs_persisted = self.log_persistence_service.get_logs(task_id, log_filter)
            return [
                LogEntry(
                    timestamp=log.timestamp,
                    level=log.level,
                    message=log.message,
                    source=log.source,
                    metadata=log.metadata,
                )
                for log in task_logs_persisted
            ]
        # For running/pending tasks, return from memory
        return task_logs if task_logs else []
    # #endregion get_task_logs

    # #region get_task_log_stats [C:2] [TYPE Function] [SEMANTICS log,stats,aggregate]
    # @BRIEF Get statistics about logs for a task.
    # @RELATION CALLS -> [EXT:method:TaskLogPersistenceService.get_log_stats]
    def get_task_log_stats(self, task_id: str) -> LogStats:
        return self.log_persistence_service.get_log_stats(task_id)
    # #endregion get_task_log_stats

    # #region get_task_log_sources [C:2] [TYPE Function] [SEMANTICS log,sources,unique]
    # @BRIEF Get unique sources for a task's logs.
    # @RELATION CALLS -> [EXT:method:TaskLogPersistenceService.get_sources]
    def get_task_log_sources(self, task_id: str) -> list[str]:
        return self.log_persistence_service.get_sources(task_id)
    # #endregion get_task_log_sources

    # #region delete_logs_for_tasks [C:2] [TYPE Function] [SEMANTICS log,delete,cleanup]
    # @BRIEF Delete logs for specified tasks.
    # @RELATION CALLS -> [EXT:method:TaskLogPersistenceService.delete_logs_for_tasks]
    def delete_logs_for_tasks(self, task_ids: list[str]) -> None:
        if task_ids:
            self.log_persistence_service.delete_logs_for_tasks(task_ids)
    # #endregion delete_logs_for_tasks
# #endregion EventBus
# #endregion EventBusModule
