"""
Hermes Rule Quality Advisor — ADR-090 § E3 AI 規則品質建議
==========================================================
每日 04:00 Taipei 分析 alert_rule_catalog,對 noise_rate > 0.7 的 rule 推 Telegram
建議 + 寫 aol(rule_rejected) 稽核,人工決策是否 deprecate.

職責邊界:
  ✅ 讀 alert_rule_catalog WHERE noise_rate >= 0.7
  ✅ 為每條寫 aol(rule_rejected) + proposed_action='review_or_deprecate'
  ✅ 推 Telegram 通知 SRE group (格式化清單)
  ⏳ 不自動改 review_status (統帥鐵律: AI 不做最終決策)
  ⏳ TODO: LLM 分析每條 rule 的假報真因 (下一階段)

統帥鐵律對齊:
  - 禁止寫死規則做最終決策 → 本 agent 只推建議,人工決策
  - 朝 AI 自主化方向 → aol 留 trail,未來可升級為 LLM 判斷
  - noise_rate threshold 0.7 是「觸發討論」而非「自動動作」

排程:
  - 首次延遲 420s
  - 每日 04:00 Taipei

2026-04-19 ogt + Claude Opus 4.7 (1M context) Asia/Taipei
ADR-090 § E3 Hermes
"""
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 structlog

logger = structlog.get_logger(__name__)

_FIRST_DELAY_SEC = 420
_LOOP_BACKOFF_SEC = 1800
_DAILY_TRIGGER_HOUR_TAIPEI = 4

# 觸發討論的噪音閾值
_NOISE_THRESHOLD = 0.7
# 樣本不足不發建議 (避免只 fire 1 次就標為噪音)
_MIN_SAMPLE_SIZE = 5


async def run_hermes_rule_quality_loop() -> None:
    """每日 04:00 分析 rule 品質."""
    logger.info("hermes_rule_quality_loop_started")
    await asyncio.sleep(_FIRST_DELAY_SEC)

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

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


async def analyze_once() -> dict[str, int]:
    """一次分析: 找噪音 rule + LLM 分析真因 + 推建議 + aol 留痕.

    2026-04-19 P0 修: 加 daily leader_lock 避免多 Pod 重複推.
    """
    from src.services.ai_advisory_helpers import try_acquire_daily_lock

    if not await try_acquire_daily_lock("hermes_rule_quality"):
        logger.info("hermes_analyze_skipped_not_leader")
        return {"skipped": "not_leader"}

    started_ms = _time.time()
    stats = {"noisy_rules": 0, "llm_analyzed": 0, "advisories_written": 0, "telegram_sent": 0}
    error_msg: str | None = None
    llm_analyses: dict[str, dict[str, Any]] = {}

    try:
        noisy = await _fetch_noisy_rules()
        stats["noisy_rules"] = len(noisy)

        # v2 升級: 對每條 noisy rule 跑 LLM 分析真因 + 具體建議
        for r in noisy:
            analysis = await _llm_analyze_noisy_rule(r)
            if analysis:
                llm_analyses[r["rule_name"]] = analysis
                stats["llm_analyzed"] += 1

        for r in noisy:
            ok = await _write_advisory_aol(r, llm_analyses.get(r["rule_name"]))
            if ok:
                stats["advisories_written"] += 1

        if noisy:
            sent = await _send_telegram_summary(noisy, llm_analyses)
            stats["telegram_sent"] = 1 if sent else 0

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

    duration_ms = int((_time.time() - started_ms) * 1000)
    logger.info(
        "hermes_rule_quality_once_done",
        noisy=stats["noisy_rules"],
        llm_analyzed=stats["llm_analyzed"],
        advisories=stats["advisories_written"],
        telegram_sent=stats["telegram_sent"],
        duration_ms=duration_ms,
    )
    return stats


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

