"""Bridge Alerts — proactive notifications to Ward Room & Captain (AD-410).

Monitors ship systems (VitalsMonitor, EmergentDetector, BehavioralMonitor,
TrustNetwork) and posts significant events to the Ward Room as threads.
Crew agents respond naturally via existing AD-407d mechanics.

AD-580: Alert resolution feedback loop — dismiss/resolve/mute suppression.

No LLM calls — purely mechanical threshold checking with deduplication.
"""

from __future__ import annotations

import logging
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

logger = logging.getLogger(__name__)


class AlertSeverity(str, Enum):
    """Bridge Alert severity levels."""
    INFO = "info"          # Department channel, no Captain notification
    ADVISORY = "advisory"  # All Hands, info notification to Captain
    ALERT = "alert"        # All Hands, action_required notification to Captain


@dataclass
class BridgeAlert:
    """A single bridge alert ready for delivery."""
    id: str
    severity: AlertSeverity
    source: str              # "vitals_monitor", "emergent_detector", etc.
    alert_type: str          # "pool_health_warning", "trust_drop", etc.
    title: str
    detail: str
    department: str | None   # For info-severity routing to department channel
    dedup_key: str
    timestamp: float = field(default_factory=time.time)
    related_agent_id: str | None = None
    related_pool: str | None = None


