"""
Capacity Forecaster Job — Phase 4 AI 容量預測 MVP
=================================================
每日 05:00 Taipei 用 Prometheus predict_linear 預測未來 7 天容量趨勢,
推 Telegram 建議 + 寫 aol(capacity_recommendation).

職責邊界 (MVP):
  ✅ 用 predict_linear (Prometheus 內建 linear regression) 預測:
     - 7d 後 disk avail < 0 (磁碟將滿)
     - 7d 後 mem available < 10% (記憶體緊繃)
     - 7d 後 cpu 使用率 > 85% (CPU 飽和)
  ✅ 對每個高風險 host 寫 aol(capacity_recommendation)
  ✅ 彙總推 Telegram SRE group
  ⏳ TODO: 真正 Holt-Winters (季節性) — Prometheus 不支援,需外接 Python statsmodels
  ⏳ TODO: 根據業務週期 (週一高峰/週末低谷) 調整預測

預測方法論:
  Prometheus predict_linear(metric[7d], 86400*N) 回傳「基於過去 7d,未來 N 秒後的預測值」
  簡單但有效 — 線性外推,適合穩定增長/下降趨勢

統帥鐵律對齊:
  - AI 預測 + 推建議,不自動 scale up (人工決策擴容)
  - 7d window 保證有足夠樣本 (Prometheus retention 15d 夠)
  - 閾值 (avail < 0, mem < 10%) 是「觸發討論」非「最終決策」

排程:
  - 首次延遲 540s (其他 scanner 都跑完後)
  - 每日 05:00 Taipei (capacity_scanner 02:00 → compliance 03:00 → Hermes 04:00 → 預測 05:00)

2026-04-19 ogt + Claude Opus 4.7 (1M context) Asia/Taipei
ADR-090 § Phase 4 AI 容量預測
"""
from __future__ import annotations

import asyncio
import json as _json
import time as _time
from datetime import datetime, timedelta, timezone
from typing import Any

import httpx
import structlog

from src.core.config import settings

logger = structlog.get_logger(__name__)

_FIRST_DELAY_SEC = 540
_LOOP_BACKOFF_SEC = 1800
_DAILY_TRIGGER_HOUR_TAIPEI = 5
_HTTP_TIMEOUT_SEC = 15

# 預測視窗 (7d) 與 horizon (未來 7d)
_WINDOW = "7d"
_HORIZON_SEC = 7 * 86400  # 7 天

# predict_linear 查詢定義
# 回傳高風險 host 的 instance label + 預測值
_FORECAST_QUERIES = {
    "disk_saturation_7d": (
        # 7d 後根目錄 avail 預測為 0 或負 = 磁碟會滿
        f'predict_linear(node_filesystem_avail_bytes{{fstype!~"tmpfs|overlay", mountpoint="/"}}[{_WINDOW}], {_HORIZON_SEC}) < 0',
        "disk 預測 7 天內會滿",
    ),
    "mem_saturation_7d": (
        # 7d 後記憶體可用 < 10%
        f'predict_linear(node_memory_MemAvailable_bytes[{_WINDOW}], {_HORIZON_SEC}) '
        f'/ node_memory_MemTotal_bytes < 0.1',
        "mem 預測 7 天內可用量 < 10%",
    ),
    "cpu_high_7d_trend": (
        # 過去 7d 平均 cpu 已 > 70% + 上升趨勢
        f'avg_over_time((100 - (avg by(instance)(rate(node_cpu_seconds_total{{mode="idle"}}[5m])) * 100))[{_WINDOW}:15m]) > 70',
        "過去 7d cpu 平均 > 70%",
    ),
}


async def run_capacity_forecaster_loop() -> None:
    """每日 05:00 Taipei 容量預測."""
    logger.info("capacity_forecaster_loop_started")
    await asyncio.sleep(_FIRST_DELAY_SEC)

    while True:
        try:
            await forecast_once()
        except Exception as e:
            logger.exception("capacity_forecaster_loop_error", error=str(e))
            await asyncio.sleep(_LOOP_BACKOFF_SEC)
            continue

        sleep_sec = _seconds_until_next_trigger()
        logger.info("capacity_forecaster_next_tick", sleep_sec=sleep_sec)
        await asyncio.sleep(sleep_sec)


