"""
Event Bridge — Unified publish pipeline for NAVIG event systems.

Connects the three previously-disconnected event subsystems:

1. **NervousSystem** (agent-internal, typed EventType enum, in-memory)
2. **SystemEventQueue** (gateway-level, string event_type, disk-persistent)
3. **MCP WebSocket notifications** (JSON-RPC push to authenticated clients)

Architecture:
    NervousSystem ──subscribe_all()──┐
                                     ├──► EventBridge ──► filter ──► broadcast
    SystemEventQueue ──subscribe(*)──┘         │
                                               ▼
                                     MCP WebSocket clients
                                     (per-client SubscriptionFilter)

Usage:
    bridge = EventBridge()
    bridge.attach_nervous_system(ns)
    bridge.attach_event_queue(eq)
    bridge.register_client(ws, SubscriptionFilter(topics={"agent.*"}))
    # Events from either source now push directly to filtered WS clients.
"""

from __future__ import annotations

import asyncio
import json
import logging
import re
import time
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Optional,
    Protocol,
    Set,
    runtime_checkable,
)
from uuid import uuid4

logger = logging.getLogger("navig.event_bridge")

# ---------------------------------------------------------------------------
# Severity classification (unified across both event systems)
# ---------------------------------------------------------------------------

class Severity(str, Enum):
    """Unified severity levels for bridged events."""
    DEBUG = "debug"
    INFO = "info"
    WARNING = "warning"
    ERROR = "error"
    CRITICAL = "critical"


# ---------------------------------------------------------------------------
# EventEnvelope — canonical wire format for all bridged events
# ---------------------------------------------------------------------------

@dataclass(frozen=True, slots=True)
class EventEnvelope:
    """
    Normalized event carried across the bridge.

    Every event from NervousSystem or SystemEventQueue is converted into
    this envelope before filtering and broadcast.
    """
    id: str
    topic: str            # dot-delimited, e.g. "agent.heartbeat", "host.disk_warning"
    source: str           # originating component, e.g. "heart", "cron_service"
    severity: Severity
    timestamp: datetime
    data: Dict[str, Any]
    origin: str           # "nervous_system" | "system_event_queue" | "direct"

    # ---- serialisation helpers ------------------------------------------

    def to_jsonrpc_notification(self) -> Dict[str, Any]:
        """Serialise as a JSON-RPC 2.0 notification (no id)."""
        return {
            "jsonrpc": "2.0",
            "method": f"navig.event.{self.topic}",
            "params": {
                "id": self.id,
                "topic": self.topic,
                "source": self.source,
                "severity": self.severity.value,
                "timestamp": self.timestamp.isoformat(),
                "data": self.data,
                "origin": self.origin,
            },
        }

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "topic": self.topic,
            "source": self.source,
            "severity": self.severity.value,
            "timestamp": self.timestamp.isoformat(),
            "data": self.data,
            "origin": self.origin,
        }


# ---------------------------------------------------------------------------
# SubscriptionFilter — per-client filter specification
# ---------------------------------------------------------------------------

