"""
Drift Narrator Service - Phase 30
===================================
職責：將 DriftReport 轉為繁體中文人話，推送 Telegram

設計邊界：
- 只負責「敘述」，不做分析、不生成修復指令
- 觸發條件：high_count > 0 or medium_count > 2
- 模型：qwen2.5:7b-instruct (Ollama 111, 90s timeout)
- Redis 快取：drift_narrative:{report_id} TTL 1h，避免重複推送
- HPA replicas 自動調整在白名單，不觸發摘要

版本: v1.0
建立: 2026-04-10 (台北時區)
建立者: Claude Code (Phase 30 ADR-067)
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import structlog

from src.core.redis_client import get_redis
from src.services.model_registry import get_model
from src.services.openclaw import get_openclaw

if TYPE_CHECKING:
    from src.models.drift import DriftInterpretation, DriftReport

logger = structlog.get_logger(__name__)

# ============================================================
# 設定
# ============================================================
OLLAMA_URL = "http://192.168.0.111:11434"
# D1 集中化 2026-04-11: 從 models.json providers.ollama.models.drift_summary 讀取
NARRATOR_MODEL = get_model("ollama", "drift_summary")
NARRATOR_TIMEOUT = 90.0  # seconds
CACHE_TTL = 3600  # 1 小時
CACHE_PREFIX = "drift_narrative:"

# HPA 自動調整白名單 field_path，不納入敘述
_HPA_ALLOWLIST_PATHS = {
    "spec.replicas",
}

# 觸發條件
TRIGGER_HIGH_MIN = 1
TRIGGER_MEDIUM_MIN = 3


# ============================================================
# Prompt
# ============================================================
_NARRATIVE_PROMPT = """你是 AWOOOI SRE 維運助理，請將以下 K8s 配置漂移報告轉為繁體中文人話。

## 漂移摘要
{drift_summary}

## 意圖分析
{intent_summary}