async def forecast_once() -> dict[str, Any]:
    """跑一次預測,對每個高風險 host 留痕 + LLM 分析 + 推 Telegram.

    2026-04-19 P0 修 (統帥截圖反饋): 加 leader_lock 避免多 Pod 重複推.
    """
    from src.services.ai_advisory_helpers import try_acquire_daily_lock

    # Leader lock: 只 leader Pod 跑,其他 skip
    if not await try_acquire_daily_lock("capacity_forecaster"):
        logger.info("capacity_forecast_skipped_not_leader")
        return {"skipped": "not_leader"}

    started_ms = _time.time()
    stats: dict[str, Any] = {
        "queries_run": 0, "high_risk_hosts": 0, "recommendations": 0, "llm_analyzed": 0,
    }
    risks: dict[str, list[dict[str, Any]]] = {}
    llm_analyses: dict[str, dict[str, Any]] = {}
    error_msg: str | None = None

    try:
        for query_name, (promql, reason) in _FORECAST_QUERIES.items():
            hits = await _run_prom_query(promql)
            stats["queries_run"] += 1
            for host, value in hits.items():
                risks.setdefault(host, []).append({
                    "query": query_name,
                    "value": value,
                    "reason": reason,
                })

        stats["high_risk_hosts"] = len(risks)

        # v2 Gap 3 LLM 升級: 對每個高風險 host 跑 LLM 分析產具體建議
        # (原 _derive_actions 是硬編 keyword mapping, LLM 能看完整 context 產客製建議)
        for host, findings in risks.items():
            analysis = await _llm_analyze_risk(host, findings)
            if analysis:
                llm_analyses[host] = analysis
                stats["llm_analyzed"] += 1

        for host, findings in risks.items():
            ok = await _write_recommendation_aol(host, findings, llm_analyses.get(host))
            if ok:
                stats["recommendations"] += 1

        if risks:
            await _send_telegram_forecast(risks, llm_analyses)

    except Exception as e:
        error_msg = f"{type(e).__name__}: {e}"[:1000]
        logger.exception("capacity_forecast_once_failed", error=error_msg)

    duration_ms = int((_time.time() - started_ms) * 1000)
    logger.info(
        "capacity_forecast_once_done",
        queries=stats["queries_run"],
        hosts=stats["high_risk_hosts"],
        recommendations=stats["recommendations"],
        llm_analyzed=stats["llm_analyzed"],
        duration_ms=duration_ms,
    )
    return stats


# ============================================================================
# v2 Gap 3 LLM 分析 — 統帥鐵律「朝 AI 自主化方向」
# ============================================================================

_LLM_FORECAST_PROMPT = """你是 AWOOOI 容量規劃專家。以下 host 過去 7 天趨勢顯示高風險,請分析真因並給具體可執行建議。

## Host
{host}

## Prometheus 預測命中
{findings_json}

## 當前主機環境資訊
  - 主機架構: 110 (Harbor/Gitea/監控), 112 (Security), 120/121 (K3s), 125 (K3s backup), 188 (PG/Redis/MinIO)
  - 判斷請考慮: 該主機上跑什麼服務、常見瓶頸模式

## 輸出規格 (必須是合法 JSON,純 JSON 無前後文字)
{{
  "root_causes": ["3 個候選真因,繁中"],
  "priority_actions": [
    {{"priority": "high|medium|low", "action": "具體動作 (繁中)", "command_hint": "可執行指令 hint"}}
  ],
  "urgency_days": 0-30,
  "confidence": 0.0-1.0
}}

## 分析方向 (不要寫死 hardcoded reason)
  - disk_saturation: 查是哪類檔案增長 (container images / PG WAL / 日誌 / build cache)
  - mem: 查哪個 process 佔最多 (JVM / Redis / cache thrashing)
  - cpu: 看是 runtime 壓力還是 cron / batch job
"""


