"""
ProposalEngine - 決策提案引擎 (積木化版本)
==========================================

Phase 6.4e: 從 apps/api/src/services/proposal_service.py 搬遷

設計原則:
- 依賴注入: 透過建構子注入 IMemoryProvider 與 ILLMProvider
- 無外部耦合: 禁止直接引用 redis_client 或 db
- Guardrails 強制: 所有提案必須通過安全檢查

統帥鐵律 + 首席架構師鐵律:
- 禁止毀滅性指令 (rm -rf, DROP DATABASE, kubectl delete ns)
- K8s 操作必須綁定 Namespace
- 所有提案必須 require_dry_run: true
"""

from datetime import datetime, timezone
from typing import Any, Protocol, Callable
from uuid import uuid4
import re

from lewooogo_brain.interfaces.proposal_engine import (
    IProposalEngine,
    Proposal,
    Guardrails,
)
from lewooogo_brain.interfaces.incident_processor import (
    Incident,
    IncidentStatus,
)


# =============================================================================
# Provider Protocols (依賴注入用)
# =============================================================================

class IIncidentMemory(Protocol):
    """Incident 記憶體提供者協定"""

    async def load_incident(self, incident_id: str) -> Incident | None:
        """載入 Incident"""
        ...

    async def update_incident(
        self,
        incident_id: str,
        updates: dict[str, Any],
    ) -> bool:
        """更新 Incident"""
        ...


class ILLMProvider(Protocol):
    """LLM 提供者協定"""

    async def generate(
        self,
        prompt: str,
        context: str | None = None,
        max_tokens: int = 2048,
    ) -> str:
        """生成 LLM 回應"""
        ...


class ISkillLoader(Protocol):
    """Skill 載入器協定"""

    def load(self, skill_id: str) -> str | None:
        """載入 Skill 內容"""
        ...


# =============================================================================
# Constants - Guardrails 黑名單
# =============================================================================

FORBIDDEN_COMMANDS = [
    "rm -rf /",
    "rm -rf /*",
    "rm -rf .",
    "drop database",
    "drop table",
    "truncate",
    "delete from",
    "kubectl delete namespace",
    "kubectl delete ns",
    "kubectl delete -A",
    "> /dev/sda",
    "mkfs",
    ":(){:|:&};:",  # Fork bomb
    "--no-preserve-root",
    "dd if=/dev/zero",
]

ALLOWED_NAMESPACES = ["awoooi-prod", "awoooi-dev"]

SYSTEM_NAMESPACES = ["kube-system", "kube-public", "kube-node-lease", "default"]


# =============================================================================
# ProposalEngine Implementation
# =============================================================================