## 要求
- 繁體中文，5 行以內
- 第 1 行：說明漂移了哪些資源（resource name）
- 第 2 行：說明嚴重程度和數量
- 第 3 行：最可能的原因（引用意圖分析）
- 第 4 行：建議的運維動作（rollback 或 adopt）
- 避免技術術語，用平實口語
- 只輸出摘要文字，不要標題或 markdown
"""


class DriftNarratorService:
    """
    Drift 報告人話摘要服務

    職責邊界：
    ✅ 呼叫 qwen2.5:7b-instruct 生成繁中摘要
    ✅ Redis 快取（避免重複推送）
    ✅ 推送 Telegram
    ❌ 不做漂移分析
    ❌ 不生成修復指令
    """

    async def narrate_and_notify(
        self,
        report: "DriftReport",
        interpretation: "DriftInterpretation | None" = None,
    ) -> None:
        """
        生成人話摘要並推送 Telegram

        只在 high_count > 0 or medium_count >= TRIGGER_MEDIUM_MIN 時執行
        """
        if not self._should_narrate(report):
            logger.debug(
                "drift_narrator_skip",
                report_id=report.report_id,
                high=report.high_count,
                medium=report.medium_count,
            )
            return

        # Redis 快取檢查（同 report_id 不重複推送）
        cache_key = f"{CACHE_PREFIX}{report.report_id}"
        redis = await get_redis()
        if await redis.exists(cache_key):
            logger.debug("drift_narrator_cache_hit", report_id=report.report_id)
            return

        narrative = await self._generate_narrative(report, interpretation)
        await self._send_telegram(report, narrative)

        # 寫入 DB narrative_text (Phase 30 ADR-067)
        try:
            from src.repositories.drift_repository import get_drift_repository
            await get_drift_repository().update_narrative(report.report_id, narrative)
        except Exception as e:
            logger.warning("drift_narrator_db_write_failed", error=str(e))

        # 寫入快取
        await redis.set(cache_key, narrative[:500], ex=CACHE_TTL)

        logger.info(
            "drift_narrator_sent",
            report_id=report.report_id,
            high=report.high_count,
            medium=report.medium_count,
        )

    def _should_narrate(self, report: "DriftReport") -> bool:
        """觸發條件：high >= 1 or medium >= 3"""
        # 過濾 HPA 白名單後重算
        non_hpa_items = [
            item for item in report.items
            if item.field_path not in _HPA_ALLOWLIST_PATHS
            and not item.is_allowlisted
        ]
        high = sum(1 for i in non_hpa_items if i.drift_level.value == "high")
        medium = sum(1 for i in non_hpa_items if i.drift_level.value == "medium")
        return high >= TRIGGER_HIGH_MIN or medium >= TRIGGER_MEDIUM_MIN

    async def _generate_narrative(
        self,
        report: "DriftReport",
        interpretation: "DriftInterpretation | None",
    ) -> str:
        """呼叫 Ollama qwen2.5:7b-instruct 生成摘要"""
        drift_summary = self._format_drift_summary(report)
        intent_summary = self._format_intent_summary(interpretation)

        prompt = _NARRATIVE_PROMPT.format(
            drift_summary=drift_summary,
            intent_summary=intent_summary,
        )

        # 2026-04-17 ogt + Claude Sonnet 4.6: 改用 OpenClaw AI Router 取代直接 Ollama httpx
        # 根因：直接呼叫 192.168.0.111:11434 繞過 AI Router，無 fallback → "All connection attempts failed"
        # 修復：統一走 openclaw.call()，自動享有 Provider 降級與 fallback 機制
        # 同 drift_interpreter.py 的修法（d952435）
        try:
            openclaw = get_openclaw()
            text, _provider, success = await openclaw.call(prompt)

            if success and text and text.strip():
                return text.strip()

            logger.warning("drift_narrator_openclaw_failed", provider=_provider)

        except Exception as e:
            logger.warning("drift_narrator_llm_error", error=str(e))

        # Fallback：結構化文字摘要
        return self._fallback_narrative(report, interpretation)

    def _format_drift_summary(self, report: "DriftReport") -> str:
        lines = []
        for item in report.items[:8]:
            if item.is_allowlisted or item.field_path in _HPA_ALLOWLIST_PATHS:
                continue
            lines.append(
                f"- [{item.drift_level.value}] {item.resource_kind}/{item.resource_name}: "
                f"{item.field_path} "
                f"(Git: {str(item.git_value)[:30]} → K8s: {str(item.actual_value)[:30]})"
            )
        return "\n".join(lines) if lines else "（均為白名單欄位）"

    def _format_intent_summary(self, interpretation: "DriftInterpretation | None") -> str:
        if not interpretation:
            return "無意圖分析"
        return (
            f"意圖: {interpretation.intent.value} | "
            f"說明: {interpretation.explanation} | "
            f"信心: {interpretation.confidence:.0%}"
        )

    def _fallback_narrative(
        self,
        report: "DriftReport",
        interpretation: "DriftInterpretation | None",
    ) -> str:
        """LLM 失敗時的結構化 fallback"""
        resources = list({
            f"{i.resource_kind}/{i.resource_name}"
            for i in report.items[:5]
            if not i.is_allowlisted
        })
        resource_str = "、".join(resources) if resources else "未知資源"
        intent_str = interpretation.explanation if interpretation else "意圖分析不可用"
        return (
            f"偵測到 {resource_str} 等資源發生配置漂移。\n"
            f"嚴重度：HIGH {report.high_count} 項、MEDIUM {report.medium_count} 項。\n"
            f"研判原因：{intent_str}\n"
            f"建議：確認是否需要 rollback 回 Git 狀態。"
        )

    async def _send_telegram(self, report: "DriftReport", narrative: str) -> None:
        """
        推送 TYPE-4D Config Drift 卡片（ADR-075）

        使用 send_drift_card() 取代舊 send_notification()，呈現結構化格式與操作按鈕。
        diff_summary = AI 研判（narrative） + 漂移詳情（前 8 筆）
        approval_id / incident_id 均使用 report_id（無需建立 ApprovalRequest）
        """
        from src.services.telegram_gateway import get_telegram_gateway

        diff_summary = (
            f"🤖 AI 研判\n{narrative}\n\n"
            f"漂移明細（HIGH: {report.high_count} | MEDIUM: {report.medium_count}）\n"
            f"{self._format_drift_summary(report)}"
        )

        try:
            tg = get_telegram_gateway()
            await tg.send_drift_card(
                incident_id=report.report_id,
                approval_id=report.report_id,
                resource_name=report.namespace,
                diff_summary=diff_summary[:500],
                detected_at="",
            )
        except Exception as e:
            logger.warning("drift_narrator_telegram_error", error=str(e))


# ============================================================
# Singleton
# ============================================================

_narrator: DriftNarratorService | None = None


def get_drift_narrator_service() -> DriftNarratorService:
    global _narrator
    if _narrator is None:
        _narrator = DriftNarratorService()
    return _narrator