_LLM_ANALYZE_PROMPT = """你是 AWOOOI SRE 告警規則品質分析專家。以下是一條 Prometheus alerting rule 過去 30 天的統計,請分析假報真因並提出具體改進建議。

## 告警規則
- rule_name: {rule_name}
- severity: {severity}
- expr: {expr}
- for: {duration_seconds}s
- labels: {labels}
- annotations: {annotations}

## 過去 30 天統計
- true_positive (確實解決的): {tp}
- false_positive (有破壞性動作但 EXPIRED 沒人理): {fp}
- noise_rate: {noise_rate}

## 輸出規格 (必須是合法 JSON,純 JSON 無前後文字)
{{
  "probable_root_causes": ["3-4 個候選真因,繁中"],
  "recommended_actions": [
    {{"action": "adjust_threshold|add_for_duration|refine_labels|deprecate|split_rule|keep_as_is", "detail": "具體怎麼做,繁中一句話"}}
  ],
  "confidence": 0.0-1.0,
  "should_deprecate": true/false
}}

## 分析思路
1. 看 expr 是否過於敏感 (閾值太低 / 沒有 for: window)
2. 看 annotations 是否暗示「這是真實需要處理的問題」但被 AI 判 NO_ACTION → 可能是 action 流程問題而非規則問題
3. 考慮 severity warning/critical 是否合理
"""


async def _llm_analyze_noisy_rule(rule: dict[str, Any]) -> dict[str, Any] | None:
    """用 OpenClaw (多 provider) 分析噪音真因. 失敗回 None 不阻塞.

    2026-04-19 P1.2 重構: 使用 llm_json_parser.parse_llm_json_response 共用 helper
    (原 30 行重複 3-path parse 邏輯已抽出到 services/llm_json_parser.py).
    """
    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_ANALYZE_PROMPT.format(
            rule_name=rule["rule_name"],
            severity=rule["severity"] or "-",
            expr=(rule.get("expr") or "")[:500],
            duration_seconds=rule.get("duration_seconds") or 0,
            labels=_j.dumps(rule.get("labels", {}), ensure_ascii=False)[:300],
            annotations=_j.dumps(rule.get("annotations", {}), ensure_ascii=False)[:300],
            tp=rule["tp"],
            fp=rule["fp"],
            noise_rate=f"{rule['noise_rate']:.1%}",
        )
        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="recommended_actions",
            logger_context=f"hermes:{rule['rule_name']}",
        )
        if parsed:
            parsed["_llm_provider"] = provider
        return parsed
    except Exception as e:
        logger.warning("hermes_llm_analyze_error", rule=rule["rule_name"], error=str(e))
        return None


# ============================================================================
# 資料查詢
# ============================================================================

async def _fetch_noisy_rules() -> list[dict[str, Any]]:
    """撈 noise_rate >= 0.7 且樣本 >= 5 的 rules，或 AOL writeback 標記 draft 的 rules.

    W2 PR-R2 2026-04-28 ogt + Claude Sonnet 4.6: 加 OR review_status = 'draft' 條件
    讓 AOL writeback 觸發的 draft 規則能被 Hermes 自動推 Telegram 建議
    （不再卡人工 SQL 才能觸發 advisory）
    """
    from sqlalchemy import text as _sql
    from src.db.base import get_db_context

    try:
        async with get_db_context() as db:
            result = await db.execute(
                _sql(f"""
                    SELECT
                        rule_id, rule_name, severity,
                        true_positive_count, false_positive_count, noise_rate,
                        last_fired_at, review_status
                    FROM alert_rule_catalog
                    WHERE (
                        (
                            noise_rate >= :thr
                            AND (true_positive_count + false_positive_count) >= :min_sample
                            AND (review_status IS NULL OR review_status = 'approved')
                        )
                        OR review_status = 'draft'
                    )
                    ORDER BY noise_rate DESC NULLS LAST,
                             (true_positive_count + false_positive_count) DESC
                """),
                {"thr": _NOISE_THRESHOLD, "min_sample": _MIN_SAMPLE_SIZE},
            )
            return [
                {
                    "rule_id": r.rule_id,
                    "rule_name": r.rule_name,
                    "severity": r.severity,
                    "tp": int(r.true_positive_count or 0),
                    "fp": int(r.false_positive_count or 0),
                    "noise_rate": float(r.noise_rate) if r.noise_rate else 0.0,
                    "last_fired_at": r.last_fired_at,
                    "review_status": r.review_status,
                }
                for r in result.fetchall()
            ]
    except Exception as e:
        logger.warning("fetch_noisy_rules_failed", error=str(e))
        return []