class BridgeAlertService:
    """Evaluates monitoring signals and produces alerts for Ward Room + notifications.

    The service itself does NOT post to the Ward Room — it returns BridgeAlert
    objects that the runtime delivers via _deliver_bridge_alert().
    """

    def __init__(
        self,
        cooldown_seconds: float = 300,
        trust_drop_threshold: float = 0.15,
        trust_drop_alert_threshold: float = 0.25,
        resolve_clean_period: float = 3600.0,
        default_dismiss_duration: float = 14400.0,
    ) -> None:
        self._cooldown = cooldown_seconds
        self._trust_drop_threshold = trust_drop_threshold
        self._trust_drop_alert = trust_drop_alert_threshold
        self._resolve_clean_period = resolve_clean_period
        self._default_dismiss_duration = default_dismiss_duration
        self._recent: dict[str, float] = {}     # dedup_key -> monotonic timestamp
        self._alert_log: list[BridgeAlert] = []
        self._max_log = 200

        # AD-580: Suppression state
        self._dismissed: dict[str, float] = {}   # dedup_key -> expiry monotonic timestamp
        self._resolved: dict[str, float] = {}    # dedup_key -> resolved-at monotonic timestamp
        self._muted: set[str] = set()            # permanently muted keys
        self._last_detected: dict[str, float] = {}  # dedup_key -> last detection monotonic timestamp

    def _should_emit(self, dedup_key: str) -> bool:
        """Check dedup cache. Returns True if alert should fire."""
        now = time.monotonic()
        # AD-580: Always track detection timestamp (independent of emission)
        self._last_detected[dedup_key] = now
        # AD-580: Check suppression before time-based dedup
        if self._is_suppressed(dedup_key):
            return False
        last = self._recent.get(dedup_key)
        if last is not None and now - last < self._cooldown:
            return False
        self._recent[dedup_key] = now
        # Prune expired entries
        cutoff = now - self._cooldown * 2
        self._recent = {k: v for k, v in self._recent.items() if v > cutoff}
        return True

    def _record(self, alert: BridgeAlert) -> None:
        """Add alert to the ring buffer log."""
        self._alert_log.append(alert)
        if len(self._alert_log) > self._max_log:
            self._alert_log = self._alert_log[-self._max_log:]

    # --- AD-580: Alert suppression ---

    def _is_suppressed(self, dedup_key: str) -> bool:
        """Check if a dedup_key is currently suppressed."""
        now = time.monotonic()

        # Muted — indefinite suppression
        if dedup_key in self._muted:
            return True

        # Dismissed — time-limited suppression
        expiry = self._dismissed.get(dedup_key)
        if expiry is not None:
            if now < expiry:
                return True
            # Expired — clean up
            del self._dismissed[dedup_key]

        # Resolved — suppressed unless clean period has elapsed with no detection
        resolved_at = self._resolved.get(dedup_key)
        if resolved_at is not None:
            last_det = self._last_detected.get(dedup_key, resolved_at)
            # If the pattern was detected after resolution, the clean period
            # hasn't started yet. Clean period starts from the last detection.
            gap = now - last_det
            if gap < self._resolve_clean_period:
                # Still within clean period or pattern still active → suppress
                return True
            # Clean period elapsed with no detection → allow re-fire
            del self._resolved[dedup_key]

        return False

    def dismiss_alert(self, dedup_key: str, duration_seconds: float | None = None) -> None:
        """Suppress an alert type for a specified duration (default 4 hours)."""
        duration = duration_seconds if duration_seconds is not None else self._default_dismiss_duration
        self._dismissed[dedup_key] = time.monotonic() + duration
        logger.info("Alert dismissed: %s for %.0fs", dedup_key, duration)

    def resolve_alert(self, dedup_key: str) -> None:
        """Mark an alert as resolved. Re-fires only after a clean detection period."""
        self._resolved[dedup_key] = time.monotonic()
        logger.info("Alert resolved: %s", dedup_key)

    def mute_alert(self, dedup_key: str) -> None:
        """Indefinitely suppress an alert type until unmuted."""
        self._muted.add(dedup_key)
        logger.info("Alert muted: %s", dedup_key)

    def unmute_alert(self, dedup_key: str) -> None:
        """Remove indefinite suppression for an alert type."""
        self._muted.discard(dedup_key)
        logger.info("Alert unmuted: %s", dedup_key)

    def list_suppressed(self) -> list[dict]:
        """Return all currently suppressed alert keys with mode and expiry."""
        now = time.monotonic()
        result: list[dict] = []

        for key, expiry in self._dismissed.items():
            remaining = expiry - now
            if remaining > 0:
                result.append({
                    "dedup_key": key, "mode": "dismissed",
                    "remaining_seconds": round(remaining, 1),
                })

        for key, resolved_at in self._resolved.items():
            last_det = self._last_detected.get(key, resolved_at)
            gap = now - last_det
            result.append({
                "dedup_key": key, "mode": "resolved",
                "clean_gap_seconds": round(gap, 1),
                "clean_period_needed": self._resolve_clean_period,
            })

        for key in self._muted:
            result.append({"dedup_key": key, "mode": "muted"})

        return result

    def find_matching_keys(self, pattern: str) -> list[str]:
        """Find dedup keys matching a pattern (exact or substring).

        Returns exact match first if found, otherwise all substring matches.
        """
        # Collect all known keys from recent emissions + suppression state
        all_keys: set[str] = set()
        all_keys.update(self._recent.keys())
        all_keys.update(self._dismissed.keys())
        all_keys.update(self._resolved.keys())
        all_keys.update(self._muted)
        all_keys.update(self._last_detected.keys())

        if pattern in all_keys:
            return [pattern]

        matches = [k for k in sorted(all_keys) if pattern in k]
        return matches

    # --- Signal processors ---

    def check_vitals(self, vitals_data: dict) -> list[BridgeAlert]:
        """Process VitalsMonitor snapshot data. Returns alerts to emit.

        Expected vitals_data keys:
            pool_health: dict[str, float]   -- pool_name -> health ratio (0-1)
            system_health: float | None     -- overall system health (0-1)
            trust_outlier_count: int        -- number of agents below trust floor
        """
        alerts: list[BridgeAlert] = []

        # Pool health checks
        for pool_name, health in vitals_data.get("pool_health", {}).items():
            if health < 0.25:
                key = f"pool_health_critical:{pool_name}"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()), severity=AlertSeverity.ALERT,
                        source="vitals_monitor", alert_type="pool_health_critical",
                        title=f"Pool '{pool_name}' Critical \u2014 {int(health * 100)}% capacity",
                        detail=(
                            f"Pool '{pool_name}' has dropped to {int(health * 100)}% of target capacity. "
                            f"Agents may be degraded or failing to spawn."
                        ),
                        department="engineering", dedup_key=key,
                        related_pool=pool_name,
                    )
                    self._record(a)
                    alerts.append(a)
            elif health < 0.5:
                key = f"pool_health_warning:{pool_name}"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()), severity=AlertSeverity.ADVISORY,
                        source="vitals_monitor", alert_type="pool_health_warning",
                        title=f"Pool '{pool_name}' Degraded \u2014 {int(health * 100)}% capacity",
                        detail=f"Pool '{pool_name}' is running at {int(health * 100)}% of target capacity.",
                        department="engineering", dedup_key=key,
                        related_pool=pool_name,
                    )
                    self._record(a)
                    alerts.append(a)

        # System health checks
        sys_health = vitals_data.get("system_health")
        if sys_health is not None:
            if sys_health < 0.3:
                key = "system_health_critical"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()), severity=AlertSeverity.ALERT,
                        source="vitals_monitor", alert_type="system_health_critical",
                        title=f"System Health Critical \u2014 {sys_health:.2f}",
                        detail=(
                            f"Overall system health has dropped to {sys_health:.2f}. "
                            f"Multiple agent pools may be affected."
                        ),
                        department=None, dedup_key=key,
                    )
                    self._record(a)
                    alerts.append(a)
            elif sys_health < 0.6:
                key = "system_health_warning"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()), severity=AlertSeverity.ADVISORY,
                        source="vitals_monitor", alert_type="system_health_warning",
                        title=f"System Health Warning \u2014 {sys_health:.2f}",
                        detail=f"Overall system health is at {sys_health:.2f}.",
                        department=None, dedup_key=key,
                    )
                    self._record(a)
                    alerts.append(a)

        # Trust outlier checks
        outlier_count = vitals_data.get("trust_outlier_count", 0)
        if outlier_count > 3:
            key = f"trust_outliers:{outlier_count}"
            if self._should_emit(key):
                a = BridgeAlert(
                    id=str(uuid.uuid4()), severity=AlertSeverity.ADVISORY,
                    source="vitals_monitor", alert_type="trust_outliers",
                    title=f"Multiple Trust Outliers Detected ({outlier_count} agents)",
                    detail=(
                        f"{outlier_count} agents are below the trust floor. "
                        f"This may indicate systemic task difficulty or environmental changes."
                    ),
                    department=None, dedup_key=key,
                )
                self._record(a)
                alerts.append(a)

        return alerts

    def check_trust_change(
        self, agent_id: str, old_score: float, new_score: float,
    ) -> BridgeAlert | None:
        """Check if a trust change is significant enough to alert.

        Args:
            agent_id: The agent whose trust changed.
            old_score: Trust score before the change.
            new_score: Trust score after the change.

        Returns:
            A BridgeAlert if the drop exceeds the threshold, else None.
        """
        drop = old_score - new_score
        if drop < self._trust_drop_threshold:
            return None

        if drop >= self._trust_drop_alert:
            severity = AlertSeverity.ALERT
            alert_type = "trust_drop_alert"
        else:
            severity = AlertSeverity.ADVISORY
            alert_type = "trust_drop_advisory"

        key = f"trust_drop:{agent_id}"
        if not self._should_emit(key):
            return None

        a = BridgeAlert(
            id=str(uuid.uuid4()), severity=severity,
            source="trust_network", alert_type=alert_type,
            title=f"Trust Drop \u2014 {agent_id[:20]} ({old_score:.2f} \u2192 {new_score:.2f})",
            detail=f"Agent {agent_id} experienced a trust drop of {drop:.2f} in a single event.",
            department=None, dedup_key=key,
            related_agent_id=agent_id,
        )
        self._record(a)
        return a

    def check_emergent_patterns(self, patterns: list) -> list[BridgeAlert]:
        """Process EmergentDetector patterns. Returns alerts to emit.

        Args:
            patterns: List of EmergentPattern objects from the detector.
        """
        alerts: list[BridgeAlert] = []
        for p in patterns:
            ptype = getattr(p, "pattern_type", "")
            severity_str = getattr(p, "severity", "info")
            desc = getattr(p, "description", str(p))

            if ptype == "trust_anomaly" and severity_str in ("significant", "notable"):
                sev = AlertSeverity.ADVISORY
            elif ptype == "cooperation_cluster":
                sev = AlertSeverity.INFO
            elif ptype == "routing_shift" and severity_str == "significant":
                sev = AlertSeverity.ADVISORY
            else:
                continue  # Skip info-level or unknown patterns

            key = f"emergent:{ptype}"
            if not self._should_emit(key):
                continue

            dept = None
            if ptype == "cooperation_cluster":
                dept = "science"

            a = BridgeAlert(
                id=str(uuid.uuid4()), severity=sev,
                source="emergent_detector", alert_type=f"emergent_{ptype}",
                title=f"{ptype.replace('_', ' ').title()} Detected",
                detail=desc, department=dept, dedup_key=key,
            )
            self._record(a)
            alerts.append(a)

        return alerts

    def check_behavioral(self, behavioral_monitor: Any) -> list[BridgeAlert]:
        """Check BehavioralMonitor for actionable alerts.

        Args:
            behavioral_monitor: The BehavioralMonitor instance.
        """
        alerts: list[BridgeAlert] = []
        if not behavioral_monitor:
            return alerts

        for alert in behavioral_monitor.get_alerts():
            atype = getattr(alert, "alert_type", "")
            agent_type = getattr(alert, "agent_type", "unknown")

            if atype == "high_failure_rate":
                key = f"behavioral_failure:{agent_type}"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()), severity=AlertSeverity.ADVISORY,
                        source="behavioral_monitor", alert_type="behavioral_failure",
                        title=f"High Failure Rate \u2014 {agent_type}",
                        detail=getattr(
                            alert, "detail",
                            f"Agent type {agent_type} is experiencing a high failure rate (>50%).",
                        ),
                        department="engineering", dedup_key=key,
                    )
                    self._record(a)
                    alerts.append(a)

        # Check for removal recommendations
        status = behavioral_monitor.get_status()
        for agent_type_key in status:
            if behavioral_monitor.should_recommend_removal(agent_type_key):
                key = f"behavioral_removal:{agent_type_key}"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()), severity=AlertSeverity.ALERT,
                        source="behavioral_monitor", alert_type="behavioral_removal",
                        title=f"Agent Removal Recommended \u2014 {agent_type_key}",
                        detail=(
                            f"Agent type '{agent_type_key}' has sustained failure rates "
                            f"warranting removal consideration."
                        ),
                        department="engineering", dedup_key=key,
                    )
                    self._record(a)
                    alerts.append(a)

        return alerts

    def check_llm_health(self, llm_health: dict) -> list[BridgeAlert]:
        """BF-069: Evaluate LLM proxy health and emit bridge alerts."""
        alerts: list[BridgeAlert] = []
        overall = llm_health.get("overall", "unknown")
        tiers = llm_health.get("tiers", {})

        if overall == "offline":
            key = "llm_offline"
            if self._should_emit(key):
                a = BridgeAlert(
                    id=str(uuid.uuid4()),
                    severity=AlertSeverity.ALERT,
                    source="llm_client",
                    alert_type="llm_offline",
                    title="Communications Array Offline",
                    detail="All LLM tiers unreachable. Crew cognitive functions suspended. Check Copilot proxy at 127.0.0.1:8080.",
                    department=None,
                    dedup_key=key,
                )
                self._record(a)
                alerts.append(a)
        elif overall == "degraded":
            unreachable = [t for t, info in tiers.items() if info.get("status") == "unreachable"]
            if unreachable:
                key = f"llm_degraded_{'_'.join(sorted(unreachable))}"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()),
                        severity=AlertSeverity.ADVISORY,
                        source="llm_client",
                        alert_type="llm_degraded",
                        title="Communications Array Degraded",
                        detail=f"LLM tier(s) unreachable: {', '.join(unreachable)}. Remaining tiers operational. Fallback routing active.",
                        department=None,
                        dedup_key=key,
                    )
                    self._record(a)
                    alerts.append(a)

        return alerts

    def check_convergence(self, convergence_data: dict) -> list[BridgeAlert]:
        """AD-551: Evaluate cross-agent convergence and emit bridge alerts."""
        alerts: list[BridgeAlert] = []
        reports_generated = convergence_data.get("convergence_reports_generated", 0)
        if reports_generated <= 0:
            return alerts
        reports = convergence_data.get("convergence_reports", [])
        for report in reports:
            topic = report.get("topic", "unknown")
            agents = report.get("agents", [])
            departments = report.get("departments", [])
            key = f"convergence:{topic}"
            if self._should_emit(key):
                a = BridgeAlert(
                    id=str(uuid.uuid4()),
                    severity=AlertSeverity.ADVISORY,
                    source="dream_consolidation",
                    alert_type="convergence_detected",
                    title="Crew Convergence Detected",
                    detail=(
                        f"{len(agents)} agents from {len(departments)} departments "
                        f"independently reached convergent conclusions on {topic}"
                    ),
                    department=None,
                    dedup_key=key,
                )
                self._record(a)
                alerts.append(a)
        return alerts

    def check_realtime_convergence(self, conv_result: dict) -> list[BridgeAlert]:
        """AD-554: Evaluate real-time cross-agent convergence and emit bridge alerts."""
        alerts: list[BridgeAlert] = []
        if not conv_result.get("convergence_detected"):
            return alerts

        topic = conv_result.get("convergence_topic", "unknown")
        agents = conv_result.get("convergence_agents", [])
        departments = conv_result.get("convergence_departments", [])
        key = f"realtime_convergence:{topic}"

        if self._should_emit(key):
            a = BridgeAlert(
                id=str(uuid.uuid4()),
                severity=AlertSeverity.ADVISORY,
                source="notebook_monitor",
                alert_type="realtime_convergence_detected",
                title="Real-Time Crew Convergence",
                detail=(
                    f"{len(agents)} agents from {len(departments)} departments "
                    f"independently reached convergent conclusions on {topic}"
                ),
                department=None,
                dedup_key=key,
            )
            self._record(a)
            alerts.append(a)
        return alerts

    def check_wrong_convergence(self, conv_result: dict) -> list[BridgeAlert]:
        """AD-583: Escalate convergence with low anchor independence."""
        alerts: list[BridgeAlert] = []
        if not conv_result.get("convergence_detected"):
            return alerts
        if conv_result.get("convergence_is_independent", True):
            return alerts  # Independent convergence — no escalation

        topic = conv_result.get("convergence_topic", "unknown")
        agents = conv_result.get("convergence_agents", [])
        departments = conv_result.get("convergence_departments", [])
        independence = conv_result.get("convergence_independence_score", 0.0)
        key = f"wrong_convergence:{topic}"

        if self._should_emit(key):
            a = BridgeAlert(
                id=str(uuid.uuid4()),
                severity=AlertSeverity.ALERT,  # Escalated from ADVISORY
                source="convergence_monitor",
                alert_type="wrong_convergence_detected",
                title="Possible Echo Chamber Detected",
                detail=(
                    f"{len(agents)} agents from {len(departments)} departments "
                    f"converged on '{topic}' but anchor independence is only "
                    f"{independence:.0%}. Claims may be echoing rather than "
                    f"independently verified."
                ),
                department=None,
                dedup_key=key,
            )
            self._record(a)
            alerts.append(a)
        return alerts

    def check_ward_room_echo(self, echo_result: dict) -> list[BridgeAlert]:
        """AD-583g: Alert when Ward Room thread shows echo amplification."""
        if not echo_result.get("echo_detected"):
            return []
        independence = echo_result.get("anchor_independence_score", 1.0)
        if independence >= 0.3:
            return []  # Independent enough — not an echo chamber

        chain_length = echo_result.get("chain_length", 0)
        source = echo_result.get("source_callsign", "unknown")
        thread_id = echo_result.get("thread_id", "")
        affected = echo_result.get("affected_callsigns", [])

        dedup_key = f"ward_room_echo:{thread_id}"
        if not self._should_emit(dedup_key):
            return []

        alert = BridgeAlert(
            id=str(uuid.uuid4()),
            severity=AlertSeverity.ALERT,
            source="ward_room_monitor",
            alert_type="ward_room_echo_detected",
            title="Ward Room Echo Chamber Detected",
            detail=(
                f"Thread shows {chain_length}-agent amplification chain "
                f"(independence={independence:.2f}). Source: {source}. "
                f"Affected: {', '.join(affected)}."
            ),
            department=None,
            dedup_key=dedup_key,
        )
        self._record(alert)
        return [alert]

    def check_observable_mismatch(self, verification_result: dict) -> list[BridgeAlert]:
        """AD-583f: Alert when agent claims contradict observable state."""
        claims_failed = verification_result.get("claims_failed", 0)
        if claims_failed == 0:
            return []

        thread_id = verification_result.get("thread_id", "")
        dedup_key = f"state_mismatch:{thread_id}"
        if not self._should_emit(dedup_key):
            return []

        severity = AlertSeverity.ALERT if claims_failed >= 2 else AlertSeverity.ADVISORY
        alert = BridgeAlert(
            id=str(uuid.uuid4()),
            severity=severity,
            source="observable_state_monitor",
            alert_type="observable_state_mismatch",
            title="Agent Claims Contradict Observable State",
            detail=verification_result.get("ground_truth_summary", ""),
            department=None,
            dedup_key=dedup_key,
        )
        self._record(alert)
        return [alert]

    def check_divergence(self, divergence_data: dict) -> list[BridgeAlert]:
        """AD-554: Evaluate cross-agent divergence and emit bridge alerts."""
        alerts: list[BridgeAlert] = []
        if not divergence_data.get("divergence_detected"):
            return alerts

        topic = divergence_data.get("divergence_topic", "unknown")
        agents = divergence_data.get("divergence_agents", [])
        departments = divergence_data.get("divergence_departments", [])
        similarity = divergence_data.get("divergence_similarity", 0.0)
        # BF-124: Agent-pair-aware dedup — same pair + same topic suppressed,
        # but new topics for the same pair or new pairs on same topic still fire
        _pair = ":".join(sorted(agents[:2])) if len(agents) >= 2 else "unknown"
        key = f"divergence:{_pair}:{topic}"

        if self._should_emit(key):
            a = BridgeAlert(
                id=str(uuid.uuid4()),
                severity=AlertSeverity.ADVISORY,
                source="notebook_monitor",
                alert_type="divergence_detected",
                title="Cross-Department Divergence",
                detail=(
                    f"{', '.join(agents)} from {', '.join(departments)} "
                    f"reached different conclusions on {topic} "
                    f"(similarity={similarity:.2f})"
                ),
                department=None,
                dedup_key=key,
            )
            self._record(a)
            alerts.append(a)
        return alerts

    def check_notebook_quality(self, quality_snapshot: dict) -> list[BridgeAlert]:
        """AD-555: Check notebook quality metrics for alert conditions."""
        alerts: list[BridgeAlert] = []
        score = quality_snapshot.get("system_quality_score", 1.0)
        stale_rate = quality_snapshot.get("stale_entry_rate", 0.0)

        # Read thresholds from snapshot (caller passes config values)
        low_threshold = quality_snapshot.get("_low_threshold", 0.3)
        warn_threshold = quality_snapshot.get("_warn_threshold", 0.5)
        staleness_alert_rate = quality_snapshot.get("_staleness_alert_rate", 0.7)

        if score < low_threshold:
            key = "notebook_quality_low"
            if self._should_emit(key):
                a = BridgeAlert(
                    id=str(uuid.uuid4()),
                    severity=AlertSeverity.ALERT,
                    source="notebook_quality",
                    alert_type="notebook_quality_low",
                    title="Notebook quality critically low",
                    detail=f"System notebook quality score {score:.2f} — high noise, low signal across crew notebooks",
                    department=None,
                    dedup_key=key,
                )
                self._record(a)
                alerts.append(a)
        elif score < warn_threshold:
            key = "notebook_quality_degraded"
            if self._should_emit(key):
                a = BridgeAlert(
                    id=str(uuid.uuid4()),
                    severity=AlertSeverity.ADVISORY,
                    source="notebook_quality",
                    alert_type="notebook_quality_degraded",
                    title="Notebook quality degraded",
                    detail=f"System notebook quality score {score:.2f} — recommend reviewing agent observation triggers",
                    department=None,
                    dedup_key=key,
                )
                self._record(a)
                alerts.append(a)

        if stale_rate > staleness_alert_rate:
            key = "notebook_staleness_high"
            if self._should_emit(key):
                a = BridgeAlert(
                    id=str(uuid.uuid4()),
                    severity=AlertSeverity.ADVISORY,
                    source="notebook_quality",
                    alert_type="notebook_staleness_high",
                    title="High notebook staleness",
                    detail=f"{stale_rate:.0%} of notebook entries are stale — crew may not be actively observing",
                    department=None,
                    dedup_key=key,
                )
                self._record(a)
                alerts.append(a)

        # Per-agent quality alerts
        for agent in quality_snapshot.get("per_agent", []):
            if isinstance(agent, dict):
                aq_score = agent.get("quality_score", 1.0)
                aq_callsign = agent.get("callsign", "unknown")
            else:
                aq_score = getattr(agent, "quality_score", 1.0)
                aq_callsign = getattr(agent, "callsign", "unknown")

            if aq_score < 0.25:
                key = f"agent_quality_low_{aq_callsign}"
                if self._should_emit(key):
                    a = BridgeAlert(
                        id=str(uuid.uuid4()),
                        severity=AlertSeverity.INFO,
                        source="notebook_quality",
                        alert_type="agent_quality_low",
                        title=f"{aq_callsign}: notebook quality low",
                        detail=f"{aq_callsign} quality score {aq_score:.2f} — may need different observation triggers",
                        department=None,
                        dedup_key=key,
                    )
                    self._record(a)
                    alerts.append(a)

        return alerts

    def check_qualification_drift(self, drift_reports: list) -> list[BridgeAlert]:
        """AD-566c: Evaluate qualification drift and emit bridge alerts.

        Warning severity (2σ): ADVISORY alert, routes to Medical department.
        Critical severity (3σ): ALERT severity, routes to Bridge (all-hands).
        """
        alerts: list[BridgeAlert] = []
        for report in drift_reports:
            if not getattr(report, "drift_detected", False):
                continue
            for signal in getattr(report, "signals", []):
                if signal.severity == "critical":
                    key = f"qual_drift_critical:{signal.agent_id}:{signal.test_name}"
                    if self._should_emit(key):
                        a = BridgeAlert(
                            id=str(uuid.uuid4()),
                            severity=AlertSeverity.ALERT,
                            source="qualification_drift",
                            alert_type="qualification_drift_critical",
                            title=f"Critical Drift — {signal.agent_id[:20]} on {signal.test_name}",
                            detail=(
                                f"Agent {signal.agent_id} shows {signal.z_score:.1f}σ deviation "
                                f"on {signal.test_name} (score: {signal.current_score:.2f}, "
                                f"mean: {signal.mean_score:.2f}, baseline: {signal.baseline_score:.2f}). "
                                f"Direction: {signal.direction}. Automatic investigation recommended."
                            ),
                            department=None,
                            dedup_key=key,
                            related_agent_id=signal.agent_id,
                        )
                        self._record(a)
                        alerts.append(a)
                elif signal.severity == "warning":
                    key = f"qual_drift_warning:{signal.agent_id}:{signal.test_name}"
                    if self._should_emit(key):
                        a = BridgeAlert(
                            id=str(uuid.uuid4()),
                            severity=AlertSeverity.ADVISORY,
                            source="qualification_drift",
                            alert_type="qualification_drift_warning",
                            title=f"Drift Detected — {signal.agent_id[:20]} on {signal.test_name}",
                            detail=(
                                f"Agent {signal.agent_id} shows {signal.z_score:.1f}σ deviation "
                                f"on {signal.test_name} (score: {signal.current_score:.2f}, "
                                f"mean: {signal.mean_score:.2f}). Direction: {signal.direction}."
                            ),
                            department="medical",
                            dedup_key=key,
                            related_agent_id=signal.agent_id,
                        )
                        self._record(a)
                        alerts.append(a)
        return alerts

    def check_cascade_risk(self, cascade_result: dict) -> list[BridgeAlert]:
        """AD-567f: Alert Bridge when cascade confabulation is detected."""
        if cascade_result.get("risk_level") not in ("medium", "high"):
            return []

        risk = cascade_result["risk_level"]
        source = cascade_result.get("source_agent", "unknown")
        affected = cascade_result.get("affected_agents", [])
        count = cascade_result.get("propagation_count", 0)
        independence = cascade_result.get("anchor_independence_score", 0.0)

        key = f"cascade:{source}:{','.join(sorted(affected))}"
        if not self._should_emit(key):
            return []

        severity = AlertSeverity.ALERT if risk == "high" else AlertSeverity.ADVISORY

        a = BridgeAlert(
            id=str(uuid.uuid4()),
            severity=severity,
            source="social_verification",
            alert_type="cascade_confabulation",
            title=f"Cascade confabulation risk ({risk}): {count} agents, anchor independence {independence:.0%}",
            detail=f"Source: {source}. Affected: {', '.join(affected)}. "
                   f"{cascade_result.get('detail', '')}",
            department=None,
            dedup_key=key,
        )
        self._record(a)
        return [a]

    def get_recent_alerts(self, limit: int = 50) -> list[BridgeAlert]:
        """Return recent alerts for status/API exposure."""
        return self._alert_log[-limit:]