async def _llm_analyze_risk(host: str, findings: list[dict[str, Any]]) -> dict[str, Any] | None:
    """用 OpenClaw 分析高風險 host. 失敗回 None 不阻塞.

    2026-04-19 P1.2 重構: 改用 llm_json_parser.parse_llm_json_response 共用 helper.
    """
    try:
        import json as _j
        from src.services.llm_json_parser import parse_llm_json_response
        from src.services.openclaw import get_openclaw

        prompt = _LLM_FORECAST_PROMPT.format(
            host=host,
            findings_json=_j.dumps(findings, ensure_ascii=False, indent=2),
        )
        openclaw = get_openclaw()
        text, provider, success = await openclaw.call(prompt)
        if not success or not text:
            return None

        parsed = parse_llm_json_response(
            text,
            required_key="priority_actions",
            logger_context=f"forecaster:{host}",
        )
        if parsed:
            parsed["_llm_provider"] = provider
        return parsed
    except Exception as e:
        logger.warning("forecast_llm_error", host=host, error=str(e))
        return None


async def _run_prom_query(promql: str) -> dict[str, float]:
    """跑 Prometheus instant query, 回傳 {host: value}."""
    url = f"{settings.PROMETHEUS_URL.rstrip('/')}/api/v1/query"
    try:
        async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT_SEC, trust_env=False) as client:
            resp = await client.get(url, params={"query": promql})
            resp.raise_for_status()
            data = resp.json()
        if data.get("status") != "success":
            return {}
        result: dict[str, float] = {}
        for r in (data.get("data", {}) or {}).get("result", []) or []:
            instance = (r.get("metric", {}) or {}).get("instance", "")
            host = instance.split(":")[0] if instance else "unknown"
            val = r.get("value", [None, None])
            if val and len(val) >= 2:
                try:
                    result[host] = float(val[1])
                except (ValueError, TypeError):
                    pass
        return result
    except Exception as e:
        logger.warning("prom_forecast_query_failed", promql=promql[:80], error=str(e))
        return {}


async def _write_recommendation_aol(
    host: str,
    findings: list[dict[str, Any]],
    llm_analysis: dict[str, Any] | None = None,
) -> bool:
    """寫 aol(capacity_recommendation) + LLM 分析結果."""
    try:
        from sqlalchemy import text as _sql
        from src.db.base import get_db_context

        input_payload = {"host": host, "forecast_horizon_days": 7, "findings_count": len(findings)}
        output_payload: dict[str, Any] = {
            "host": host,
            "findings": findings,
            "proposed_actions": _derive_actions(findings),
            "requires_human_decision": True,
        }
        if llm_analysis:
            output_payload["llm_analysis"] = llm_analysis

        async with get_db_context() as db:
            await db.execute(
                _sql("""
                    INSERT INTO automation_operation_log (
                        operation_type, actor, status,
                        input, output, tags
                    ) VALUES (
                        'capacity_recommendation',
                        'capacity_forecaster',
                        'success',
                        CAST(:input AS jsonb),
                        CAST(:output AS jsonb),
                        :tags
                    )
                """),
                {
                    "input": _json.dumps(input_payload, ensure_ascii=False),
                    "output": _json.dumps(output_payload, ensure_ascii=False),
                    "tags": ["capacity", "forecast", "phase4", "predict_linear"],
                },
            )
        return True
    except Exception as e:
        logger.warning("capacity_forecast_aol_failed", host=host, error=str(e))
        return False


def _derive_actions(findings: list[dict[str, Any]]) -> list[str]:
    """根據 findings 產生建議動作清單."""
    actions: list[str] = []
    queries = {f["query"] for f in findings}
    if "disk_saturation_7d" in queries:
        actions.append("清理 /var/log, /var/lib/docker, PG WAL archive;或擴容磁碟")
    if "mem_saturation_7d" in queries:
        actions.append("檢查 top mem consumer;考慮加記憶體或調整 JVM/Redis maxmemory")
    if "cpu_high_7d_trend" in queries:
        actions.append("分析 top CPU process;考慮擴充 vCPU 或 scale out")
    if not actions:
        actions.append("人工審查各指標")
    return actions