# ============================================================================
# 建議寫入 (aol only,不改 rule 本身)
# ============================================================================

async def _write_advisory_aol(rule: dict[str, Any], llm_analysis: dict[str, Any] | None = None) -> bool:
    """寫 aol(rule_rejected) — 紀錄 AI 建議人工審查 + LLM 分析結果."""
    try:
        from sqlalchemy import text as _sql
        from src.db.base import get_db_context

        input_payload = {
            "rule_name": rule["rule_name"],
            "severity": rule["severity"],
            "noise_rate": rule["noise_rate"],
            "true_positive_count": rule["tp"],
            "false_positive_count": rule["fp"],
        }
        output_payload: dict[str, Any] = {
            "proposed_action": "review_or_deprecate",
            "reason": (
                f"過去 30d noise_rate {rule['noise_rate']:.1%} "
                f"(tp={rule['tp']}, fp={rule['fp']}),"
                f"假報過多應考慮 deprecate 或改進 expr"
            ),
            "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 (
                        'rule_rejected',
                        'hermes_rule_quality',
                        '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": ["hermes", "rule_quality", "advisory"],
                },
            )
        return True
    except Exception as e:
        logger.warning("write_advisory_aol_failed", rule=rule["rule_name"], error=str(e))
        return False


# ============================================================================
# Telegram 推送
# ============================================================================

async def _send_telegram_summary(
    noisy: list[dict[str, Any]],
    llm_analyses: dict[str, dict[str, Any]] | None = None,
) -> bool:
    """推 Telegram 摘要訊息給 SRE group,含 LLM 分析結果 + 互動按鈕 (P0 修)."""
    try:
        import html
        from src.core.config import settings
        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:
            logger.info("hermes_telegram_skip_no_chat_id")
            return False

        # Snooze check: 以第一條 noisy rule_name 為 key
        primary_rule = noisy[0]["rule_name"] if noisy else "unknown"
        if await is_snoozed("rule_quality", primary_rule):
            logger.info("hermes_rule_snoozed", rule=primary_rule)
            return False

        llm_analyses = llm_analyses or {}
        lines = [
            "🔍 <b>Hermes 規則品質檢測 (AI 分析)</b>",
            f"檢測到 {len(noisy)} 條規則噪音率 ≥ {_NOISE_THRESHOLD:.0%},請統帥審查:",
            "",
        ]
        for r in noisy[:8]:  # LLM 分析含建議,單條訊息較長,只秀 8 條
            safe_name = html.escape(r["rule_name"])
            lines.append(
                f"🟡 <code>{safe_name}</code> — noise {r['noise_rate']:.1%} (tp={r['tp']} fp={r['fp']})"
            )
            ai = llm_analyses.get(r["rule_name"])
            if ai:
                deprecate = ai.get("should_deprecate")
                conf = ai.get("confidence", 0.0)
                lines.append(f"   AI 判定: should_deprecate={deprecate} confidence={conf:.0%}")
                actions = ai.get("recommended_actions", []) or []
                for act in actions[:2]:  # 最多秀前 2 個建議
                    safe_detail = html.escape(str(act.get("detail", ""))[:120])
                    lines.append(f"   ▸ <i>{html.escape(str(act.get('action', '')))}</i>: {safe_detail}")
            else:
                lines.append("   (LLM 分析不可用,僅依噪音率判斷)")
            lines.append("")
        if len(noisy) > 8:
            lines.append(f"…還有 {len(noisy) - 8} 條,見 automation_operation_log")
        lines.append("決策: 人工 UPDATE alert_rule_catalog SET review_status='deprecated' WHERE rule_name='...'")

        msg = "\n".join(lines)
        keyboard = build_ai_advisory_keyboard(
            advisory_type="rule_quality",
            advisory_id=primary_rule,
            include_view=False,
            include_produce_cmd=False,
        )

        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("hermes_telegram_send_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))