class ProposalEngine(IProposalEngine):
    """
    決策提案引擎

    職責:
    1. 分析 Incident 生成修復建議
    2. 評估風險等級
    3. 強制 Guardrails 檢查
    4. 更新 Incident 狀態

    使用方式:
        memory = IncidentMemoryAdapter(redis_client, db_session)
        llm = OllamaProvider(base_url="http://192.168.0.188:11434")
        skill_loader = SkillLoader(skills_dir=".agents/skills")

        engine = ProposalEngine(memory, llm, skill_loader)
        proposal, message = await engine.generate(incident_id)
    """

    def __init__(
        self,
        memory: IIncidentMemory,
        llm: ILLMProvider | None = None,
        skill_loader: ISkillLoader | None = None,
        logger: Any | None = None,
    ):
        """
        初始化 ProposalEngine

        Args:
            memory: Incident 記憶體提供者
            llm: LLM 提供者 (用於生成提案)
            skill_loader: Skill 載入器 (可選)
            logger: 日誌記錄器 (可選)
        """
        self._memory = memory
        self._llm = llm
        self._skill_loader = skill_loader
        self._logger = logger

    def _log(self, event: str, **kwargs) -> None:
        """記錄日誌"""
        if self._logger:
            self._logger.info(event, **kwargs)

    def get_default_guardrails(self) -> Guardrails:
        """取得預設安全護欄配置"""
        return Guardrails(
            require_dry_run=True,
            allowed_namespace=ALLOWED_NAMESPACES.copy(),
            forbidden_commands=FORBIDDEN_COMMANDS.copy(),
            max_retries=1,
            timeout_sec=60,
            audit_log="mandatory",
            rollback_window_sec=300,
        )

    async def generate(
        self,
        incident_id: str,
    ) -> tuple[Proposal | None, str]:
        """
        生成決策提案

        Args:
            incident_id: 事件 ID

        Returns:
            (Proposal, message) 或 (None, error_message)
        """
        return await self._generate_proposal(incident_id, skill_id=None)

    async def generate_with_skill(
        self,
        incident_id: str,
        skill_id: str,
    ) -> tuple[Proposal | None, str]:
        """
        使用指定 Skill 生成決策提案

        Args:
            incident_id: 事件 ID
            skill_id: Skill 識別碼 (e.g., "04-awoooi-devops-commander")

        Returns:
            (Proposal, message) 或 (None, error_message)
        """
        return await self._generate_proposal(incident_id, skill_id=skill_id)

    async def _generate_proposal(
        self,
        incident_id: str,
        skill_id: str | None,
    ) -> tuple[Proposal | None, str]:
        """內部提案生成邏輯"""
        try:
            # Step 1: 載入 Incident
            incident = await self._memory.load_incident(incident_id)
            if not incident:
                return None, f"Incident {incident_id} not found"

            # Step 2: 載入 Skill (如果指定)
            skill_context = None
            if skill_id and self._skill_loader:
                skill_context = self._skill_loader.load(skill_id)
                if not skill_context:
                    self._log("skill_not_found", skill_id=skill_id)

            # Step 3: 構建提案
            if self._llm:
                proposal = await self._generate_with_llm(incident, skill_context)
            else:
                proposal = self._generate_fallback(incident)

            # Step 4: Guardrails 檢查
            is_safe, violation = self._validate_guardrails(proposal)
            if not is_safe:
                self._log(
                    "guardrails_violation",
                    incident_id=incident_id,
                    violation=violation,
                )
                return None, f"Guardrails violation: {violation}"

            # Step 5: 更新 Incident
            await self._memory.update_incident(
                incident_id,
                {
                    "status": IncidentStatus.MITIGATING.value,
                    "proposal_ids": incident.proposal_ids + [proposal.proposal_id],
                    "updated_at": datetime.now(timezone.utc).isoformat(),
                },
            )

            self._log(
                "proposal_generated",
                incident_id=incident_id,
                proposal_id=proposal.proposal_id,
                risk_level=proposal.risk_level,
            )

            return proposal, "Proposal generated successfully"

        except Exception as e:
            self._log("proposal_generation_error", error=str(e))
            return None, f"Error generating proposal: {str(e)}"

    async def _generate_with_llm(
        self,
        incident: Incident,
        skill_context: str | None,
    ) -> Proposal:
        """使用 LLM 生成提案"""
        # 構建 prompt
        prompt = self._build_prompt(incident, skill_context)

        # 調用 LLM
        response = await self._llm.generate(prompt, context=skill_context)

        # 解析 LLM 回應 (簡化版，實際應使用結構化輸出)
        action = self._extract_action(response)
        description = self._extract_description(response)
        risk_level = self._assess_risk(incident, action)

        return Proposal(
            proposal_id=str(uuid4()),
            incident_id=incident.incident_id,
            action=action,
            description=description,
            risk_level=risk_level,
            guardrails=self.get_default_guardrails().model_dump(),
            metadata={
                "generated_by": "llm",
                "skill_used": skill_context is not None,
                "signal_count": len(incident.signals),
            },
        )

    def _generate_fallback(self, incident: Incident) -> Proposal:
        """備援提案生成 (無 LLM 時使用)"""
        # 根據嚴重度和服務決定動作
        if incident.severity.value in ["P0", "P1"]:
            action = "kubectl rollout restart deployment/<service> -n awoooi-prod"
            description = "重啟受影響的 Deployment 以快速恢復服務"
        else:
            action = "kubectl describe pod -l app=<service> -n awoooi-prod"
            description = "檢查 Pod 狀態以診斷問題根因"

        # 替換服務名稱
        if incident.affected_services:
            service = incident.affected_services[0]
            action = action.replace("<service>", service)

        return Proposal(
            proposal_id=str(uuid4()),
            incident_id=incident.incident_id,
            action=action,
            description=description,
            risk_level="low" if incident.severity.value in ["P2", "P3"] else "medium",
            guardrails=self.get_default_guardrails().model_dump(),
            metadata={
                "generated_by": "fallback",
                "skill_used": False,
                "signal_count": len(incident.signals),
            },
        )

    def _build_prompt(self, incident: Incident, skill_context: str | None) -> str:
        """構建 LLM Prompt"""
        signals_summary = "\n".join([
            f"- [{s.severity.value}] {s.alert_name}: {s.source}"
            for s in incident.signals[:5]  # 限制 5 個
        ])

        base_prompt = f"""你是 AWOOOI 智能運維系統的決策引擎。

## 事件資訊
- Incident ID: {incident.incident_id}
- 嚴重度: {incident.severity.value}
- 狀態: {incident.status.value}
- 受影響服務: {', '.join(incident.affected_services) or 'N/A'}

## 告警摘要
{signals_summary}

## 任務
請根據以上資訊，生成一個修復提案:
1. 建議的動作 (kubectl 指令或腳本)
2. 風險評估 (low/medium/high)
3. 預估影響時間

## 安全約束
- 所有 kubectl 指令必須包含 -n awoooi-prod
- 禁止使用 rm -rf、DROP DATABASE 等毀滅性指令
- 必須支援 dry-run 預覽
"""

        if skill_context:
            base_prompt = f"{skill_context}\n\n---\n\n{base_prompt}"

        return base_prompt

    def _extract_action(self, llm_response: str) -> str:
        """從 LLM 回應提取動作"""
        # 簡化版：尋找 kubectl 或 shell 指令
        lines = llm_response.split("\n")
        for line in lines:
            line = line.strip()
            if line.startswith("kubectl") or line.startswith("bash"):
                return line
            if "kubectl" in line and "-n" in line:
                # 提取 kubectl 指令
                match = re.search(r'(kubectl\s+[^\n]+)', line)
                if match:
                    return match.group(1)

        return "kubectl get pods -n awoooi-prod"  # 預設安全指令

    def _extract_description(self, llm_response: str) -> str:
        """從 LLM 回應提取描述"""
        # 取前 200 字符作為描述
        clean = llm_response.replace("\n", " ").strip()
        return clean[:200] if len(clean) > 200 else clean

    def _assess_risk(self, incident: Incident, action: str) -> str:
        """評估風險等級"""
        high_risk_keywords = ["delete", "scale 0", "drain", "cordon"]
        medium_risk_keywords = ["restart", "rollout", "patch", "apply"]

        action_lower = action.lower()

        for keyword in high_risk_keywords:
            if keyword in action_lower:
                return "high"

        for keyword in medium_risk_keywords:
            if keyword in action_lower:
                return "medium"

        return "low"

    # =========================================================================
    # Guardrails Validation (首席架構師鐵律)
    # =========================================================================

    def _validate_guardrails(self, proposal: Proposal) -> tuple[bool, str | None]:
        """
        驗證提案是否符合安全護欄

        Returns:
            (is_safe, violation_message)
        """
        action = proposal.action.lower()

        # 1. 檢查毀滅性指令
        for forbidden in FORBIDDEN_COMMANDS:
            if forbidden.lower() in action:
                return False, f"Forbidden command detected: {forbidden}"

        # 2. 檢查 K8s Namespace 綁定
        if "kubectl" in action:
            if not self._has_namespace(action):
                return False, "kubectl command missing -n namespace flag"

            # 檢查是否使用允許的 namespace
            ns = self._extract_namespace(action)
            if ns and ns in SYSTEM_NAMESPACES:
                return False, f"Forbidden namespace: {ns} (system namespace)"

            if ns and ns not in ALLOWED_NAMESPACES:
                return False, f"Namespace {ns} not in allowed list: {ALLOWED_NAMESPACES}"

        # 3. 確保 guardrails 配置正確
        guardrails = proposal.guardrails
        if not guardrails.get("require_dry_run", False):
            return False, "require_dry_run must be true"

        if not guardrails.get("allowed_namespace"):
            return False, "allowed_namespace must be specified"

        return True, None

    def _has_namespace(self, action: str) -> bool:
        """檢查 kubectl 指令是否包含 namespace"""
        return "-n " in action or "--namespace=" in action or "--namespace " in action

    def _extract_namespace(self, action: str) -> str | None:
        """從 kubectl 指令提取 namespace"""
        # 匹配 -n <namespace> 或 --namespace=<namespace>
        patterns = [
            r'-n\s+([a-zA-Z0-9_-]+)',
            r'--namespace[=\s]+([a-zA-Z0-9_-]+)',
        ]

        for pattern in patterns:
            match = re.search(pattern, action)
            if match:
                return match.group(1)

        return None


