"""
AWOOOI AIOps Phase 2 — Solver Agent（軍師）
===========================================
職責：對每個根因假設產修復方案

輸入：DiagnosisReport（來自 Diagnostician）
輸出：ActionPlan（候選動作，含 blast_radius + rollback_cost + confidence）

設計原則：
1. 每個 Hypothesis 至少產 1 個 CandidateAction
2. blast_radius 評分影響 Reviewer 的審查嚴格度
3. blast_radius > 50 → Reviewer 必須 request_revision
4. 熔斷降級：LLM 失敗 → rule-based mock（基於 category 推 RESTART 為兜底動作）
5. Solver 不直接觸碰執行層（Coordinator 的工作）

ADR-082: Phase 2 多 Agent 協作
2026-04-15 ogt + Claude Sonnet 4.6（亞太）: Phase 2 初始建立
"""

from __future__ import annotations

import hashlib
import time
from typing import Any

import structlog

from src.agents.base import BaseAgent
from src.agents.protocol import (
    ActionPlan,
    AgentRole,
    AgentVote,
    CandidateAction,
    DiagnosisReport,
)
from src.services.sanitization_service import sanitize

logger = structlog.get_logger(__name__)


class SolverAgent(BaseAgent):
    """
    Solver Agent — 修復方案軍師

    Usage:
        agent = SolverAgent()
        plan = await agent.run(diagnosis_report)
    """

    AGENT_NAME = AgentRole.SOLVER.value
    AGENT_DESCRIPTION = "Remediation plan specialist. Produces candidate actions with blast radius scoring."

    async def run(
        self,
        diagnosis: DiagnosisReport,
        timeout_sec: float = 0.0,  # noqa: ARG002 — 已廢棄，保留簽名相容性
    ) -> ActionPlan:
        """
        根據診斷報告產出修復計畫。

        Args:
            diagnosis: Diagnostician 輸出
            timeout_sec: 已廢棄 (2026-04-16 ogt) — LLM 等完整回應，真實異常才降級

        Returns:
            ActionPlan（真實異常時 degraded=True）
        """
        start_ms = int(time.monotonic() * 1000)

        # 若 Diagnostician 已棄權，Solver 也應棄權（無論降級假設是否存在）
        # Gate 2: 原條件 `and not diagnosis.hypotheses` 誤放行降級的 confidence=0.2 假設
        if diagnosis.vote == AgentVote.ABSTAIN:
            return ActionPlan(
                candidates=[],
                diagnosis_report=diagnosis,
                latency_ms=0,
                vote=AgentVote.ABSTAIN,
                degraded=diagnosis.degraded,
            )

        try:
            plan = await self._solve(diagnosis)
            plan.latency_ms = int(time.monotonic() * 1000) - start_ms
            logger.info(
                "solver_done",
                candidates=len(plan.candidates),
                vote=plan.vote,
                latency_ms=plan.latency_ms,
            )
            return plan

        except Exception:
            latency = int(time.monotonic() * 1000) - start_ms
            logger.exception("solver_error")
            return self._degraded_plan(diagnosis, latency, "error")

    async def _solve(self, diagnosis: DiagnosisReport) -> ActionPlan:
        """核心 LLM 推理邏輯。"""
        top = diagnosis.top_hypothesis
        if not top:
            return ActionPlan(
                candidates=[],
                diagnosis_report=diagnosis,
                latency_ms=0,
                vote=AgentVote.ABSTAIN,
            )

        prompt = self._build_prompt({
            "hypothesis": top.description,
            "category": top.category,
            "confidence": top.confidence,
        })

        # 2026-04-16 ogt + Claude Sonnet 4.6: 傳遞 hypothesis 結構化資料給 OPENCLAW_NEMO
        # 根因：原本 call(prompt) 不傳 context → nemo fallback 把 prompt[:500]（系統說明）
        #       當 signal description → LLM 回傳「設計修復方案的軍師 Agent」垃圾
        # 修復：把 top hypothesis description 放進 alert_context.signals 讓 nemo 看到真實診斷
        _hypothesis_text = (top.description or "（待診斷）")[:800]
        alert_context = {
            "incident_id": diagnosis.evidence_snapshot_id or "UNKNOWN",
            "severity": "P3",
            "signals": [{"alert_name": "diagnosis_hypothesis", "description": _hypothesis_text}],
            "affected_services": [],
        }

        from src.services.openclaw import get_openclaw
        openclaw = get_openclaw()
        response_text, _provider, success = await openclaw.call(prompt, alert_context=alert_context)

        if not success or not response_text:
            return self._degraded_plan(diagnosis, 0, "llm_failed")

        parsed = self._parse_response(sanitize(response_text, "solver_output"))
        candidates = _extract_candidates(parsed)

        if not candidates:
            return self._degraded_plan(diagnosis, 0, "no_candidates")

        return ActionPlan(
            candidates=candidates,
            diagnosis_report=diagnosis,
            latency_ms=0,
            vote=AgentVote.APPROVE,
        )

    def _build_prompt(self, context: dict[str, Any]) -> str:
        # 2026-04-17 ogt + Claude Sonnet 4.6: 修復 Solver action 格式問題
        # 根因：舊 prompt action 範例為 "restart_service:awoooi-api"（自訂格式）
        #       LLM 模仿範例輸出自然語言描述，而非 kubectl 命令
        #       → auto_approve Condition 1c 拒絕（無 kubectl 關鍵字）
        #       → blast_radius_calculator 永遠不被調用（fill rate = 0%）
        # 修復：要求 action 必須是真實 kubectl 命令，並提供正確範例
        return f"""你是 AWOOOI SRE 系統的軍師 Agent，專職修復方案設計。

根因假設：{context.get("hypothesis", "")}
告警類別：{context.get("category", "")}
診斷信心：{context.get("confidence", 0.0):.0%}

你的工作：為此根因提出 1-3 個修復候選方案。
每個方案必須評估：
- blast_radius（0-100）：影響範圍（越高 = 風險越大）
- rollback_cost（0-100）：回滾難度（越高 = 越難還原）

blast_radius 參考：
- kubectl rollout restart deployment = 10
- kubectl scale deployment --replicas=N = 15
- kubectl rollout undo deployment = 25
- kubectl apply -f = 40
- kubectl delete deployment = 75
- kubectl delete pvc = 95

🔴 關鍵規則：action 欄位必須是真實的 kubectl 命令，不可用自然語言描述。
目標資源格式：deployment/<name>，命名空間統一用 awoooi-prod。

以 JSON 回覆：
{{
  "candidates": [
    {{
      "action": "kubectl rollout restart deployment/awoooi-api -n awoooi-prod",
      "blast_radius": 10,
      "rollback_cost": 5,
      "confidence": 0.8,
      "rationale": "重啟可清除 OOM 導致的記憶體碎片化"
    }}
  ]
}}"""

    def _parse_response(self, response: str) -> dict[str, Any]:
        return self._extract_json(response)

    def analyze(self, context: dict[str, Any]) -> Any:
        raise NotImplementedError("Use run() for Phase 2 agents")

    def _degraded_plan(
        self,
        diagnosis: DiagnosisReport,
        latency_ms: int,
        reason: str = "unknown",
    ) -> ActionPlan:
        """熔斷降級：rule-based mock（依 category 推 RESTART 兜底）"""
        category = diagnosis.top_hypothesis.category if diagnosis.top_hypothesis else "Unknown"
        fallback_action = _default_action_for_category(category)
        return ActionPlan(
            candidates=[
                CandidateAction(
                    action=fallback_action,
                    blast_radius=20,
                    rollback_cost=5,
                    confidence=0.2,
                    rationale=f"[降級] LLM 分析失敗（{reason}），使用類別 {category} 的預設兜底動作",
                )
            ],
            diagnosis_report=diagnosis,
            latency_ms=latency_ms,
            vote=AgentVote.DEGRADED,
            degraded=True,
        )


# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────

def _extract_candidates(parsed: dict[str, Any]) -> list[CandidateAction]:
    """從 LLM 解析結果提取候選方案（按信心降序）。

    支援兩種格式：
    1. 標準格式：{"candidates": [{action, blast_radius, rollback_cost, confidence, rationale}]}
    2. OpenClaw Nemo 格式：{"action_title": "...", "risk_level": "...", "confidence": 0.85}

    2026-04-16 ogt + Claude Sonnet 4.6: 與 diagnostician 同步，修復 openclaw_nemo 格式不相容
    """
    # OpenClaw Nemo 格式轉換
    # 2026-04-17 ogt + Claude Sonnet 4.6: Nemo path kubectl 驗證
    # 根因：Nemo 回傳 {"action_title": "重啟 Crash Looping Pod"} 自然語言
    #       直接用 action_title 作為 action → 無 kubectl → auto_approve 誤通過 → 死迴圈
    # 修復：action_title 不含 kubectl → return []（觸發 _degraded_plan 輸出真實 kubectl）
    if "action_title" in parsed and "candidates" not in parsed:
        action_title = str(parsed.get("action_title", ""))
        if "kubectl" not in action_title.lower():
            return []  # 交由 _degraded_plan 接手，輸出真實 kubectl 調查指令
        confidence = float(parsed.get("confidence", 0.5))
        risk_level = str(parsed.get("risk_level", "medium"))
        risk_to_blast = {"critical": 60, "high": 40, "medium": 25, "low": 10}
        blast = risk_to_blast.get(risk_level.lower(), 30)
        if action_title and confidence > 0:
            return [CandidateAction(
                action=action_title[:200],
                blast_radius=blast,
                rollback_cost=20,
                confidence=confidence,
                rationale=f"OpenClaw Nemo 建議: {action_title}",
            )]
        return []

    raw = parsed.get("candidates", [])
    candidates = []
    for item in raw:
        if not isinstance(item, dict):
            continue
        c = CandidateAction(
            action=str(item.get("action", ""))[:200],
            blast_radius=max(0, min(100, int(item.get("blast_radius", 50)))),
            rollback_cost=max(0, min(100, int(item.get("rollback_cost", 50)))),
            confidence=float(item.get("confidence", 0.0)),
            rationale=str(item.get("rationale", ""))[:500],
        )
        candidates.append(c)
    candidates.sort(key=lambda c: c.confidence, reverse=True)
    return candidates


