"""
Phase 38: Internal Pub/Sub Event Bus.

Decoupled event system for intra-platform communication. Modules subscribe
to event types and react without knowing about each other — the classic
Observer pattern applied to a multi-agent orchestration platform.

Features:
  - SUBSCRIBE: Register handlers for specific event types
  - PUBLISH: Emit events to all matching subscribers
  - WILDCARD: "task.*" matches task.created, task.completed, etc.
  - FILTER: Optional filter functions narrow delivery per subscriber
  - DEFERRED: Schedule events for later emission
  - LOGGING: All events persisted to JSONL for audit / replay
  - STATS: Emission counts, subscription counts, error tracking

Storage:
  state/broker/events/
    event_log.jsonl          — complete event history
    deferred_events.jsonl    — events scheduled for later firing
"""

import fnmatch
import json
import logging
import threading
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from brynq.server import BROKER_DIR, _append_jsonl_sync, _read_jsonl_sync

logger = logging.getLogger(__name__)

# ── Paths ──────────────────────────────────────────────────────────────────

EVENTS_DIR = BROKER_DIR / "events"
EVENT_LOG_FILE = EVENTS_DIR / "event_log.jsonl"
DEFERRED_FILE = EVENTS_DIR / "deferred_events.jsonl"

for _d in (EVENTS_DIR,):
    _d.mkdir(parents=True, exist_ok=True)

# ── Predefined Event Types ────────────────────────────────────────────────

# Agent lifecycle
AGENT_REGISTERED = "agent.registered"
AGENT_EXILED = "agent.exiled"
AGENT_STATUS_CHANGED = "agent.status_changed"

# Task lifecycle
TASK_CREATED = "task.created"
TASK_ACCEPTED = "task.accepted"
TASK_COMPLETED = "task.completed"
TASK_FAILED = "task.failed"

# Messaging
MESSAGE_SENT = "message.sent"
MESSAGE_RECEIVED = "message.received"

# Jobs
JOB_POSTED = "job.posted"
JOB_HIRED = "job.hired"

# Payments
PAYMENT_MADE = "payment.made"
PAYMENT_FAILED = "payment.failed"

# Workflows
WORKFLOW_STARTED = "workflow.started"
WORKFLOW_COMPLETED = "workflow.completed"
WORKFLOW_FAILED = "workflow.failed"

# Swarms
SWARM_FORMED = "swarm.formed"
SWARM_DISBANDED = "swarm.disbanded"

ALL_EVENT_TYPES = [
    AGENT_REGISTERED, AGENT_EXILED, AGENT_STATUS_CHANGED,
    TASK_CREATED, TASK_ACCEPTED, TASK_COMPLETED, TASK_FAILED,
    MESSAGE_SENT, MESSAGE_RECEIVED,
    JOB_POSTED, JOB_HIRED,
    PAYMENT_MADE, PAYMENT_FAILED,
    WORKFLOW_STARTED, WORKFLOW_COMPLETED, WORKFLOW_FAILED,
    SWARM_FORMED, SWARM_DISBANDED,
]


# ── Subscription ──────────────────────────────────────────────────────────

class Subscription:
    """A single event subscription."""

    __slots__ = (
        "subscription_id", "event_pattern", "handler",
        "subscriber_id", "filter_fn",
    )

    def __init__(
        self,
        event_pattern: str,
        handler: Callable[[dict], Any],
        subscriber_id: Optional[str] = None,
        filter_fn: Optional[Callable[[dict], bool]] = None,
    ):
        self.subscription_id: str = uuid.uuid4().hex[:12]
        self.event_pattern = event_pattern
        self.handler = handler
        self.subscriber_id = subscriber_id
        self.filter_fn = filter_fn


# ── EventBus Core ─────────────────────────────────────────────────────────