async def _send_telegram_forecast(
    risks: dict[str, list[dict[str, Any]]],
    llm_analyses: dict[str, dict[str, Any]] | None = None,
) -> bool:
    """推 Telegram 預測摘要 (含 LLM 分析 + 互動按鈕).

    2026-04-19 P0 修 (統帥截圖反饋): 加 snooze check + inline_keyboard 4 按鈕
    (✅ 已處理 / 😴 忽略 24h / 🔍 查看詳情 / 📋 產 kubectl 指令).
    """
    try:
        import html
        from src.services.ai_advisory_helpers import build_ai_advisory_keyboard, is_snoozed
        from src.services.telegram_gateway import get_telegram_gateway

        target_chat_id = settings.SRE_GROUP_CHAT_ID
        if not target_chat_id:
            return False

        # Snooze check: 過濾掉被人工 snooze 的 host (按「忽略 24h」後)
        active_risks = {}
        skipped_hosts: list[str] = []
        for host, findings in risks.items():
            if await is_snoozed("capacity_forecast", host):
                skipped_hosts.append(host)
                continue
            active_risks[host] = findings

        if not active_risks:
            logger.info("capacity_forecast_all_snoozed", total=len(risks))
            return False

        llm_analyses = llm_analyses or {}
        lines = [
            "📈 <b>容量預測 (Phase 4 AI 升級版)</b>",
            f"未來 7 天高風險 host: {len(active_risks)} 台"
            + (f" (含 {len(skipped_hosts)} 台已忽略)" if skipped_hosts else ""),
            "",
        ]
        for host, findings in list(active_risks.items())[:8]:
            lines.append(f"🟡 <code>{html.escape(host)}</code>")
            for f in findings[:3]:
                lines.append(f"   ▸ {html.escape(f['reason'])} (value={f['value']:.2f})")

            ai = llm_analyses.get(host)
            if ai:
                urgency = ai.get("urgency_days", "?")
                conf = ai.get("confidence", 0.0)
                lines.append(f"   🤖 AI 判定: 緊急 {urgency}d, 信心 {conf:.0%}")
                for act in (ai.get("priority_actions") or [])[:2]:
                    pri = act.get("priority", "")
                    detail = html.escape(str(act.get("action", ""))[:100])
                    lines.append(f"   ▸ [{pri}] {detail}")
            else:
                actions = _derive_actions(findings)
                if actions:
                    lines.append(f"   建議: {html.escape(actions[0])[:100]}")
            lines.append("")

        # advisory_id 用第一個 host (snooze / aol 對應用)
        primary_host = next(iter(active_risks.keys()))
        keyboard = build_ai_advisory_keyboard(
            advisory_type="capacity_forecast",
            advisory_id=primary_host,
            include_view=True,
            include_produce_cmd=True,
        )

        msg = "\n".join(lines)

        tg = get_telegram_gateway()
        await tg._send_request("sendMessage", {  # type: ignore[attr-defined]
            "chat_id": target_chat_id,
            "text": msg,
            "parse_mode": "HTML",
            "disable_web_page_preview": True,
            "reply_markup": keyboard,
        })
        return True
    except Exception as e:
        logger.warning("capacity_forecast_telegram_failed", error=str(e))
        return False


def _seconds_until_next_trigger() -> float:
    tz_taipei = timezone(timedelta(hours=8))
    now = datetime.now(tz_taipei)
    today_trigger = now.replace(hour=_DAILY_TRIGGER_HOUR_TAIPEI, minute=0, second=0, microsecond=0)
    if now >= today_trigger:
        today_trigger = today_trigger + timedelta(days=1)
    delta = (today_trigger - now).total_seconds()
    return max(300.0, min(delta, 25 * 3600))
