"""
Agent Quality Watchdog — Anomaly Detection & Auto-Demotion
============================================================

Moat feature: No competitor (CrewAI, AutoGen, LangGraph) has this.
Tracks per-agent quality over time, detects sudden degradation,
auto-demotes bad agents from routing until they recover.

Uses rolling window statistics + Z-score anomaly detection (stdlib only).

Usage:
    watchdog = get_watchdog()
    watchdog.record("chatgpt-coder", quality=8.5, response_time=2.3, success=True)
    status = watchdog.get_agent_status("chatgpt-coder")  # "active" or "demoted"
    demoted = watchdog.get_demoted_agents()  # ["grok-coder"] if degraded
"""

import json
import logging
import math
import os
import time
import uuid
from collections import deque
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List

logger = logging.getLogger(__name__)


@dataclass
class QualityDataPoint:
    """A single quality measurement for an agent."""
    timestamp: float
    quality_score: float  # 0-10
    response_time: float  # seconds
    success: bool
    task_type: str = ""
    tokens: int = 0


@dataclass
class AgentHealth:
    """Health status for a single agent."""
    agent_name: str
    status: str = "active"  # active, warning, demoted, probation
    demoted_at: float = 0.0
    demoted_reason: str = ""
    probation_until: float = 0.0
    consecutive_failures: int = 0
    total_recordings: int = 0
    alerts: List[Dict[str, Any]] = field(default_factory=list)

    # Rolling stats
    avg_quality: float = 0.0
    avg_response_time: float = 0.0
    success_rate: float = 1.0
    quality_trend: str = "stable"  # improving, stable, declining


@dataclass
class WatchdogAlert:
    """An alert generated by the watchdog."""
    alert_id: str
    agent_name: str
    alert_type: str  # quality_drop, slow_response, high_failure, recovery
    severity: str  # info, warning, critical
    message: str
    timestamp: str
    current_value: float
    baseline_value: float
    resolved: bool = False


# Thresholds
QUALITY_DROP_ZSCORE = 2.0       # Z-score for quality anomaly
RESPONSE_TIME_ZSCORE = 2.5     # Z-score for response time anomaly
FAILURE_THRESHOLD = 3           # Consecutive failures before demotion
DEMOTE_QUALITY_FLOOR = 4.0     # Absolute quality floor (0-10)
PROBATION_SECONDS = 300        # 5 min probation after recovery
RECOVERY_QUALITY_MIN = 6.0     # Min quality to recover from demotion
WINDOW_SIZE = 20               # Rolling window size


