"""Diagnostic Agent — investigates failures, analyzes logs, generates root cause reports."""
from __future__ import annotations

import logging
from collections import Counter
from typing import Any, Dict, List

from mesh.codex.base_agent import CodexAgent
from mesh.codex.models import CodexTask, CodexTaskResult, FindingSeverity

logger = logging.getLogger(__name__)


class DiagnosticAgent(CodexAgent):
    """Analyzes node health history and job failure patterns to detect
    anomalies and generate root cause analysis reports.

    Detects:
    - CPU spikes and sustained saturation
    - Memory leak patterns
    - Disk pressure trends
    - Job failure correlations with node health
    - Cascading failure patterns
    - Resource exhaustion trends
    """

    def __init__(self, mesh_db, config=None):
        super().__init__("diagnostics", mesh_db, config)

    async def execute(self, task: CodexTask) -> CodexTaskResult:
        findings: List[Dict[str, Any]] = []
        recommendations: List[str] = []

        target_node = task.target or None

        # Get nodes to analyze
        nodes = self._get_nodes(target_node)
        if not nodes:
            return CodexTaskResult(
                task_id=task.task_id,
                success=True,
                findings=[],
                recommendations=["No nodes found for diagnostics"],
            )

        # Analyze each node
        for node in nodes:
            node_id = node.get("node_id", "unknown")
            node_findings = self._diagnose_node(node_id, node)
            findings.extend(node_findings)

        # Analyze job failures
        job_findings = self._analyze_job_failures(target_node)
        findings.extend(job_findings)

        # Cross-node analysis for cascading failures
        if len(nodes) > 1:
            cascade_findings = self._detect_cascading_failures(nodes)
            findings.extend(cascade_findings)

        # Generate root cause report
        recommendations = self._generate_rca(findings, nodes)

        return CodexTaskResult(
            task_id=task.task_id,
            success=True,
            findings=findings,
            recommendations=recommendations,
        )

    def _get_nodes(self, target_node: str | None) -> List[dict]:
        if not self.mesh_db:
            return []
        if target_node:
            node = self.mesh_db.get_node(target_node)
            return [node] if node else []
        return self.mesh_db.get_all_nodes()

    def _diagnose_node(self, node_id: str, node: dict) -> List[Dict[str, Any]]:
        """Run diagnostics on a single node."""
        findings = []
        if not self.mesh_db:
            return findings

        history = self.mesh_db.get_health_history(node_id, limit=100)
        if not history:
            findings.append({
                "severity": FindingSeverity.MEDIUM.value,
                "category": "missing_telemetry",
                "location": node_id,
                "message": "No health telemetry data available",
                "suggestion": "Check agent connectivity and heartbeat configuration",
            })
            return findings

        # Check node status
        status = node.get("status", "unknown")
        if status == "offline":
            findings.append({
                "severity": FindingSeverity.HIGH.value,
                "category": "node_offline",
                "location": node_id,
                "message": f"Node is offline (last status: {status})",
                "suggestion": "Check network connectivity, SSH access, and agent process",
            })

        # CPU spike detection (sudden jumps)
        cpu_vals = [h.get("cpu_pct", 0) for h in history if h.get("cpu_pct") is not None]
        if len(cpu_vals) >= 3:
            for i in range(1, len(cpu_vals)):
                delta = cpu_vals[i-1] - cpu_vals[i]  # history is DESC order
                if abs(delta) > 50:
                    findings.append({
                        "severity": FindingSeverity.MEDIUM.value,
                        "category": "cpu_spike",
                        "location": node_id,
                        "message": f"CPU spike detected: {delta:+.1f}% change between samples",
                        "suggestion": "Check for runaway processes or sudden load changes",
                    })
                    break  # Report only the most recent spike

        # Load average analysis
        load_vals = [h.get("load_1m", 0) for h in history if h.get("load_1m") is not None]
        hw = node.get("hardware") or {}
        cpu_info = hw.get("cpu", {}) if isinstance(hw, dict) else {}
        core_count = cpu_info.get("cores", 1) if isinstance(cpu_info, dict) else 1
        if load_vals and core_count > 0:
            avg_load = sum(load_vals) / len(load_vals)
            load_ratio = avg_load / core_count
            if load_ratio > 2.0:
                findings.append({
                    "severity": FindingSeverity.HIGH.value,
                    "category": "overloaded",
                    "location": node_id,
                    "message": f"System overloaded: load avg {avg_load:.1f} on {core_count} cores ({load_ratio:.1f}x)",
                    "suggestion": "Reduce concurrent jobs or add capacity",
                })

        # Memory trend analysis
        mem_vals = [h.get("memory_used_pct", 0) for h in history if h.get("memory_used_pct") is not None]
        if len(mem_vals) >= 10:
            # Linear trend detection
            n = len(mem_vals)
            # Reverse because history is DESC
            mem_asc = list(reversed(mem_vals))
            x_mean = (n - 1) / 2
            y_mean = sum(mem_asc) / n
            numerator = sum((i - x_mean) * (v - y_mean) for i, v in enumerate(mem_asc))
            denominator = sum((i - x_mean) ** 2 for i in range(n))
            if denominator > 0:
                slope = numerator / denominator
                if slope > 0.5:  # >0.5% per sample increase
                    findings.append({
                        "severity": FindingSeverity.HIGH.value,
                        "category": "memory_leak",
                        "location": node_id,
                        "message": f"Memory usage trending up at {slope:.2f}%/sample (leak suspected)",
                        "suggestion": "Profile memory usage; check for unclosed resources or growing caches",
                    })

        # Disk pressure trend
        disk_vals = [h.get("disk_used_pct", 0) for h in history if h.get("disk_used_pct") is not None]
        if len(disk_vals) >= 5:
            disk_asc = list(reversed(disk_vals))
            if disk_asc[-1] > disk_asc[0] + 5:
                rate = (disk_asc[-1] - disk_asc[0]) / len(disk_asc)
                findings.append({
                    "severity": FindingSeverity.MEDIUM.value,
                    "category": "disk_growth",
                    "location": node_id,
                    "message": f"Disk usage growing at {rate:.2f}%/sample ({disk_asc[0]:.1f}% -> {disk_asc[-1]:.1f}%)",
                    "suggestion": "Set up log rotation and checkpoint pruning",
                })

        return findings

    def _analyze_job_failures(self, target_node: str | None) -> List[Dict[str, Any]]:
        """Analyze job failure patterns."""
        findings = []
        if not self.mesh_db:
            return findings

        failed_jobs = self.mesh_db.get_jobs(status="failed", limit=50)
        if not failed_jobs:
            return findings

        # Filter by node if target specified
        if target_node:
            failed_jobs = [j for j in failed_jobs if j.get("assigned_node") == target_node]

        if not failed_jobs:
            return findings

        # Error pattern analysis
        error_counts: Counter = Counter()
        node_failures: Counter = Counter()
        for job in failed_jobs:
            error = job.get("error", "unknown")
            # Normalize error messages
            if "timeout" in error.lower():
                error_counts["timeout"] += 1
            elif "oom" in error.lower() or "memory" in error.lower():
                error_counts["out_of_memory"] += 1
            elif "connection" in error.lower() or "network" in error.lower():
                error_counts["network"] += 1
            else:
                error_counts["other"] += 1

            node = job.get("assigned_node", "unassigned")
            node_failures[node] += 1

        # Report common failure patterns
        total_failures = len(failed_jobs)
        for error_type, count in error_counts.most_common(5):
            if count >= 2:
                pct = (count / total_failures) * 100
                sev = FindingSeverity.HIGH.value if pct > 50 else FindingSeverity.MEDIUM.value
                findings.append({
                    "severity": sev,
                    "category": f"job_failure_{error_type}",
                    "location": target_node or "cluster",
                    "message": f"{count}/{total_failures} job failures ({pct:.0f}%) due to {error_type}",
                    "suggestion": self._failure_suggestion(error_type),
                })

        # Nodes with disproportionate failures
        for node, count in node_failures.most_common(3):
            if count >= 3:
                findings.append({
                    "severity": FindingSeverity.HIGH.value,
                    "category": "failure_hotspot",
                    "location": node,
                    "message": f"Node {node} has {count} recent failures — possible hardware issue",
                    "suggestion": "Investigate node health; consider draining jobs from this node",
                })

        return findings

    def _detect_cascading_failures(self, nodes: List[dict]) -> List[Dict[str, Any]]:
        """Detect patterns suggesting cascading failures across nodes."""
        findings = []
        offline_nodes = [n for n in nodes if n.get("status") in ("offline", "degraded")]

        if len(offline_nodes) > 1:
            offline_pct = (len(offline_nodes) / len(nodes)) * 100
            if offline_pct > 50:
                findings.append({
                    "severity": FindingSeverity.CRITICAL.value,
                    "category": "cascading_failure",
                    "location": "cluster",
                    "message": f"{len(offline_nodes)}/{len(nodes)} nodes offline/degraded ({offline_pct:.0f}%)",
                    "suggestion": "Possible cascading failure — check shared infrastructure (network, storage, controller)",
                })
            elif len(offline_nodes) >= 2:
                names = [n.get("hostname", n.get("node_id", "?")) for n in offline_nodes]
                findings.append({
                    "severity": FindingSeverity.HIGH.value,
                    "category": "multi_node_failure",
                    "location": "cluster",
                    "message": f"Multiple nodes offline: {', '.join(names)}",
                    "suggestion": "Check for common failure cause — shared switch, power, or storage",
                })

        return findings

    def _failure_suggestion(self, error_type: str) -> str:
        suggestions = {
            "timeout": "Increase job timeouts or investigate slow network/storage",
            "out_of_memory": "Reduce batch sizes, enable memory limits, or add swap",
            "network": "Check network connectivity, firewall rules, and DNS resolution",
            "other": "Review job logs for specific error details",
        }
        return suggestions.get(error_type, "Investigate error logs for more details")

    def _generate_rca(self, findings: List[Dict[str, Any]],
                      nodes: List[dict]) -> List[str]:
        """Generate root cause analysis recommendations."""
        recommendations = []

        critical = [f for f in findings if f["severity"] == "critical"]
        high = [f for f in findings if f["severity"] == "high"]

        if critical:
            recommendations.append(
                f"ROOT CAUSE ANALYSIS: {len(critical)} critical issues detected"
            )
            for f in critical:
                recommendations.append(f"  - [{f['category']}] {f['message']}")

        if high:
            recommendations.append(f"{len(high)} high-severity issues require attention")

        # Cross-reference patterns
        categories = Counter(f["category"] for f in findings)
        if categories.get("memory_leak", 0) > 0 and categories.get("job_failure_out_of_memory", 0) > 0:
            recommendations.append(
                "CORRELATION: Memory leaks detected alongside OOM job failures — "
                "memory leak is likely root cause of OOM failures"
            )
        if categories.get("overloaded", 0) > 0 and categories.get("job_failure_timeout", 0) > 0:
            recommendations.append(
                "CORRELATION: System overload detected alongside timeout failures — "
                "reduce concurrent job count or add capacity"
            )
        if categories.get("disk_growth", 0) > 0 and categories.get("disk_pressure", 0) > 0:
            recommendations.append(
                "CORRELATION: Disk growth trend will lead to disk exhaustion — "
                "implement cleanup automation"
            )

        if not findings:
            recommendations.append(
                f"Diagnostics clean: {len(nodes)} nodes healthy, no failure patterns detected"
            )

        return recommendations