class EventBus:
    """Thread-safe publish/subscribe event bus with wildcard support."""

    def __init__(self):
        self._lock = threading.Lock()
        self._subscriptions: Dict[str, Subscription] = {}  # sub_id -> Subscription
        self._stats = {
            "total_emitted": 0,
            "events_by_type": {},
            "handler_errors": 0,
        }

    # ── Subscribe ─────────────────────────────────────────────────────

    def subscribe(
        self,
        event_type: str,
        handler: Callable[[dict], Any],
        subscriber_id: Optional[str] = None,
        filter_fn: Optional[Callable[[dict], bool]] = None,
    ) -> str:
        """Register a handler for an event type (supports wildcards).

        Returns the subscription_id.
        """
        sub = Subscription(event_type, handler, subscriber_id, filter_fn)
        with self._lock:
            self._subscriptions[sub.subscription_id] = sub
        return sub.subscription_id

    # ── Unsubscribe ───────────────────────────────────────────────────

    def unsubscribe(self, subscription_id: str) -> bool:
        """Remove a single subscription. Returns True if found."""
        with self._lock:
            return self._subscriptions.pop(subscription_id, None) is not None

    def unsubscribe_all(self, subscriber_id: str) -> int:
        """Remove all subscriptions for a given subscriber_id.

        Returns the number of subscriptions removed.
        """
        with self._lock:
            to_remove = [
                sid for sid, sub in self._subscriptions.items()
                if sub.subscriber_id == subscriber_id
            ]
            for sid in to_remove:
                del self._subscriptions[sid]
            return len(to_remove)

    # ── Publish ───────────────────────────────────────────────────────

    def emit(
        self,
        event_type: str,
        data: Optional[dict] = None,
        source: Optional[str] = None,
    ) -> dict:
        """Fire an event to all matching subscribers.

        Handlers run synchronously. Exceptions are caught and logged
        without crashing the emission.

        Returns the event record dict.
        """
        now = datetime.now(timezone.utc).isoformat()
        event = {
            "event_id": uuid.uuid4().hex[:12],
            "event_type": event_type,
            "data": data or {},
            "source": source,
            "timestamp": now,
        }

        # Persist to event log
        try:
            _append_jsonl_sync(EVENT_LOG_FILE, event)
        except Exception as exc:
            logger.error("Failed to persist event: %s", exc)

        # Update stats
        with self._lock:
            self._stats["total_emitted"] += 1
            self._stats["events_by_type"][event_type] = (
                self._stats["events_by_type"].get(event_type, 0) + 1
            )
            # Snapshot matching subscriptions under lock
            matching = [
                sub for sub in self._subscriptions.values()
                if self._matches(sub.event_pattern, event_type)
            ]

        # Dispatch outside lock to avoid holding it during handler execution
        for sub in matching:
            try:
                # Apply optional filter
                if sub.filter_fn is not None:
                    if not sub.filter_fn(event):
                        continue
                sub.handler(event)
            except Exception as exc:
                logger.error(
                    "Handler error in subscription %s for event %s: %s",
                    sub.subscription_id, event_type, exc,
                )
                with self._lock:
                    self._stats["handler_errors"] += 1

        return event

    # ── Deferred Events ───────────────────────────────────────────────

    def emit_deferred(
        self,
        event_type: str,
        data: Optional[dict] = None,
        delay_seconds: float = 0,
        source: Optional[str] = None,
    ) -> str:
        """Schedule an event for later emission.

        The event is stored in the deferred file. Call process_deferred()
        periodically to fire any events whose time has come.

        Returns a deferred_id.
        """
        deferred_id = uuid.uuid4().hex[:12]
        fire_at = time.time() + delay_seconds
        record = {
            "deferred_id": deferred_id,
            "event_type": event_type,
            "data": data or {},
            "source": source,
            "fire_at": fire_at,
            "created": datetime.now(timezone.utc).isoformat(),
            "fired": False,
        }
        try:
            _append_jsonl_sync(DEFERRED_FILE, record)
        except Exception as exc:
            logger.error("Failed to persist deferred event: %s", exc)
        return deferred_id

    def process_deferred(self) -> List[dict]:
        """Check for due deferred events and fire them.

        Rewrites the deferred file marking fired events.
        Returns list of events that were emitted.
        """
        now = time.time()
        records = _read_jsonl_sync(DEFERRED_FILE)
        if not records:
            return []

        emitted: List[dict] = []
        updated: List[dict] = []

        for rec in records:
            if not rec.get("fired") and rec.get("fire_at", 0) <= now:
                event = self.emit(rec["event_type"], rec.get("data"), rec.get("source"))
                emitted.append(event)
                rec["fired"] = True
            updated.append(rec)

        # Rewrite deferred file with updated statuses
        try:
            with DEFERRED_FILE.open("w", encoding="utf-8") as f:
                for rec in updated:
                    f.write(json.dumps(rec, default=str) + "\n")
        except Exception as exc:
            logger.error("Failed to rewrite deferred file: %s", exc)

        return emitted

    # ── Query ─────────────────────────────────────────────────────────

    def get_event_log(
        self,
        event_type: Optional[str] = None,
        since: Optional[str] = None,
        limit: int = 100,
    ) -> List[dict]:
        """Query the persisted event log.

        Args:
            event_type: Filter to a specific event type (exact match).
            since: ISO timestamp — only return events after this time.
            limit: Max number of events to return (most recent first).
        """
        events = _read_jsonl_sync(EVENT_LOG_FILE)
        if event_type:
            events = [e for e in events if e.get("event_type") == event_type]
        if since:
            events = [e for e in events if e.get("timestamp", "") >= since]
        # Return most recent first, capped at limit
        return list(reversed(events[-limit:]))

    # ── Stats ─────────────────────────────────────────────────────────

    def get_stats(self) -> dict:
        """Return event bus statistics."""
        with self._lock:
            return {
                "total_emitted": self._stats["total_emitted"],
                "events_by_type": dict(self._stats["events_by_type"]),
                "active_subscriptions": len(self._subscriptions),
                "handler_errors": self._stats["handler_errors"],
            }

    # ── Internals ─────────────────────────────────────────────────────

    @staticmethod
    def _matches(pattern: str, event_type: str) -> bool:
        """Check if an event type matches a subscription pattern.

        Supports:
          - Exact: "task.created" matches "task.created"
          - Wildcard: "task.*" matches "task.created", "task.completed"
          - Global: "*" matches everything
        """
        if pattern == "*":
            return True
        return fnmatch.fnmatch(event_type, pattern)

    def _subscription_count(self) -> int:
        """Return the number of active subscriptions."""
        with self._lock:
            return len(self._subscriptions)