class QualityWatchdog:
    """
    Monitors agent quality and auto-manages agent health.

    Detection methods:
    - Z-score anomaly: quality drop > 2 standard deviations from rolling mean
    - Consecutive failures: 3+ failures in a row → demotion
    - Absolute floor: quality < 4.0 → immediate demotion
    - Response time spike: response time > 2.5 std devs → warning

    Recovery:
    - Demoted agents get periodic test queries (probation)
    - If quality recovers above threshold for 3+ queries → promote back
    - Gradual trust restoration (probation period)
    """

    def __init__(self, storage_dir: str = "~/.ai-dev-team/watchdog"):
        self.storage_dir = Path(os.path.expanduser(storage_dir))
        self.storage_dir.mkdir(parents=True, exist_ok=True)
        self._windows: Dict[str, deque] = {}  # agent -> deque of QualityDataPoint
        self._health: Dict[str, AgentHealth] = {}
        self._alerts: List[WatchdogAlert] = []
        self._load()

    def record(
        self,
        agent_name: str,
        quality_score: float,
        response_time: float = 0.0,
        success: bool = True,
        task_type: str = "",
        tokens: int = 0,
    ):
        """Record a quality data point for an agent."""
        if agent_name not in self._windows:
            self._windows[agent_name] = deque(maxlen=WINDOW_SIZE)
        if agent_name not in self._health:
            self._health[agent_name] = AgentHealth(agent_name=agent_name)

        dp = QualityDataPoint(
            timestamp=time.time(),
            quality_score=quality_score,
            response_time=response_time,
            success=success,
            task_type=task_type,
            tokens=tokens,
        )
        self._windows[agent_name].append(dp)
        self._health[agent_name].total_recordings += 1

        # Run checks
        self._check_quality(agent_name, dp)
        self._check_failures(agent_name, dp)
        self._check_response_time(agent_name, dp)
        self._check_recovery(agent_name, dp)
        self._update_rolling_stats(agent_name)

        # Save periodically
        if self._health[agent_name].total_recordings % 5 == 0:
            self._save()

    def get_agent_status(self, agent_name: str) -> str:
        """Get current status of an agent: active, warning, demoted, probation."""
        health = self._health.get(agent_name)
        if not health:
            return "unknown"

        # Check if probation period expired → promote
        if health.status == "probation" and time.time() > health.probation_until:
            health.status = "active"
            self._create_alert(agent_name, "recovery", "info",
                               f"{agent_name} completed probation — fully active")

        return health.status

    def get_demoted_agents(self) -> List[str]:
        """Get list of currently demoted agents."""
        return [
            name for name, health in self._health.items()
            if health.status in ("demoted", "probation")
        ]

    def get_active_agents(self, available: List[str]) -> List[str]:
        """Filter a list of agents, removing demoted ones."""
        demoted = set(self.get_demoted_agents())
        return [a for a in available if a not in demoted]

    def get_health_report(self) -> Dict[str, Any]:
        """Get comprehensive health report for all agents."""
        report = {
            "agents": {},
            "demoted_count": 0,
            "total_alerts": len(self._alerts),
            "active_alerts": sum(1 for a in self._alerts if not a.resolved),
        }

        for name, health in self._health.items():
            status = self.get_agent_status(name)
            report["agents"][name] = {
                "status": status,
                "avg_quality": round(health.avg_quality, 2),
                "avg_response_time": round(health.avg_response_time, 2),
                "success_rate": f"{health.success_rate:.0%}",
                "quality_trend": health.quality_trend,
                "total_recordings": health.total_recordings,
                "consecutive_failures": health.consecutive_failures,
            }
            if status in ("demoted", "probation"):
                report["demoted_count"] += 1
                report["agents"][name]["demoted_reason"] = health.demoted_reason

        return report

    def get_alerts(self, limit: int = 20, agent_name: str = "") -> List[Dict[str, Any]]:
        """Get recent alerts."""
        alerts = self._alerts
        if agent_name:
            alerts = [a for a in alerts if a.agent_name == agent_name]
        sorted_alerts = sorted(alerts, key=lambda a: a.timestamp, reverse=True)
        return [asdict(a) for a in sorted_alerts[:limit]]

    def force_demote(self, agent_name: str, reason: str = "manual") -> bool:
        """Manually demote an agent."""
        if agent_name not in self._health:
            self._health[agent_name] = AgentHealth(agent_name=agent_name)
        self._demote(agent_name, reason)
        return True

    def force_promote(self, agent_name: str) -> bool:
        """Manually promote a demoted agent."""
        health = self._health.get(agent_name)
        if not health:
            return False
        health.status = "active"
        health.demoted_at = 0.0
        health.demoted_reason = ""
        health.consecutive_failures = 0
        self._create_alert(agent_name, "recovery", "info", f"{agent_name} manually promoted")
        self._save()
        return True

    def _check_quality(self, agent_name: str, dp: QualityDataPoint):
        """Check for quality anomalies."""
        window = self._windows[agent_name]
        if len(window) < 5:
            return

        # Absolute floor check
        if dp.quality_score < DEMOTE_QUALITY_FLOOR:
            self._demote(agent_name, f"Quality below floor: {dp.quality_score:.1f} < {DEMOTE_QUALITY_FLOOR}")
            return

        # Z-score check
        scores = [p.quality_score for p in window]
        mean = sum(scores) / len(scores)
        if len(scores) > 1:
            variance = sum((x - mean) ** 2 for x in scores) / (len(scores) - 1)
            std = math.sqrt(variance) if variance > 0 else 0.1
            z_score = (dp.quality_score - mean) / std if std > 0 else 0

            if z_score < -QUALITY_DROP_ZSCORE:
                self._create_alert(
                    agent_name, "quality_drop", "warning",
                    f"Quality dropped to {dp.quality_score:.1f} "
                    f"(baseline: {mean:.1f}, z={z_score:.1f})",
                    current=dp.quality_score, baseline=mean,
                )
                health = self._health[agent_name]
                if health.status == "active":
                    health.status = "warning"

    def _check_failures(self, agent_name: str, dp: QualityDataPoint):
        """Check for consecutive failures."""
        health = self._health[agent_name]

        if not dp.success:
            health.consecutive_failures += 1
            if health.consecutive_failures >= FAILURE_THRESHOLD:
                self._demote(agent_name,
                             f"{health.consecutive_failures} consecutive failures")
        else:
            health.consecutive_failures = 0

    def _check_response_time(self, agent_name: str, dp: QualityDataPoint):
        """Check for response time spikes."""
        if dp.response_time <= 0:
            return

        window = self._windows[agent_name]
        times = [p.response_time for p in window if p.response_time > 0]
        if len(times) < 5:
            return

        mean = sum(times) / len(times)
        variance = sum((x - mean) ** 2 for x in times) / (len(times) - 1)
        std = math.sqrt(variance) if variance > 0 else 1.0
        z_score = (dp.response_time - mean) / std if std > 0 else 0

        if z_score > RESPONSE_TIME_ZSCORE:
            self._create_alert(
                agent_name, "slow_response", "warning",
                f"Response time spike: {dp.response_time:.1f}s "
                f"(baseline: {mean:.1f}s, z={z_score:.1f})",
                current=dp.response_time, baseline=mean,
            )

    def _check_recovery(self, agent_name: str, dp: QualityDataPoint):
        """Check if a demoted agent has recovered."""
        health = self._health[agent_name]
        if health.status not in ("demoted", "warning"):
            return

        window = self._windows[agent_name]
        recent = list(window)[-3:]  # Last 3 data points

        if len(recent) >= 3:
            recent_quality = sum(p.quality_score for p in recent) / len(recent)
            recent_success = all(p.success for p in recent)

            if recent_quality >= RECOVERY_QUALITY_MIN and recent_success:
                health.status = "probation"
                health.probation_until = time.time() + PROBATION_SECONDS
                health.demoted_reason = ""
                self._create_alert(
                    agent_name, "recovery", "info",
                    f"{agent_name} recovering — on probation for {PROBATION_SECONDS}s "
                    f"(avg quality: {recent_quality:.1f})",
                    current=recent_quality, baseline=RECOVERY_QUALITY_MIN,
                )

    def _update_rolling_stats(self, agent_name: str):
        """Update rolling statistics for an agent."""
        window = self._windows[agent_name]
        health = self._health[agent_name]

        if not window:
            return

        scores = [p.quality_score for p in window]
        times = [p.response_time for p in window if p.response_time > 0]
        successes = [p.success for p in window]

        health.avg_quality = sum(scores) / len(scores)
        health.avg_response_time = sum(times) / len(times) if times else 0.0
        health.success_rate = sum(successes) / len(successes)

        # Trend: compare first half vs second half
        if len(scores) >= 6:
            mid = len(scores) // 2
            first_half = sum(scores[:mid]) / mid
            second_half = sum(scores[mid:]) / (len(scores) - mid)
            diff = second_half - first_half
            if diff > 0.5:
                health.quality_trend = "improving"
            elif diff < -0.5:
                health.quality_trend = "declining"
            else:
                health.quality_trend = "stable"

    def _demote(self, agent_name: str, reason: str):
        """Demote an agent."""
        health = self._health[agent_name]
        if health.status == "demoted":
            return  # Already demoted

        health.status = "demoted"
        health.demoted_at = time.time()
        health.demoted_reason = reason

        self._create_alert(
            agent_name, "quality_drop", "critical",
            f"DEMOTED: {agent_name} — {reason}",
            current=health.avg_quality, baseline=DEMOTE_QUALITY_FLOOR,
        )
        logger.warning(f"Agent {agent_name} DEMOTED: {reason}")
        self._save()

    def _create_alert(
        self, agent_name: str, alert_type: str, severity: str,
        message: str, current: float = 0.0, baseline: float = 0.0,
    ):
        """Create a watchdog alert."""
        alert = WatchdogAlert(
            alert_id=f"wdg-{uuid.uuid4().hex[:8]}",
            agent_name=agent_name,
            alert_type=alert_type,
            severity=severity,
            message=message,
            timestamp=datetime.now(timezone.utc).isoformat(),
            current_value=round(current, 2),
            baseline_value=round(baseline, 2),
        )
        self._alerts.append(alert)
        # Keep last 200 alerts
        if len(self._alerts) > 200:
            self._alerts = self._alerts[-200:]

    def _save(self):
        """Save state to disk."""
        try:
            state = {
                "health": {name: asdict(h) for name, h in self._health.items()},
                "alerts": [asdict(a) for a in self._alerts[-100:]],
            }
            with open(self.storage_dir / "watchdog.json", "w") as f:
                json.dump(state, f, indent=2)
        except Exception as e:
            logger.debug(f"Watchdog save skipped: {e}")

    def _load(self):
        """Load state from disk."""
        state_file = self.storage_dir / "watchdog.json"
        if state_file.exists():
            try:
                with open(state_file) as f:
                    state = json.load(f)
                for name, data in state.get("health", {}).items():
                    self._health[name] = AgentHealth(**data)
                for alert_data in state.get("alerts", []):
                    self._alerts.append(WatchdogAlert(**alert_data))
            except Exception as e:
                logger.warning(f"Error loading watchdog state: {e}")


def get_watchdog() -> QualityWatchdog:
    """Get singleton watchdog instance."""
    if not hasattr(get_watchdog, "_instance"):
        get_watchdog._instance = QualityWatchdog()
    return get_watchdog._instance