# =============================================================================
# Guardrails Validator (獨立使用)
# =============================================================================

class GuardrailsValidator:
    """
    獨立的 Guardrails 驗證器

    可在 ProposalEngine 外部使用，例如:
    - API 層再次驗證
    - 執行前最終檢查
    """

    @staticmethod
    def validate_script(script: str) -> tuple[bool, str | None]:
        """
        驗證腳本是否安全

        Args:
            script: 要驗證的腳本內容

        Returns:
            (is_safe, violation_message)
        """
        script_lower = script.lower()

        for forbidden in FORBIDDEN_COMMANDS:
            if forbidden.lower() in script_lower:
                return False, f"Forbidden command: {forbidden}"

        return True, None

    @staticmethod
    def validate_namespace(namespace: str) -> tuple[bool, str | None]:
        """
        驗證 Namespace 是否允許

        Args:
            namespace: K8s namespace

        Returns:
            (is_allowed, error_message)
        """
        if namespace in SYSTEM_NAMESPACES:
            return False, f"System namespace forbidden: {namespace}"

        if namespace not in ALLOWED_NAMESPACES:
            return False, f"Namespace not allowed: {namespace}"

        return True, None

    @staticmethod
    def enforce_dry_run(proposal: dict) -> dict:
        """
        強制設定 dry-run 標記

        Args:
            proposal: 提案字典

        Returns:
            修改後的提案
        """
        if "guardrails" not in proposal:
            proposal["guardrails"] = {}

        proposal["guardrails"]["require_dry_run"] = True
        proposal["guardrails"]["allowed_namespace"] = ALLOWED_NAMESPACES.copy()

        return proposal