def _default_action_for_category(category: str) -> str:
    """降級時的預設調查指令 — 必須是真實 kubectl 命令（調查優先，不執行破壞性操作）

    2026-04-17 ogt + Claude Sonnet 4.6: 改為真實 kubectl 指令
    舊：自然語言如 "restart_pod"、"check_disk_usage" → 無法被 auto_approve 執行
    新：kubectl 調查指令 → 可執行，且均為唯讀操作，無副作用
    """
    category_lower = category.lower()
    if "pod" in category_lower or "kube" in category_lower or "crash" in category_lower:
        return "kubectl get pods -n awoooi-prod -o wide"
    if "disk" in category_lower or "storage" in category_lower or "pvc" in category_lower:
        return "kubectl exec -n awoooi-prod deployment/postgresql -- df -h"
    if "cpu" in category_lower or "load" in category_lower:
        return "kubectl top pods -n awoooi-prod --sort-by=cpu"
    if "memory" in category_lower or "oom" in category_lower:
        return "kubectl top pods -n awoooi-prod --sort-by=memory"
    if "network" in category_lower or "connect" in category_lower:
        return "kubectl get services -n awoooi-prod"
    return "kubectl get pods -n awoooi-prod"


def compute_input_hash(diagnosis: DiagnosisReport) -> str:
    """計算 Solver 輸入的 fingerprint。"""
    key = diagnosis.evidence_snapshot_id + (
        diagnosis.top_hypothesis.description if diagnosis.top_hypothesis else ""
    )
    return hashlib.sha256(key.encode()).hexdigest()[:16]


# ─────────────────────────────────────────────────────────────────────────────
# Singleton
# ─────────────────────────────────────────────────────────────────────────────

_agent: SolverAgent | None = None


def get_solver_agent() -> SolverAgent:
    global _agent
    if _agent is None:
        _agent = SolverAgent()
    return _agent