@dataclass
class SubscriptionFilter:
    """
    Client-side subscription preferences.

    Fields use *include-set* semantics — an empty set means "accept all".
    Topic patterns support simple globs: ``agent.*`` matches ``agent.heartbeat``.
    """
    topics: Set[str] = field(default_factory=set)
    severities: Set[Severity] = field(default_factory=set)
    sources: Set[str] = field(default_factory=set)

    # pre-compiled regexes (built lazily)
    _topic_patterns: Optional[List[re.Pattern]] = field(
        default=None, init=False, repr=False, compare=False
    )

    def _compile_topics(self) -> List[re.Pattern]:
        if self._topic_patterns is None:
            patterns: List[re.Pattern] = []
            for t in self.topics:
                # Convert simple glob to regex: "agent.*" → "^agent\..*$"
                regex = "^" + re.escape(t).replace(r"\*", ".*") + "$"
                patterns.append(re.compile(regex))
            self._topic_patterns = patterns
        return self._topic_patterns

    def matches(self, envelope: EventEnvelope) -> bool:
        """Return True if the envelope passes this filter."""
        # Topic filter
        if self.topics:
            patterns = self._compile_topics()
            if not any(p.match(envelope.topic) for p in patterns):
                return False

        # Severity filter
        if self.severities and envelope.severity not in self.severities:
            return False

        # Source filter
        if self.sources and envelope.source not in self.sources:
            return False

        return True

    def to_dict(self) -> Dict[str, Any]:
        return {
            "topics": sorted(self.topics),
            "severities": sorted(s.value for s in self.severities),
            "sources": sorted(self.sources),
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "SubscriptionFilter":
        return cls(
            topics=set(data.get("topics", [])),
            severities={Severity(s) for s in data.get("severities", [])},
            sources=set(data.get("sources", [])),
        )

    @classmethod
    def accept_all(cls) -> "SubscriptionFilter":
        """Factory: filter that accepts every event."""
        return cls()


# ---------------------------------------------------------------------------
# WebSocket client protocol (duck-typed)
# ---------------------------------------------------------------------------

@runtime_checkable
class WebSocketLike(Protocol):
    """Minimal protocol a WebSocket object must satisfy."""
    async def send(self, data: str) -> None: ...


# ---------------------------------------------------------------------------
# Severity mapping helpers
# ---------------------------------------------------------------------------

# NervousSystem EventPriority → Severity
_PRIORITY_TO_SEVERITY = {
    "LOW": Severity.DEBUG,
    "NORMAL": Severity.INFO,
    "HIGH": Severity.WARNING,
    "CRITICAL": Severity.CRITICAL,
}

# SystemEventQueue EventPriority → Severity
_SYS_PRIORITY_TO_SEVERITY = _PRIORITY_TO_SEVERITY  # same enum names

# NervousSystem EventType name → topic prefix mapping
_EVENT_TYPE_TOPIC_MAP: Dict[str, str] = {
    # Lifecycle
    "AGENT_STARTING": "agent.starting",
    "AGENT_STARTED": "agent.started",
    "AGENT_STOPPING": "agent.stopping",
    "AGENT_STOPPED": "agent.stopped",
    # Component
    "COMPONENT_STARTING": "component.starting",
    "COMPONENT_STARTED": "component.started",
    "COMPONENT_STOPPING": "component.stopping",
    "COMPONENT_STOPPED": "component.stopped",
    "COMPONENT_ERROR": "component.error",
    "COMPONENT_DEGRADED": "component.degraded",
    # Heart
    "HEARTBEAT": "agent.heartbeat",
    "HEALTH_CHECK": "agent.health_check",
    # Eyes
    "METRIC_COLLECTED": "monitor.metric",
    "ALERT_TRIGGERED": "monitor.alert",
    "ANOMALY_DETECTED": "monitor.anomaly",
    "LOG_ENTRY": "monitor.log",
    "FILE_CHANGED": "monitor.file_changed",
    # Ears
    "MESSAGE_RECEIVED": "input.message",
    "COMMAND_RECEIVED": "input.command",
    "WEBHOOK_RECEIVED": "input.webhook",
    "USER_INPUT": "input.user",
    # Hands
    "COMMAND_STARTED": "exec.started",
    "COMMAND_COMPLETED": "exec.completed",
    "COMMAND_FAILED": "exec.failed",
    "ACTION_PENDING": "approval.pending",
    "ACTION_APPROVED": "approval.approved",
    "ACTION_REJECTED": "approval.rejected",
    # Brain
    "THOUGHT": "brain.thought",
    "DECISION_MADE": "brain.decision",
    "PLAN_CREATED": "brain.plan",
    "LEARNING": "brain.learning",
    "REASONING": "brain.reasoning",
    # Memory
    "CONTEXT_UPDATED": "memory.context",
    "MEMORY_STORED": "memory.stored",
    "MEMORY_RECALLED": "memory.recalled",
    # Soul
    "MOOD_CHANGED": "soul.mood",
    "RESPONSE_GENERATED": "soul.response",
    # System
    "SYSTEM_INFO": "system.info",
    "SYSTEM_WARNING": "system.warning",
    "SYSTEM_ERROR": "system.error",
    # Custom
    "CUSTOM": "custom",
}

# Severity hints derived from EventType name
_EVENT_TYPE_SEVERITY_HINTS: Dict[str, Severity] = {
    "COMPONENT_ERROR": Severity.ERROR,
    "COMPONENT_DEGRADED": Severity.WARNING,
    "ALERT_TRIGGERED": Severity.WARNING,
    "ANOMALY_DETECTED": Severity.WARNING,
    "COMMAND_FAILED": Severity.ERROR,
    "SYSTEM_WARNING": Severity.WARNING,
    "SYSTEM_ERROR": Severity.ERROR,
}


# ---------------------------------------------------------------------------
# EventBridge — the core unifier
# ---------------------------------------------------------------------------

class EventBridge:
    """
    Central event bridge that unifies NervousSystem, SystemEventQueue, and
    MCP WebSocket notifications into a single push pipeline.

    Lifecycle:
        bridge = EventBridge()
        bridge.attach_nervous_system(ns)       # optional
        bridge.attach_event_queue(eq)           # optional
        bridge.register_client(ws, filter)
        ...
        bridge.detach_all()
    """

    def __init__(
        self,
        *,
        debounce_seconds: float = 0.5,
        max_payload_bytes: int = 131_072,
        broadcast_timeout: float = 0.25,
        enable_ipc_offload: bool = False,
        ipc_socket_path: Optional[str] = None,
    ):
        self.debounce_seconds = debounce_seconds
        self.max_payload_bytes = max_payload_bytes
        self.broadcast_timeout = broadcast_timeout
        self.enable_ipc_offload = enable_ipc_offload
        import sys
        self.ipc_socket_path = ipc_socket_path or (
            r"\\.\pipe\navig-sysd.sock" if sys.platform == "win32"
            else "/tmp/navig-sysd.sock"
        )

        # Client registry: ws → filter
        self._clients: Dict[int, tuple[WebSocketLike, SubscriptionFilter]] = {}

        # Recent envelopes (for debounce + dedup)
        self._recent: Dict[str, float] = {}   # topic → last_emit_time
        self._dedup_window: float = 0.3        # seconds

        # History ring buffer
        self._history: List[EventEnvelope] = []
        self._max_history: int = 500

        # Attached sources
        self._ns_attached = False
        self._eq_attached = False

        # Stats
        self._stats = {
            "events_received": 0,
            "events_broadcast": 0,
            "events_filtered": 0,
            "events_dropped": 0,
            "clients_registered": 0,
        }

        # Listeners to remove on detach
        self._ns_handler: Optional[Callable] = None
        self._eq_handler: Optional[Callable] = None

        # References to attached sources
        self._nervous_system: Any = None
        self._event_queue: Any = None

    # ------------------------------------------------------------------
    # Source attachment
    # ------------------------------------------------------------------

    def attach_nervous_system(self, ns: Any) -> None:
        """
        Subscribe to all NervousSystem events and forward them.

        Args:
            ns: A NervousSystem instance.
        """
        if self._ns_attached:
            return

        async def _on_ns_event(event: Any) -> None:
            envelope = self._normalise_ns_event(event)
            await self.push(envelope)

        ns.subscribe_all(_on_ns_event)
        self._ns_handler = _on_ns_event
        self._nervous_system = ns
        self._ns_attached = True
        logger.info("EventBridge attached to NervousSystem")

    def attach_event_queue(self, eq: Any) -> None:
        """
        Subscribe to all SystemEventQueue events and forward them.

        Args:
            eq: A SystemEventQueue instance.
        """
        if self._eq_attached:
            return

        async def _on_eq_event(event: Any) -> None:
            envelope = self._normalise_eq_event(event)
            await self.push(envelope)

        eq.subscribe("*", _on_eq_event)
        self._eq_handler = _on_eq_event
        self._event_queue = eq
        self._eq_attached = True
        logger.info("EventBridge attached to SystemEventQueue")

    def detach_all(self) -> None:
        """Remove subscriptions from attached sources."""
        if self._ns_attached and self._nervous_system and self._ns_handler:
            self._nervous_system.unsubscribe_all(self._ns_handler)
            self._ns_attached = False
            logger.info("EventBridge detached from NervousSystem")

        if self._eq_attached and self._event_queue and self._eq_handler:
            self._event_queue.unsubscribe("*", self._eq_handler)
            self._eq_attached = False
            logger.info("EventBridge detached from SystemEventQueue")

    # ------------------------------------------------------------------
    # Client management
    # ------------------------------------------------------------------

    def register_client(
        self,
        ws: WebSocketLike,
        subscription: Optional[SubscriptionFilter] = None,
    ) -> None:
        """Register a WebSocket client with an optional subscription filter."""
        filt = subscription or SubscriptionFilter.accept_all()
        self._clients[id(ws)] = (ws, filt)
        self._stats["clients_registered"] += 1
        logger.debug(f"Client registered: {id(ws)} filter={filt.to_dict()}")

    def unregister_client(self, ws: WebSocketLike) -> None:
        """Remove a WebSocket client."""
        self._clients.pop(id(ws), None)
        logger.debug(f"Client unregistered: {id(ws)}")

    def update_client_filter(
        self, ws: WebSocketLike, subscription: SubscriptionFilter
    ) -> bool:
        """Update the subscription filter for an existing client."""
        key = id(ws)
        if key in self._clients:
            old_ws, _ = self._clients[key]
            self._clients[key] = (old_ws, subscription)
            return True
        return False

    @property
    def client_count(self) -> int:
        return len(self._clients)

    # ------------------------------------------------------------------
    # Core push pipeline
    # ------------------------------------------------------------------

    async def push(self, envelope: EventEnvelope) -> int:
        """
        Push an event envelope through the bridge.

        Returns the number of clients that received the event.
        """
        self._stats["events_received"] += 1

        # Store in history
        self._history.append(envelope)
        if len(self._history) > self._max_history:
            self._history = self._history[-self._max_history:]

        # No clients → skip broadcast
        if not self._clients:
            return 0

        # Rate limit: suppress rapid-fire duplicates of same topic
        # dynamically based on serverity to protect UI from freezing
        if envelope.severity in (Severity.DEBUG, Severity.INFO):
            window = self.debounce_seconds
        elif envelope.severity == Severity.WARNING:
            window = 0.2
        else:  # ERROR, CRITICAL
            window = 0.0

        now = time.monotonic()
        last = self._recent.get(envelope.topic)
        if last is not None and (now - last) < window:
            self._stats["events_filtered"] += 1
            # Return early without updating last_time, creating a rate limit
            # instead of a trailing debounce that would indefinitely silence streams.
            return 0

        self._recent[envelope.topic] = now

        # Build JSON-RPC payload once
        payload_obj = envelope.to_jsonrpc_notification()
        payload_str = json.dumps(payload_obj, default=str)

        # Fast-path: offload to Go daemon via IPC
        if self.enable_ipc_offload:
            asyncio.create_task(self._forward_to_ipc(payload_str))

        # Truncate oversized payloads
        if len(payload_str.encode("utf-8", errors="replace")) > self.max_payload_bytes:
            payload_obj["params"] = {
                "id": envelope.id,
                "topic": envelope.topic,
                "severity": envelope.severity.value,
                "timestamp": envelope.timestamp.isoformat(),
                "truncated": True,
            }
            payload_str = json.dumps(payload_obj, default=str)

        # Broadcast to matching clients
        sent = 0
        dead: List[int] = []

        for key, (ws, filt) in list(self._clients.items()):
            if not filt.matches(envelope):
                self._stats["events_filtered"] += 1
                continue
            try:
                await asyncio.wait_for(
                    ws.send(payload_str), timeout=self.broadcast_timeout
                )
                sent += 1
            except Exception:
                dead.append(key)
                self._stats["events_dropped"] += 1

        # Prune dead sockets
        for key in dead:
            self._clients.pop(key, None)

        self._stats["events_broadcast"] += sent
        return sent

    async def push_direct(
        self,
        topic: str,
        source: str,
        data: Optional[Dict[str, Any]] = None,
        severity: Severity = Severity.INFO,
    ) -> int:
        """Push an event directly (not from NervousSystem or SystemEventQueue)."""
        envelope = EventEnvelope(
            id=str(uuid4())[:8],
            topic=topic,
            source=source,
            severity=severity,
            timestamp=datetime.now(),
            data=data or {},
            origin="direct",
        )
        return await self.push(envelope)

    async def _forward_to_ipc(self, payload: str) -> None:
        """
        Forward event payload directly to the Go IPC daemon over domain sockets
        or named pipes, bypassing the heavier Python asyncio broadcast loop.
        """
        if not self.ipc_socket_path:
            return

        import sys
        try:
            if sys.platform == "win32":
                # Named pipe connection
                reader, writer = await asyncio.open_connection(self.ipc_socket_path)
            else:
                # Unix domain socket connection
                reader, writer = await asyncio.open_unix_connection(self.ipc_socket_path)

            writer.write(payload.encode("utf-8") + b"\n")
            await writer.drain()
            writer.close()
            await writer.wait_closed()
        except Exception as e:
            # Silently fail if native daemon isn't running yet
            _logger = logging.getLogger("navig.event_bridge.ipc")
            _logger.debug(f"IPC offload skipped: {e}")

    # ------------------------------------------------------------------
    # Normalisation helpers
    # ------------------------------------------------------------------

    def _normalise_ns_event(self, event: Any) -> EventEnvelope:
        """Convert a NervousSystem Event to EventEnvelope."""
        type_name = event.type.name if hasattr(event.type, "name") else str(event.type)
        topic = _EVENT_TYPE_TOPIC_MAP.get(type_name, f"agent.{type_name.lower()}")

        priority_name = (
            event.priority.name if hasattr(event.priority, "name") else "NORMAL"
        )
        severity = _EVENT_TYPE_SEVERITY_HINTS.get(
            type_name, _PRIORITY_TO_SEVERITY.get(priority_name, Severity.INFO)
        )

        return EventEnvelope(
            id=getattr(event, "id", str(uuid4())[:8]),
            topic=topic,
            source=getattr(event, "source", "unknown"),
            severity=severity,
            timestamp=getattr(event, "timestamp", datetime.now()),
            data=getattr(event, "data", {}),
            origin="nervous_system",
        )

    def _normalise_eq_event(self, event: Any) -> EventEnvelope:
        """Convert a SystemEventQueue SystemEvent to EventEnvelope."""
        event_type = getattr(event, "event_type", "unknown")
        # Convert underscore-style to dot-delimited: "host_check" → "host.check"
        topic = event_type.replace("_", ".")

        priority_name = (
            event.priority.name
            if hasattr(event, "priority") and hasattr(event.priority, "name")
            else "NORMAL"
        )
        severity = _SYS_PRIORITY_TO_SEVERITY.get(priority_name, Severity.INFO)

        return EventEnvelope(
            id=getattr(event, "id", str(uuid4())[:8]),
            topic=topic,
            source=getattr(event, "payload", {}).get("source", "gateway"),
            severity=severity,
            timestamp=getattr(event, "timestamp", datetime.now()),
            data=getattr(event, "payload", {}),
            origin="system_event_queue",
        )

    # ------------------------------------------------------------------
    # History / stats
    # ------------------------------------------------------------------

    def get_history(
        self,
        *,
        topic: Optional[str] = None,
        severity: Optional[Severity] = None,
        source: Optional[str] = None,
        limit: int = 50,
    ) -> List[EventEnvelope]:
        """Query bridge event history with optional filters."""
        events = self._history

        if topic:
            pat = re.compile("^" + re.escape(topic).replace(r"\*", ".*") + "$")
            events = [e for e in events if pat.match(e.topic)]

        if severity:
            events = [e for e in events if e.severity == severity]

        if source:
            events = [e for e in events if e.source == source]

        return events[-limit:]

    def get_stats(self) -> Dict[str, Any]:
        """Return bridge statistics."""
        return {
            **self._stats,
            "active_clients": len(self._clients),
            "history_size": len(self._history),
            "ns_attached": self._ns_attached,
            "eq_attached": self._eq_attached,
        }

    def reset_stats(self) -> None:
        """Reset counters (not history)."""
        for k in self._stats:
            self._stats[k] = 0