# ── Module-Level Singleton ────────────────────────────────────────────────

_bus = EventBus()


def subscribe(
    event_type: str,
    handler: Callable[[dict], Any],
    subscriber_id: Optional[str] = None,
    filter_fn: Optional[Callable[[dict], bool]] = None,
) -> str:
    """Register a handler for an event type. Returns subscription_id."""
    return _bus.subscribe(event_type, handler, subscriber_id, filter_fn)


def unsubscribe(subscription_id: str) -> bool:
    """Remove a subscription by ID."""
    return _bus.unsubscribe(subscription_id)


def unsubscribe_all(subscriber_id: str) -> int:
    """Remove all subscriptions for a subscriber. Returns count removed."""
    return _bus.unsubscribe_all(subscriber_id)


def emit(
    event_type: str,
    data: Optional[dict] = None,
    source: Optional[str] = None,
) -> dict:
    """Emit an event to all matching subscribers."""
    return _bus.emit(event_type, data, source)


def emit_deferred(
    event_type: str,
    data: Optional[dict] = None,
    delay_seconds: float = 0,
    source: Optional[str] = None,
) -> str:
    """Schedule an event for later emission."""
    return _bus.emit_deferred(event_type, data, delay_seconds, source)


def process_deferred() -> List[dict]:
    """Fire any due deferred events."""
    return _bus.process_deferred()


def get_event_log(
    event_type: Optional[str] = None,
    since: Optional[str] = None,
    limit: int = 100,
) -> List[dict]:
    """Query the persisted event log."""
    return _bus.get_event_log(event_type, since, limit)


def get_stats() -> dict:
    """Return event bus statistics."""
    return _bus.get_stats()


def get_bus() -> EventBus:
    """Return the module-level singleton EventBus instance."""
    return _bus


def reset_bus() -> None:
    """Replace the singleton with a fresh EventBus (testing only)."""
    global _bus
    _bus = EventBus()
