"""
Telegram Gateway - OpenClaw 行動戰情室 + SignOz 整合
====================================================
Phase 5.4.3 & 5.4.4: Telegram 推送與簽核接收
統帥校正: SignOz 為唯一全能視力中心

Features:
- 推送待簽核卡片到 Telegram (含 SignOz 指標)
- 動態 SignOz Trace URL (告警前後 5 分鐘)
- 自動調優按鈕 (Shadow Mode: 僅日誌輸出)
- 接收統帥簽核回調
- SOUL.md 訊息壓縮原則 100% 遵守

SOUL.md 鐵律 (4.1 Telegram 訊息壓縮原則):
- 狀態標籤: 20 字元
- 資源名稱: 50 字元
- 根因摘要: 100 字元
- 建議行動: 50 字元
- 總長度: 800 字元 (v7.0 擴展以容納 SignOz 區塊)

修復紀錄:
- 2026-03-26 Claude Code: 修復 HTML 解析錯誤 (Can't parse entities)
"""

import asyncio
import html
from dataclasses import dataclass
from datetime import UTC, datetime

import httpx
import structlog
from opentelemetry import trace

from src.core.config import settings
from src.core.redis_client import get_redis
from src.services.security_interceptor import (
    NonceReplayError,
    UserNotWhitelistedError,
    get_security_interceptor,
)
from src.services.chat_manager import get_chat_manager

# =============================================================================
# Snooze/Silence Redis Keys (2026-03-27 P1 優化)
# =============================================================================
SNOOZE_KEY_PREFIX = "telegram_snooze:"  # {approval_id} -> 稍後提醒
SILENCE_KEY_PREFIX = "telegram_silence:"  # {resource_name} -> 靜默
SNOOZE_TTL_SECONDS = 30 * 60  # 30 分鐘
SILENCE_TTL_SECONDS = 60 * 60  # 1 小時

logger = structlog.get_logger(__name__)

# =============================================================================
# OTEL Tracer (Phase C P1 可觀測性)
# 2026-03-30 Claude Code: 新增 Telegram Gateway 追蹤
# =============================================================================
_tracer = trace.get_tracer("awoooi.telegram_gateway", "1.0.0")


# =============================================================================
# Long Polling 配置 (Phase 5 內網修復)
# =============================================================================
LONG_POLLING_TIMEOUT = 30  # getUpdates timeout (秒)
LONG_POLLING_RETRY_DELAY = 5  # 錯誤後重試延遲 (秒)


# =============================================================================
# SignOz Metrics Block (v7.0)
# =============================================================================

@dataclass
class SignOzMetricsBlock:
    """
    SignOz 指標區塊 (嵌入 Telegram 卡片)

    格式:
    📊 SignOz 指標
    ├ RPS: 150.2 📈
    ├ Error: 🟢 0.5%
    └ P99: 245ms ➡️
    """
    rps: float = 0.0
    rps_trend: str = "stable"  # up, down, stable
    error_rate: float = 0.0
    p99_latency_ms: float = 0.0
    latency_trend: str = "stable"
    trace_url: str = ""

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        trend_emoji = {"up": "📈", "down": "📉", "stable": "➡️"}
        error_emoji = "🟢" if self.error_rate < 1 else ("🟡" if self.error_rate < 5 else "🔴")

        return (
            f"📊 <b>SignOz 指標</b>\n"
            f"├ RPS: <code>{self.rps:.1f}</code> {trend_emoji.get(self.rps_trend, '➡️')}\n"
            f"├ Error: {error_emoji} <code>{self.error_rate:.2f}%</code>\n"
            f"└ P99: <code>{self.p99_latency_ms:.0f}ms</code> {trend_emoji.get(self.latency_trend, '➡️')}"
        )


# =============================================================================
# SOUL.md 訊息格式定義 (v7.0 + SignOz)
# =============================================================================

@dataclass
class TelegramMessage:
    """
    Telegram 訊息結構 (SOUL.md 4.1 + v7.0 SignOz 整合)

    格式:
    ═══════════════════════════
    🚨 CRITICAL | harbor-core
    ═══════════════════════════
    📋 INC-20260321-0001
    🎯 資源: harbor-core-7d4b8c9f5
    ━━━━━━━━━━━━━━━━━━━
    🤖 AI 仲裁判定
    👥 責任: BE (後端)
    📊 信心: 🟢 88%
    💡 原因: JVM Heap 配置不當
    ━━━━━━━━━━━━━━━━━━━
    📊 SignOz 指標
    ├ RPS: 150.2 📈
    ├ Error: 🟢 0.5%
    └ P99: 245ms ➡️
    ━━━━━━━━━━━━━━━━━━━
    🔧 建議: 刪除 Pod
    ⏱️ 停機: ~30s
    🔍 SignOz Trace (±5min)

    [✅ 簽核] [❌ 拒絕] [⚡ 自動調優]
    """
    status_emoji: str           # 🚨, ⚠️, ℹ️
    risk_level: str             # CRITICAL, MEDIUM, LOW
    resource_name: str          # Pod/Deployment 名稱 (max 50)
    root_cause: str             # 根因摘要 (max 100)
    suggested_action: str       # 建議操作 (max 50)
    estimated_downtime: str     # 預計停機時間
    approval_id: str            # 簽核單 ID
    # v6.0 AI 仲裁欄位
    incident_id: str = ""       # 事件編號 INC-YYYYMMDD-XXXX
    primary_responsibility: str = "COLLAB"  # FE/BE/INFRA/DB/COLLAB
    confidence: float = 0.0     # 信心度 0.0-1.0
    namespace: str = "default"  # K8s namespace
    # v7.0 SignOz 整合
    signoz_metrics: SignOzMetricsBlock | None = None
    signoz_trace_url: str = ""  # 動態時間參數 URL
    auto_tuning_command: str = ""  # kubectl 調優指令
    # 2026-03-29 ogt: AI Token/Cost 追蹤
    ai_tokens: int = 0          # LLM Token 使用量
    ai_cost: float = 0.0        # LLM 成本 (USD)
    # 2026-03-29 ogt: ADR-037 異常頻率統計
    anomaly_frequency: dict | None = None  # AnomalyCounter 統計
    # 2026-03-29 ogt: AI Provider 來源顯示
    ai_provider: str = ""  # ollama/gemini/claude/expert_system/mock

    # ==========================================================================
    # Phase 22: Nemotron 協作欄位 (ADR-044)
    # 2026-03-31 Claude Code: OpenClaw + Nemotron 雙軌顯示
    # ==========================================================================
    nemotron_enabled: bool = False  # 是否啟用 Nemotron 協作
    nemotron_tools: list[dict] | None = None  # Tool Calling 結果 [{"tool": str, "args": dict, "valid": bool}]
    nemotron_validation: str = ""  # "✅ 驗證通過" / "❌ 驗證失敗" / "⏳ 驗證中"
    nemotron_latency_ms: float = 0.0  # Nemotron 呼叫延遲 (ms)

    def format(self) -> str:
        """
        格式化為 SOUL.md 規範的訊息 (含 AI 仲裁 + SignOz)

        Returns:
            str: 格式化的 Telegram 訊息 (max 900 字元)
        """
        # 責任映射
        resp_map = {
            "FE": "👨‍💻 FE (前端)",
            "BE": "⚙️ BE (後端)",
            "INFRA": "🏗️ INFRA (基礎設施)",
            "DB": "🗄️ DB (資料庫)",
            "COLLAB": "🤝 COLLAB (協同處理)",
        }
        resp_display = resp_map.get(self.primary_responsibility, "❓ 未知")

        # 信心度顯示
        confidence_pct = int(self.confidence * 100)
        if confidence_pct >= 80:
            conf_emoji = "🟢"
        elif confidence_pct >= 70:
            conf_emoji = "🟡"
        else:
            conf_emoji = "🔴"

        # 自動生成事件編號 (2026-03-27 ogt: 修復 INC-INC- 重複前綴)
        if self.incident_id:
            incident_id = self.incident_id
        elif self.approval_id.startswith("INC-"):
            incident_id = self.approval_id
        else:
            incident_id = f"INC-{self.approval_id[:8].upper()}"

        # SignOz URL (優先使用動態 URL) - 必須 HTML 轉義防止解析錯誤
        service_name = self.resource_name.split("-")[0] if "-" in self.resource_name else self.resource_name
        raw_url = self.signoz_trace_url or f"http://192.168.0.188:3301/traces?service={service_name}"
        signoz_url = html.escape(raw_url, quote=True)

        # SignOz 指標區塊
        signoz_block = ""
        if self.signoz_metrics:
            signoz_block = f"━━━━━━━━━━━━━━━━━━━\n{self.signoz_metrics.format()}\n"

        # HTML 轉義用戶輸入內容，防止 "Can't parse entities" 錯誤
        safe_resource = html.escape(self.resource_name[:35])
        safe_root_cause = html.escape(self.root_cause[:50])
        safe_action = html.escape(self.suggested_action[:35])
        safe_downtime = html.escape(self.estimated_downtime)

        # 2026-03-29 ogt: AI Token/Cost 顯示
        ai_cost_display = ""
        if self.ai_tokens > 0 or self.ai_cost > 0:
            ai_cost_display = f"💰 Tokens: {self.ai_tokens:,} / ${self.ai_cost:.4f}\n"

        # 2026-03-29 ogt: ADR-037 異常頻率顯示
        frequency_block = ""
        if self.anomaly_frequency and self.anomaly_frequency.get("count_24h", 0) > 1:
            freq = self.anomaly_frequency
            escalation_emoji = {
                None: "",
                "REPEAT": "⚠️",
                "ESCALATE": "🔴",
                "PERMANENT_FIX": "🚨",
            }.get(freq.get("escalation_level"), "")

            frequency_block = (
                f"━━━━━━━━━━━━━━━━━━━\n"
                f"📊 <b>頻率統計</b> {escalation_emoji}\n"
                f"├ 1h: <code>{freq.get('count_1h', 0)}</code> 次\n"
                f"├ 24h: <code>{freq.get('count_24h', 0)}</code> 次\n"
                f"└ 修復: <code>{freq.get('auto_repair_count', 0)}</code> 次\n"
            )
            if freq.get("escalation_level"):
                frequency_block += f"🔺 升級: <b>{freq['escalation_level']}</b>\n"

        # 2026-03-29 ogt: 根據 confidence + ai_provider 動態顯示來源
        # confidence > 0 = 真實 AI 分析, confidence == 0 = 規則匹配/降級
        if self.confidence > 0 and self.ai_provider:
            # 顯示具體 AI 模型
            provider_names = {
                "ollama": "Ollama",
                "gemini": "Gemini",
                "claude": "Claude",
            }
            provider_display = provider_names.get(self.ai_provider.lower(), self.ai_provider.upper())
            source_label = f"🤖 <b>{provider_display} 仲裁</b>"
        elif self.confidence > 0:
            source_label = "🤖 <b>AI 仲裁判定</b>"
        else:
            source_label = "⚙️ <b>規則匹配</b>"

        # 組裝訊息
        message = (
            f"═══════════════════════════\n"
            f"{self.status_emoji} <b>{html.escape(self.risk_level)}</b> | {html.escape(self.resource_name[:25])}\n"
            f"═══════════════════════════\n"
            f"📋 <code>{html.escape(incident_id)}</code>\n"
            f"🎯 資源: <code>{safe_resource}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{source_label}\n"
            f"👥 責任: {resp_display}\n"
            f"📊 信心: {conf_emoji} {confidence_pct}%\n"
            f"{ai_cost_display}"
            f"💡 原因: {safe_root_cause}\n"
            f"{frequency_block}"
            f"{signoz_block}"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"🔧 建議: {safe_action}\n"
            f"⏱️ 停機: {safe_downtime}\n"
            f"🔍 <a href='{signoz_url}'>查看 SignOz Trace (±5min)</a>"
        )

        return message[:900]

    def format_with_nemotron(self) -> str:
        """
        格式化含 Nemotron 結果的訊息 (Phase 22 ADR-044)

        格式:
        ═══════════════════════════
        🚨 CRITICAL | harbor-core
        ═══════════════════════════
        📋 INC-20260331-0001
        🎯 資源: harbor-core-7d4b8c9f5
        ━━━━━━━━━━━━━━━━━━━
        🤖 OpenClaw 仲裁
        ├ 📊 信心: 🟢 85%
        ├ 👥 責任: BE (後端)
        └ 💡 原因: JVM Heap 配置不當
        ━━━━━━━━━━━━━━━━━━━
        🔧 Nemotron 執行方案
          ✅ restart_deployment: awoooi-api
          ✅ scale_deployment: replicas=3
        └ 驗證: ✅ 驗證通過
        ━━━━━━━━━━━━━━━━━━━
        🔧 建議: 刪除 Pod
        ⏱️ 停機: ~30s

        Returns:
            str: 格式化的 Telegram 訊息 (max 1000 字元)
        """
        # 責任映射
        resp_map = {
            "FE": "👨‍💻 FE (前端)",
            "BE": "⚙️ BE (後端)",
            "INFRA": "🏗️ INFRA (基礎設施)",
            "DB": "🗄️ DB (資料庫)",
            "COLLAB": "🤝 COLLAB (協同處理)",
        }
        resp_display = resp_map.get(self.primary_responsibility, "❓ 未知")

        # 信心度顯示
        confidence_pct = int(self.confidence * 100)
        if confidence_pct >= 80:
            conf_emoji = "🟢"
        elif confidence_pct >= 70:
            conf_emoji = "🟡"
        else:
            conf_emoji = "🔴"

        # 自動生成事件編號
        if self.incident_id:
            incident_id = self.incident_id
        elif self.approval_id.startswith("INC-"):
            incident_id = self.approval_id
        else:
            incident_id = f"INC-{self.approval_id[:8].upper()}"

        # HTML 轉義
        safe_resource = html.escape(self.resource_name[:35])
        safe_root_cause = html.escape(self.root_cause[:50])
        safe_action = html.escape(self.suggested_action[:35])
        safe_downtime = html.escape(self.estimated_downtime)

        # AI Provider 顯示
        if self.confidence > 0 and self.ai_provider:
            provider_names = {
                "ollama": "Ollama",
                "gemini": "Gemini",
                "claude": "Claude",
                "nvidia": "Nemotron",
            }
            provider_display = provider_names.get(self.ai_provider.lower(), self.ai_provider.upper())
            source_label = f"🤖 <b>{provider_display} 仲裁</b>"
        elif self.confidence > 0:
            source_label = "🤖 <b>OpenClaw 仲裁</b>"
        else:
            source_label = "⚙️ <b>規則匹配</b>"

        # Nemotron 區塊
        nemotron_block = ""
        if self.nemotron_enabled and self.nemotron_tools:
            tools_lines = []
            for t in self.nemotron_tools[:3]:  # 最多顯示 3 個
                valid_emoji = "✅" if t.get("valid", False) else "❌"
                tool_name = html.escape(str(t.get("tool", "unknown"))[:20])
                args_str = str(t.get("args", {}))[:25]
                safe_args = html.escape(args_str)
                tools_lines.append(f"  {valid_emoji} {tool_name}: {safe_args}")

            tools_str = "\n".join(tools_lines)
            validation_display = html.escape(self.nemotron_validation or "⏳ 驗證中")

            nemotron_block = (
                f"━━━━━━━━━━━━━━━━━━━\n"
                f"🔧 <b>Nemotron 執行方案</b>\n"
                f"{tools_str}\n"
                f"└ 驗證: {validation_display}\n"
            )
            if self.nemotron_latency_ms > 0:
                nemotron_block += f"└ 延遲: {self.nemotron_latency_ms:.0f}ms\n"

        # 組裝訊息
        message = (
            f"═══════════════════════════\n"
            f"{self.status_emoji} <b>{html.escape(self.risk_level)}</b> | {html.escape(self.resource_name[:25])}\n"
            f"═══════════════════════════\n"
            f"📋 <code>{html.escape(incident_id)}</code>\n"
            f"🎯 資源: <code>{safe_resource}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{source_label}\n"
            f"├ 📊 信心: {conf_emoji} {confidence_pct}%\n"
            f"├ 👥 責任: {resp_display}\n"
            f"└ 💡 原因: {safe_root_cause}\n"
            f"{nemotron_block}"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"🔧 建議: {safe_action}\n"
            f"⏱️ 停機: {safe_downtime}"
        )

        return message[:1000]


# =============================================================================
# 新訊息模板 (2026-03-29 ogt: ADR-038 Telegram 訊息規範)
# =============================================================================

@dataclass
class SentryErrorMessage:
    """
    Sentry 錯誤訊息 (SENTRY_ERROR)

    2026-03-29 ogt: 新增，用於 Sentry 錯誤通知
    按鈕: [🔍 查看詳情] [🔕 靜默 1h]
    """
    error_id: str               # Sentry Issue ID
    error_type: str             # TypeError, ValueError, etc.
    error_message: str          # 錯誤訊息 (max 100)
    service_name: str           # awoooi-api, awoooi-web, etc.
    file_location: str          # src/api/v1/incidents.py:123
    occurrence_count: int = 1   # 發生次數
    affected_users: int = 0     # 影響用戶數
    first_seen: str = ""        # 首次發生時間
    stack_trace: list[str] | None = None  # Stack trace (前 3 行)
    sentry_url: str = ""        # Sentry 連結

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        safe_error = html.escape(self.error_message[:80])
        safe_type = html.escape(self.error_type[:30])
        safe_service = html.escape(self.service_name[:25])
        safe_file = html.escape(self.file_location[:50])

        # Stack trace 區塊
        trace_block = ""
        if self.stack_trace:
            trace_lines = "\n".join(f"  → {html.escape(line[:50])}" for line in self.stack_trace[:3])
            trace_block = f"🔗 Stack Trace (前 3 行):\n{trace_lines}\n"

        # Sentry URL
        sentry_link = ""
        if self.sentry_url:
            safe_url = html.escape(self.sentry_url, quote=True)
            sentry_link = f"\n🔍 <a href='{safe_url}'>查看 Sentry</a>"

        message = (
            f"═══════════════════════════\n"
            f"🐛 <b>SENTRY ERROR</b> | {safe_service}\n"
            f"═══════════════════════════\n"
            f"📋 <code>{html.escape(self.error_id)}</code>\n"
            f"🎯 錯誤: <code>{safe_type}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"💬 {safe_error}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📊 <b>統計</b>\n"
            f"├ 發生次數: <code>{self.occurrence_count}</code>\n"
            f"├ 影響用戶: <code>{self.affected_users}</code>\n"
            f"└ 首次發生: {html.escape(self.first_seen) if self.first_seen else 'N/A'}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📍 位置: <code>{safe_file}</code>\n"
            f"{trace_block}"
            f"{sentry_link}"
        )

        return message[:900]


@dataclass
class ResourceWarnMessage:
    """
    資源告警訊息 (RESOURCE_WARN)

    2026-03-29 ogt: 新增，用於資源耗盡警告
    按鈕: [⚡ 自動擴展] [🔕 靜默 1h]
    """
    resource_id: str            # RES-YYYYMMDD-XXXX
    pod_name: str               # Pod 名稱
    namespace: str = "default"  # K8s namespace
    cpu_percent: float = 0.0    # CPU 使用率
    cpu_limit: str = ""         # CPU limit (e.g., 500m)
    memory_percent: float = 0.0 # Memory 使用率
    memory_limit: str = ""      # Memory limit (e.g., 512Mi)
    disk_percent: float = 0.0   # Disk 使用率
    trend_info: str = ""        # 趨勢資訊
    suggestion: str = ""        # 建議操作

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        safe_pod = html.escape(self.pod_name[:35])
        safe_ns = html.escape(self.namespace[:20])

        # 資源狀態 emoji
        def get_status_emoji(percent: float) -> str:
            if percent >= 90:
                return "🔴"
            elif percent >= 70:
                return "🟡"
            return "🟢"

        cpu_emoji = get_status_emoji(self.cpu_percent)
        mem_emoji = get_status_emoji(self.memory_percent)
        disk_emoji = get_status_emoji(self.disk_percent)

        # 趨勢和建議
        trend_block = ""
        if self.trend_info:
            trend_block = f"📈 趨勢: {html.escape(self.trend_info[:50])}\n"

        suggestion_block = ""
        if self.suggestion:
            suggestion_block = f"💡 建議: {html.escape(self.suggestion[:50])}\n"

        message = (
            f"═══════════════════════════\n"
            f"⚠️ <b>資源告警</b> | {safe_ns}\n"
            f"═══════════════════════════\n"
            f"📋 <code>{html.escape(self.resource_id)}</code>\n"
            f"🎯 Pod: <code>{safe_pod}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📊 <b>資源使用率</b>\n"
            f"├ CPU: {cpu_emoji} <code>{self.cpu_percent:.1f}%</code>"
            f"{f' (limit: {self.cpu_limit})' if self.cpu_limit else ''}\n"
            f"├ Memory: {mem_emoji} <code>{self.memory_percent:.1f}%</code>"
            f"{f' (limit: {self.memory_limit})' if self.memory_limit else ''}\n"
            f"└ Disk: {disk_emoji} <code>{self.disk_percent:.1f}%</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{trend_block}"
            f"{suggestion_block}"
        )

        return message[:900]


@dataclass
class RepairReportMessage:
    """
    自動修復報告訊息 (REPAIR_REPORT)

    2026-03-29 ogt: 新增，用於每日自動修復彙總
    按鈕: 無
    """
    report_date: str            # 報告日期 (YYYY-MM-DD)
    total_repairs: int = 0      # 總修復次數
    success_count: int = 0      # 成功次數
    failure_count: int = 0      # 失敗次數
    saved_minutes: int = 0      # 節省人工時間 (分鐘)
    top_issues: list[tuple[str, int]] | None = None  # Top 問題 [(name, count)]
    ai_cost_gemini: float = 0.0  # Gemini 成本
    ai_cost_nvidia: float = 0.0  # NVIDIA 成本 (免費)
    ai_tokens_total: int = 0     # 總 Token 數

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        # 成功率
        success_rate = (self.success_count / self.total_repairs * 100) if self.total_repairs > 0 else 0

        # Top 問題區塊
        issues_block = ""
        if self.top_issues:
            issues_lines = "\n".join(
                f"  {i+1}. {html.escape(name[:30])} ({count} 次)"
                for i, (name, count) in enumerate(self.top_issues[:3])
            )
            issues_block = f"━━━━━━━━━━━━━━━━━━━\n🔝 <b>Top 3 問題</b>:\n{issues_lines}\n"

        # AI 成本
        total_cost = self.ai_cost_gemini + self.ai_cost_nvidia

        message = (
            f"═══════════════════════════\n"
            f"🔧 <b>自動修復報告</b> | 每日彙總\n"
            f"═══════════════════════════\n"
            f"📅 {html.escape(self.report_date)}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📊 <b>統計</b>\n"
            f"├ 總修復次數: <code>{self.total_repairs}</code>\n"
            f"├ 成功: ✅ <code>{self.success_count}</code> ({success_rate:.0f}%)\n"
            f"├ 失敗: ❌ <code>{self.failure_count}</code>\n"
            f"└ 節省人工: ~<code>{self.saved_minutes}</code> 分鐘\n"
            f"{issues_block}"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"💰 <b>AI 成本</b>\n"
            f"├ Gemini: ${self.ai_cost_gemini:.4f} ({self.ai_tokens_total:,} tokens)\n"
            f"├ NVIDIA: ${self.ai_cost_nvidia:.4f} (免費)\n"
            f"└ 總計: ${total_cost:.4f}"
        )

        return message[:900]


@dataclass
class DailySummaryMessage:
    """
    每日摘要訊息 (DAILY_SUMMARY)

    2026-03-29 ogt: 新增，用於每日系統狀態摘要
    按鈕: 無
    """
    summary_date: str           # 摘要日期 (YYYY-MM-DD)
    # 告警統計
    alert_total: int = 0
    alert_critical: int = 0
    alert_medium: int = 0
    alert_low: int = 0
    # 處理統計
    auto_repair_count: int = 0
    manual_approval_count: int = 0
    ignored_count: int = 0
    avg_response_minutes: float = 0.0
    # 可用性
    api_availability: float = 99.9
    web_availability: float = 99.9
    worker_availability: float = 99.9
    # 成本
    ai_cost: float = 0.0
    cloud_cost: float = 0.0
    budget_remaining: float = 0.0

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        # 處理百分比
        total_handled = self.auto_repair_count + self.manual_approval_count + self.ignored_count
        auto_pct = (self.auto_repair_count / total_handled * 100) if total_handled > 0 else 0
        manual_pct = (self.manual_approval_count / total_handled * 100) if total_handled > 0 else 0
        ignored_pct = (self.ignored_count / total_handled * 100) if total_handled > 0 else 0

        message = (
            f"═══════════════════════════\n"
            f"📊 <b>每日摘要</b> | AWOOOI\n"
            f"═══════════════════════════\n"
            f"📅 {html.escape(self.summary_date)}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"🚨 <b>告警統計</b>\n"
            f"├ 總數: <code>{self.alert_total}</code>\n"
            f"├ Critical: <code>{self.alert_critical}</code>\n"
            f"├ Medium: <code>{self.alert_medium}</code>\n"
            f"└ Low: <code>{self.alert_low}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"✅ <b>處理統計</b>\n"
            f"├ 自動修復: <code>{self.auto_repair_count}</code> ({auto_pct:.0f}%)\n"
            f"├ 人工簽核: <code>{self.manual_approval_count}</code> ({manual_pct:.0f}%)\n"
            f"├ 忽略/靜默: <code>{self.ignored_count}</code> ({ignored_pct:.0f}%)\n"
            f"└ 平均回應: <code>{self.avg_response_minutes:.1f}</code> 分鐘\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📈 <b>可用性</b>\n"
            f"├ API: <code>{self.api_availability:.2f}%</code>\n"
            f"├ Web: <code>{self.web_availability:.2f}%</code>\n"
            f"└ Worker: <code>{self.worker_availability:.2f}%</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"💰 <b>成本</b>\n"
            f"├ AI: ${self.ai_cost:.2f}\n"
            f"├ 雲端: ${self.cloud_cost:.2f}\n"
            f"└ 預算剩餘: ${self.budget_remaining:.2f}"
        )

        return message[:900]


@dataclass
class CICDProgressMessage:
    """
    CI/CD 進度訊息 (CICD_PROGRESS)

    2026-03-30 ogt: 新增，用於 CI/CD 流程進度通知
    特性: 簡潔、不走 AI 仲裁、無按鈕
    """
    job_name: str               # Job 名稱 (e.g., Build, Test, Deploy)
    status: str                 # running, success, failed
    stage: str = ""             # CI/CD 階段 (e.g., build, test, deploy)
    commit_sha: str = ""        # Git commit SHA
    triggered_by: str = ""      # 觸發者
    duration_seconds: int = 0   # 執行時間
    message: str = ""           # 額外訊息
    workflow_url: str = ""      # Workflow 連結

    def format(self) -> str:
        """格式化為 Telegram HTML (簡潔版)"""
        # 狀態 emoji
        status_emoji = {
            "running": "🔄",
            "success": "✅",
            "failed": "❌",
            "pending": "⏳",
        }.get(self.status.lower(), "📦")

        safe_job = html.escape(self.job_name[:40])
        safe_stage = html.escape(self.stage[:20]) if self.stage else ""

        # 時間格式化
        duration_str = ""
        if self.duration_seconds > 0:
            minutes = self.duration_seconds // 60
            seconds = self.duration_seconds % 60
            duration_str = f" ({minutes}m {seconds}s)" if minutes > 0 else f" ({seconds}s)"

        # Commit 資訊
        commit_info = ""
        if self.commit_sha:
            commit_info = f"\n📋 <code>{html.escape(self.commit_sha[:8])}</code>"

        # Workflow 連結
        workflow_link = ""
        if self.workflow_url:
            safe_url = html.escape(self.workflow_url, quote=True)
            workflow_link = f"\n🔗 <a href='{safe_url}'>Workflow</a>"

        # 簡潔訊息
        stage_label = f" | {safe_stage}" if safe_stage else ""
        message = (
            f"{status_emoji} <b>[AWOOOI CI/CD]</b>{stage_label}\n"
            f"📦 {safe_job}{duration_str}"
            f"{commit_info}"
            f"{workflow_link}"
        )

        return message[:900]


@dataclass
class DeploySuccessMessage:
    """
    部署成功訊息 (DEPLOY_SUCCESS)

    2026-03-29 ogt: 新增，用於 CD 部署成功通知
    按鈕: 無
    """
    commit_sha: str             # Git commit SHA (short)
    triggered_by: str           # 觸發者
    environment: str = "Production"  # 環境
    # 版本資訊
    api_version: str = ""
    web_version: str = ""
    worker_version: str = ""
    # 部署時間
    duration_seconds: int = 0
    # 測試結果
    e2e_passed: int = 0
    e2e_total: int = 0
    health_check_passed: bool = True
    # 連結
    workflow_url: str = ""

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        safe_commit = html.escape(self.commit_sha[:8])
        safe_user = html.escape(self.triggered_by[:20])
        safe_env = html.escape(self.environment[:15])

        # 部署時間格式化
        minutes = self.duration_seconds // 60
        seconds = self.duration_seconds % 60
        duration_str = f"{minutes}m {seconds}s" if minutes > 0 else f"{seconds}s"

        # 測試結果
        e2e_status = "✅" if self.e2e_passed == self.e2e_total else "⚠️"
        health_status = "✅ 全部通過" if self.health_check_passed else "❌ 部分失敗"

        # Workflow 連結
        workflow_link = ""
        if self.workflow_url:
            safe_url = html.escape(self.workflow_url, quote=True)
            workflow_link = f"\n🔗 <a href='{safe_url}'>查看 Workflow</a>"

        message = (
            f"✅ <b>部署成功</b> | {safe_env}\n\n"
            f"📋 Commit: <code>{safe_commit}</code>\n"
            f"👤 觸發者: @{safe_user}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📊 <b>部署詳情</b>\n"
            f"├ API: {html.escape(self.api_version) if self.api_version else 'N/A'} ✅\n"
            f"├ Web: {html.escape(self.web_version) if self.web_version else 'N/A'} ✅\n"
            f"├ Worker: {html.escape(self.worker_version) if self.worker_version else 'N/A'} ✅\n"
            f"└ 耗時: {duration_str}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"🧪 E2E 測試: {e2e_status} {self.e2e_passed}/{self.e2e_total} PASSED\n"
            f"📊 健康檢查: {health_status}"
            f"{workflow_link}"
        )

        return message[:900]


@dataclass
class RateLimitMessage:
    """
    API 限額警告訊息 (RATE_LIMIT)

    2026-03-29 ogt: 新增，用於 AI API 限額警告
    按鈕: 無
    """
    provider: str               # gemini, openai, etc.
    # 用量統計
    daily_usage: int = 0
    daily_limit: int = 0
    token_usage: int = 0
    token_limit: int = 0
    cost_usd: float = 0.0
    # 建議
    suggestions: list[str] | None = None
    # 重置時間
    reset_time: str = ""

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        safe_provider = html.escape(self.provider.upper()[:15])

        # 使用率百分比
        usage_pct = (self.daily_usage / self.daily_limit * 100) if self.daily_limit > 0 else 0
        token_pct = (self.token_usage / self.token_limit * 100) if self.token_limit > 0 else 0

        # 建議區塊
        suggestion_block = ""
        if self.suggestions:
            suggestion_lines = "\n".join(f"  - {html.escape(s[:50])}" for s in self.suggestions[:3])
            suggestion_block = f"━━━━━━━━━━━━━━━━━━━\n💡 <b>建議</b>:\n{suggestion_lines}\n"

        # 重置時間
        reset_block = ""
        if self.reset_time:
            reset_block = f"\n🔄 將於 {html.escape(self.reset_time)} 重置"

        message = (
            f"⚠️ <b>API 限額警告</b>\n\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📊 <b>{safe_provider} API</b>\n"
            f"├ 今日用量: <code>{self.daily_usage}/{self.daily_limit}</code> ({usage_pct:.0f}%)\n"
            f"├ Token: <code>{self.token_usage:,}/{self.token_limit:,}</code> ({token_pct:.0f}%)\n"
            f"└ 成本: ${self.cost_usd:.4f}\n"
            f"{suggestion_block}"
            f"{reset_block}"
        )

        return message[:900]


@dataclass
class K3sStatusMessage:
    """
    K3s 叢集狀態報告訊息 (K3S_STATUS)

    2026-03-31 Claude Code: Phase 21.2 定期報告
    用於每日 K3s 健康狀態推送
    按鈕: 無
    """
    report_date: str            # 報告日期 (YYYY-MM-DD HH:MM)
    # 節點狀態
    node_total: int = 0
    node_ready: int = 0
    # Pod 狀態
    pod_total: int = 0
    pod_running: int = 0
    pod_pending: int = 0
    pod_failed: int = 0
    # HPA 狀態
    hpa_api_replicas: str = "2/6"
    hpa_web_replicas: str = "2/6"
    hpa_worker_replicas: str = "1/3"
    # 備份狀態
    etcd_backup_last: str = ""
    velero_backup_last: str = ""
    # 穩定指標
    alert_count_48h: int = 0
    pod_restart_48h: int = 0
    # 版本資訊
    k3s_version: str = ""

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        # 健康狀態 emoji
        node_health = "✅" if self.node_ready == self.node_total else "⚠️"
        pod_health = "✅" if self.pod_failed == 0 and self.pod_pending == 0 else "⚠️"
        stability = "✅" if self.alert_count_48h == 0 and self.pod_restart_48h == 0 else "⚠️"

        # 備份狀態
        etcd_status = html.escape(self.etcd_backup_last[:20]) if self.etcd_backup_last else "N/A"
        velero_status = html.escape(self.velero_backup_last[:20]) if self.velero_backup_last else "N/A"

        message = (
            f"═══════════════════════════\n"
            f"🎛️ <b>K3s 叢集狀態</b> | Daily\n"
            f"═══════════════════════════\n"
            f"📅 {html.escape(self.report_date)}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{node_health} <b>節點</b>: {self.node_ready}/{self.node_total} Ready\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{pod_health} <b>Pod 狀態</b>\n"
            f"├ Running: <code>{self.pod_running}</code>\n"
            f"├ Pending: <code>{self.pod_pending}</code>\n"
            f"└ Failed: <code>{self.pod_failed}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📊 <b>HPA 副本數</b>\n"
            f"├ API: <code>{html.escape(self.hpa_api_replicas)}</code>\n"
            f"├ Web: <code>{html.escape(self.hpa_web_replicas)}</code>\n"
            f"└ Worker: <code>{html.escape(self.hpa_worker_replicas)}</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"💾 <b>備份</b>\n"
            f"├ etcd: {etcd_status}\n"
            f"└ Velero: {velero_status}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{stability} <b>48h 穩定度</b>\n"
            f"├ 告警: <code>{self.alert_count_48h}</code>\n"
            f"└ Pod 重啟: <code>{self.pod_restart_48h}</code>"
        )

        return message[:900]


@dataclass
class WeeklyReportMessage:
    """
    週報訊息 (WEEKLY_REPORT)

    2026-03-31 Claude Code: Phase 21.3 定期報告
    每週五 18:00 台北發送
    按鈕: 無
    """
    week_range: str             # 週次 (e.g., "2026-W14")
    report_date: str            # 報告日期時間
    # 告警統計
    alert_total: int = 0
    alert_critical: int = 0
    alert_resolved: int = 0
    resolved_rate: float = 0.0
    # AI 效能
    ai_proposal_count: int = 0
    ai_executed_count: int = 0
    ai_success_rate: float = 0.0
    avg_response_minutes: float = 0.0
    # K3s 健康
    k3s_uptime_pct: float = 99.9
    pod_restart_total: int = 0
    hpa_scale_events: int = 0
    # Git 活動
    commits_count: int = 0
    deploy_count: int = 0
    # 成本
    ai_cost_week: float = 0.0
    ai_tokens_week: int = 0

    def format(self) -> str:
        """格式化為 Telegram HTML"""
        # 健康狀態 emoji
        alert_health = "✅" if self.resolved_rate >= 80 else "⚠️"
        ai_health = "✅" if self.ai_success_rate >= 70 else "⚠️"
        k3s_health = "✅" if self.k3s_uptime_pct >= 99 else "⚠️"

        message = (
            f"═══════════════════════════\n"
            f"📊 <b>AWOOOI 週報</b>\n"
            f"═══════════════════════════\n"
            f"📅 {html.escape(self.week_range)} | {html.escape(self.report_date)}\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{alert_health} <b>告警統計</b>\n"
            f"├ 總數: <code>{self.alert_total}</code>\n"
            f"├ Critical: <code>{self.alert_critical}</code>\n"
            f"├ 已解決: <code>{self.alert_resolved}</code>\n"
            f"└ 解決率: <code>{self.resolved_rate:.1f}%</code>\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{ai_health} <b>AI 效能</b>\n"
            f"├ 提案數: <code>{self.ai_proposal_count}</code>\n"
            f"├ 執行數: <code>{self.ai_executed_count}</code>\n"
            f"├ 成功率: <code>{self.ai_success_rate:.1f}%</code>\n"
            f"└ 平均回應: <code>{self.avg_response_minutes:.1f}</code> 分鐘\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"{k3s_health} <b>K3s 健康</b>\n"
            f"├ Uptime: <code>{self.k3s_uptime_pct:.2f}%</code>\n"
            f"├ Pod 重啟: <code>{self.pod_restart_total}</code>\n"
            f"└ HPA 擴縮: <code>{self.hpa_scale_events}</code> 次\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"📦 <b>開發活動</b>\n"
            f"├ Commits: <code>{self.commits_count}</code>\n"
            f"└ 部署: <code>{self.deploy_count}</code> 次\n"
            f"━━━━━━━━━━━━━━━━━━━\n"
            f"💰 <b>AI 成本</b>\n"
            f"├ 費用: $<code>{self.ai_cost_week:.2f}</code>\n"
            f"└ Tokens: <code>{self.ai_tokens_week:,}</code>"
        )

        return message[:900]


# =============================================================================
# Risk Level Emoji Mapping
# =============================================================================

RISK_EMOJI_MAP = {
    "critical": "🚨",
    "high": "🔴",
    "medium": "⚠️",
    "low": "ℹ️",
}


# =============================================================================
# Telegram Gateway
# =============================================================================

class TelegramGatewayError(Exception):
    """Telegram Gateway 錯誤"""
    pass


class TelegramGateway:
    """
    Telegram Gateway - 行動戰情室 + SignOz 整合

    職責:
    1. 推送待簽核卡片到 Telegram (含 SignOz 指標)
    2. 接收並驗證簽核/調優回調
    3. Shadow Mode 調優執行 (僅日誌)
    4. 遵守 SOUL.md 訊息壓縮原則
    """

    TELEGRAM_API_BASE = "https://api.telegram.org"

    def __init__(self):
        self._http_client: httpx.AsyncClient | None = None
        self._security = get_security_interceptor()
        self._initialized = False
        # Long Polling 狀態 (Phase 5 內網修復)
        self._polling_active = False
        self._polling_task: asyncio.Task | None = None
        self._last_update_id = 0
        # Phase 6.5: 心跳監控 (防止沉默盲點)
        self._last_message_time: datetime | None = None
        self._heartbeat_task: asyncio.Task | None = None
        self._heartbeat_active = False

    async def initialize(self) -> bool:
        """初始化 Gateway"""
        if not settings.OPENCLAW_TG_BOT_TOKEN:
            logger.warning("telegram_gateway_disabled", reason="Bot token not configured")
            return False

        if not settings.OPENCLAW_TG_CHAT_ID:
            logger.warning("telegram_gateway_disabled", reason="Chat ID not configured")
            return False

        self._http_client = httpx.AsyncClient(
            timeout=30.0,
            headers={"Content-Type": "application/json"},
        )

        await self._security.initialize()
        self._initialized = True

        logger.info("telegram_gateway_initialized")
        return True

    @property
    def bot_token(self) -> str:
        """取得 Bot Token"""
        return settings.OPENCLAW_TG_BOT_TOKEN

    @property
    def chat_id(self) -> str:
        """取得 Chat ID"""
        return settings.OPENCLAW_TG_CHAT_ID

    @property
    def api_url(self) -> str:
        """取得 Telegram API URL"""
        return f"{self.TELEGRAM_API_BASE}/bot{self.bot_token}"

    async def _send_request(
        self,
        method: str,
        payload: dict,
    ) -> dict:
        """
        發送 Telegram API 請求

        Phase C P1: 新增 OTEL 追蹤
        @author Claude Code
        @date 2026-03-30 (台北時間)

        Args:
            method: API 方法 (sendMessage, editMessageText, etc.)
            payload: 請求 Payload

        Returns:
            dict: API 回應
        """
        if not self._initialized:
            await self.initialize()

        if not self._http_client:
            raise TelegramGatewayError("HTTP client not initialized")

        url = f"{self.api_url}/{method}"

        # OTEL Span: telegram.api.{method}
        with _tracer.start_as_current_span(
            f"telegram.api.{method}",
            attributes={
                "telegram.method": method,
                "telegram.chat_id": str(payload.get("chat_id", "")),
                "telegram.has_reply_markup": "reply_markup" in payload,
            },
        ) as span:
            try:
                response = await self._http_client.post(url, json=payload)
                response.raise_for_status()
                result = response.json()

                if not result.get("ok"):
                    span.set_attribute("telegram.error", result.get("description", "Unknown"))
                    span.set_status(trace.Status(trace.StatusCode.ERROR))
                    raise TelegramGatewayError(
                        f"Telegram API error: {result.get('description', 'Unknown error')}"
                    )

                # 成功: 記錄 message_id
                if "result" in result and "message_id" in result["result"]:
                    span.set_attribute("telegram.message_id", result["result"]["message_id"])

                span.set_status(trace.Status(trace.StatusCode.OK))
                return result

            except httpx.HTTPStatusError as e:
                span.set_attribute("telegram.http_status", e.response.status_code)
                span.set_status(trace.Status(trace.StatusCode.ERROR))
                span.record_exception(e)
                logger.error("telegram_api_error", method=method, status=e.response.status_code)
                raise TelegramGatewayError(f"HTTP error: {e.response.status_code}") from e

            except TelegramGatewayError:
                # 已處理的錯誤，直接拋出
                raise

            except Exception as e:
                span.set_status(trace.Status(trace.StatusCode.ERROR))
                span.record_exception(e)
                logger.error("telegram_request_failed", method=method, error=str(e))
                raise TelegramGatewayError(str(e)) from e

    def _build_inline_keyboard(
        self,
        approval_id: str,
        include_auto_tuning: bool = True,
        auto_tuning_command: str = "",
    ) -> dict:
        """
        建立 Inline Keyboard (簽核按鈕 + 延遲/靜默 + 自動調優)

        2026-03-27 ogt: P1 優化 - 新增稍後/靜默按鈕

        佈局:
        第一行: [✅ 簽核] [❌ 拒絕]
        第二行: [⏰ 稍後] [🔕 靜默 1h]
        第三行: [⚡ 執行自動調優] (可選)

        Args:
            approval_id: 簽核單 ID
            include_auto_tuning: 是否包含自動調優按鈕
            auto_tuning_command: kubectl 調優指令

        Returns:
            dict: Telegram InlineKeyboardMarkup
        """
        # 產生 Nonce (防重放)
        approve_nonce = self._security.generate_callback_nonce(approval_id, "approve")
        reject_nonce = self._security.generate_callback_nonce(approval_id, "reject")
        snooze_nonce = self._security.generate_callback_nonce(approval_id, "snooze")
        silence_nonce = self._security.generate_callback_nonce(approval_id, "silence")

        # 第一行: 主要操作
        buttons = [
            [
                {
                    "text": "✅ 簽核",
                    "callback_data": approve_nonce,
                },
                {
                    "text": "❌ 拒絕",
                    "callback_data": reject_nonce,
                },
            ],
            # 第二行: 延遲/靜默 (2026-03-27 P1 優化)
            [
                {
                    "text": "⏰ 稍後",
                    "callback_data": snooze_nonce,
                },
                {
                    "text": "🔕 靜默 1h",
                    "callback_data": silence_nonce,
                },
            ],
        ]

        # 第三行: 自動調優按鈕 (v7.0)
        if include_auto_tuning and auto_tuning_command:
            tuning_nonce = self._security.generate_callback_nonce(approval_id, "tune")
            buttons.append([
                {
                    "text": "⚡ 執行自動調優",
                    "callback_data": tuning_nonce,
                }
            ])

        return {"inline_keyboard": buttons}

    async def send_approval_card(
        self,
        approval_id: str,
        risk_level: str,
        resource_name: str,
        root_cause: str,
        suggested_action: str,
        estimated_downtime: str = "~30s",
        # v6.0 AI 仲裁欄位
        primary_responsibility: str = "COLLAB",
        confidence: float = 0.0,
        namespace: str = "default",
        # v7.0 SignOz 整合
        signoz_rps: float = 0.0,
        signoz_rps_trend: str = "stable",
        signoz_error_rate: float = 0.0,
        signoz_p99_latency: float = 0.0,
        signoz_latency_trend: str = "stable",
        signoz_trace_url: str = "",
        auto_tuning_command: str = "",
        # 2026-03-29 ogt: AI Token/Cost 追蹤
        ai_tokens: int = 0,
        ai_cost: float = 0.0,
        # 2026-03-29 ogt: ADR-037 異常頻率統計
        anomaly_frequency: dict | None = None,
        # 2026-03-29 ogt: AI Provider 來源顯示
        ai_provider: str = "",
    ) -> dict:
        """
        推送待簽核卡片到 Telegram (v7.0 含 SignOz 整合)

        SOUL.md 4.1 + AI 仲裁 + SignOz 訊息格式
        Phase 21 (ADR-037): 含異常頻率統計

        Args:
            approval_id: 簽核單 ID
            risk_level: 風險等級 (critical/medium/low)
            resource_name: 資源名稱
            root_cause: 根因摘要
            suggested_action: 建議操作
            estimated_downtime: 預計停機時間
            primary_responsibility: 責任團隊 (FE/BE/INFRA/DB/COLLAB)
            confidence: AI 信心度 (0.0-1.0)
            namespace: K8s namespace
            signoz_*: SignOz Gold Metrics
            signoz_trace_url: 動態時間參數的 Trace URL
            auto_tuning_command: kubectl 調優指令
            anomaly_frequency: 異常頻率統計 (ADR-037)

        Returns:
            dict: Telegram API 回應
        """
        # 取得狀態 Emoji
        emoji = RISK_EMOJI_MAP.get(risk_level.lower(), "⚠️")

        # 建立 SignOz 指標區塊
        signoz_metrics = None
        if signoz_rps > 0 or signoz_error_rate > 0 or signoz_p99_latency > 0:
            signoz_metrics = SignOzMetricsBlock(
                rps=signoz_rps,
                rps_trend=signoz_rps_trend,
                error_rate=signoz_error_rate,
                p99_latency_ms=signoz_p99_latency,
                latency_trend=signoz_latency_trend,
                trace_url=signoz_trace_url,
            )

        # 建立訊息結構 (含 AI 仲裁 + SignOz + Token/Cost + 頻率統計)
        message = TelegramMessage(
            status_emoji=emoji,
            risk_level=risk_level.upper(),
            resource_name=resource_name,
            root_cause=root_cause,
            suggested_action=suggested_action,
            estimated_downtime=estimated_downtime,
            approval_id=approval_id,
            primary_responsibility=primary_responsibility,
            confidence=confidence,
            namespace=namespace,
            signoz_metrics=signoz_metrics,
            signoz_trace_url=signoz_trace_url,
            auto_tuning_command=auto_tuning_command,
            # 2026-03-29 ogt: AI Token/Cost 追蹤
            ai_tokens=ai_tokens,
            ai_cost=ai_cost,
            # 2026-03-29 ogt: ADR-037 異常頻率統計
            anomaly_frequency=anomaly_frequency,
            # 2026-03-29 ogt: AI Provider 來源顯示
            ai_provider=ai_provider,
        )

        # 格式化訊息
        text = message.format()

        # 建立按鈕 (含自動調優)
        keyboard = self._build_inline_keyboard(
            approval_id=approval_id,
            include_auto_tuning=bool(auto_tuning_command),
            auto_tuning_command=auto_tuning_command,
        )

        # 發送訊息
        payload = {
            "chat_id": self.chat_id,
            "text": text,
            "parse_mode": "HTML",
            "reply_markup": keyboard,
            "disable_web_page_preview": True,  # 避免 SignOz URL 預覽
        }

        logger.info(
            "telegram_approval_card_sending",
            approval_id=approval_id,
            risk_level=risk_level,
            resource=resource_name,
            signoz_integrated=signoz_metrics is not None,
            auto_tuning_available=bool(auto_tuning_command),
        )

        result = await self._send_request("sendMessage", payload)

        logger.info(
            "telegram_approval_card_sent",
            approval_id=approval_id,
            message_id=result.get("result", {}).get("message_id"),
        )

        return result

    # =========================================================================
    # 新訊息發送方法 (2026-03-29 ogt: ADR-038)
    # =========================================================================

    def _build_sentry_keyboard(self, error_id: str) -> dict:
        """建立 Sentry 錯誤訊息按鈕"""
        view_nonce = self._security.generate_callback_nonce(error_id, "view")
        silence_nonce = self._security.generate_callback_nonce(error_id, "silence")

        return {
            "inline_keyboard": [
                [
                    {"text": "🔍 查看詳情", "callback_data": view_nonce},
                    {"text": "🔕 靜默 1h", "callback_data": silence_nonce},
                ]
            ]
        }

    def _build_resource_keyboard(self, resource_id: str) -> dict:
        """建立資源告警按鈕"""
        scale_nonce = self._security.generate_callback_nonce(resource_id, "scale")
        silence_nonce = self._security.generate_callback_nonce(resource_id, "silence")

        return {
            "inline_keyboard": [
                [
                    {"text": "⚡ 自動擴展", "callback_data": scale_nonce},
                    {"text": "🔕 靜默 1h", "callback_data": silence_nonce},
                ]
            ]
        }

    async def send_sentry_error(
        self,
        error_id: str,
        error_type: str,
        error_message: str,
        service_name: str,
        file_location: str,
        occurrence_count: int = 1,
        affected_users: int = 0,
        first_seen: str = "",
        stack_trace: list[str] | None = None,
        sentry_url: str = "",
    ) -> dict:
        """
        發送 Sentry 錯誤通知

        2026-03-29 ogt: 新增

        Args:
            error_id: Sentry Issue ID
            error_type: 錯誤類型 (TypeError, etc.)
            error_message: 錯誤訊息
            service_name: 服務名稱
            file_location: 檔案位置
            occurrence_count: 發生次數
            affected_users: 影響用戶數
            first_seen: 首次發生時間
            stack_trace: Stack trace
            sentry_url: Sentry 連結

        Returns:
            dict: Telegram API 回應
        """
        message = SentryErrorMessage(
            error_id=error_id,
            error_type=error_type,
            error_message=error_message,
            service_name=service_name,
            file_location=file_location,
            occurrence_count=occurrence_count,
            affected_users=affected_users,
            first_seen=first_seen,
            stack_trace=stack_trace,
            sentry_url=sentry_url,
        )

        payload = {
            "chat_id": self.chat_id,
            "text": message.format(),
            "parse_mode": "HTML",
            "reply_markup": self._build_sentry_keyboard(error_id),
            "disable_web_page_preview": True,
        }

        logger.info("telegram_sentry_error_sending", error_id=error_id, service=service_name)
        result = await self._send_request("sendMessage", payload)
        logger.info("telegram_sentry_error_sent", error_id=error_id)

        return result

    async def send_resource_warning(
        self,
        resource_id: str,
        pod_name: str,
        namespace: str = "default",
        cpu_percent: float = 0.0,
        cpu_limit: str = "",
        memory_percent: float = 0.0,
        memory_limit: str = "",
        disk_percent: float = 0.0,
        trend_info: str = "",
        suggestion: str = "",
    ) -> dict:
        """
        發送資源告警通知

        2026-03-29 ogt: 新增

        Args:
            resource_id: 資源 ID
            pod_name: Pod 名稱
            namespace: K8s namespace
            cpu_percent: CPU 使用率
            memory_percent: Memory 使用率
            disk_percent: Disk 使用率
            trend_info: 趨勢資訊
            suggestion: 建議

        Returns:
            dict: Telegram API 回應
        """
        message = ResourceWarnMessage(
            resource_id=resource_id,
            pod_name=pod_name,
            namespace=namespace,
            cpu_percent=cpu_percent,
            cpu_limit=cpu_limit,
            memory_percent=memory_percent,
            memory_limit=memory_limit,
            disk_percent=disk_percent,
            trend_info=trend_info,
            suggestion=suggestion,
        )

        payload = {
            "chat_id": self.chat_id,
            "text": message.format(),
            "parse_mode": "HTML",
            "reply_markup": self._build_resource_keyboard(resource_id),
            "disable_web_page_preview": True,
        }

        logger.info("telegram_resource_warning_sending", resource_id=resource_id, pod=pod_name)
        result = await self._send_request("sendMessage", payload)
        logger.info("telegram_resource_warning_sent", resource_id=resource_id)

        return result

    async def send_repair_report(
        self,
        report_date: str,
        total_repairs: int = 0,
        success_count: int = 0,
        failure_count: int = 0,
        saved_minutes: int = 0,
        top_issues: list[tuple[str, int]] | None = None,
        ai_cost_gemini: float = 0.0,
        ai_cost_nvidia: float = 0.0,
        ai_tokens_total: int = 0,
    ) -> dict:
        """
        發送自動修復報告

        2026-03-29 ogt: 新增

        Args:
            report_date: 報告日期
            total_repairs: 總修復次數
            success_count: 成功次數
            failure_count: 失敗次數
            saved_minutes: 節省人工時間
            top_issues: Top 問題列表
            ai_cost_gemini: Gemini 成本
            ai_cost_nvidia: NVIDIA 成本
            ai_tokens_total: 總 Token 數

        Returns:
            dict: Telegram API 回應
        """
        message = RepairReportMessage(
            report_date=report_date,
            total_repairs=total_repairs,
            success_count=success_count,
            failure_count=failure_count,
            saved_minutes=saved_minutes,
            top_issues=top_issues,
            ai_cost_gemini=ai_cost_gemini,
            ai_cost_nvidia=ai_cost_nvidia,
            ai_tokens_total=ai_tokens_total,
        )

        payload = {
            "chat_id": self.chat_id,
            "text": message.format(),
            "parse_mode": "HTML",
            "disable_web_page_preview": True,
        }

        logger.info("telegram_repair_report_sending", date=report_date)
        result = await self._send_request("sendMessage", payload)
        logger.info("telegram_repair_report_sent", date=report_date)

        return result

    async def send_daily_summary(
        self,
        summary_date: str,
        alert_total: int = 0,
        alert_critical: int = 0,
        alert_medium: int = 0,
        alert_low: int = 0,
        auto_repair_count: int = 0,
        manual_approval_count: int = 0,
        ignored_count: int = 0,
        avg_response_minutes: float = 0.0,
        api_availability: float = 99.9,
        web_availability: float = 99.9,
        worker_availability: float = 99.9,
        ai_cost: float = 0.0,
        cloud_cost: float = 0.0,
        budget_remaining: float = 0.0,
    ) -> dict:
        """
        發送每日摘要

        2026-03-29 ogt: 新增

        Returns:
            dict: Telegram API 回應
        """
        message = DailySummaryMessage(
            summary_date=summary_date,
            alert_total=alert_total,
            alert_critical=alert_critical,
            alert_medium=alert_medium,
            alert_low=alert_low,
            auto_repair_count=auto_repair_count,
            manual_approval_count=manual_approval_count,
            ignored_count=ignored_count,
            avg_response_minutes=avg_response_minutes,
            api_availability=api_availability,
            web_availability=web_availability,
            worker_availability=worker_availability,
            ai_cost=ai_cost,
            cloud_cost=cloud_cost,
            budget_remaining=budget_remaining,
        )

        payload = {
            "chat_id": self.chat_id,
            "text": message.format(),
            "parse_mode": "HTML",
            "disable_web_page_preview": True,
        }

        logger.info("telegram_daily_summary_sending", date=summary_date)
        result = await self._send_request("sendMessage", payload)
        logger.info("telegram_daily_summary_sent", date=summary_date)

        return result

    async def send_cicd_progress(
        self,
        job_name: str,
        status: str,
        stage: str = "",
        commit_sha: str = "",
        triggered_by: str = "",
        duration_seconds: int = 0,
        message: str = "",
        workflow_url: str = "",
        max_retries: int = 3,
    ) -> dict:
        """
        發送 CI/CD 進度通知 (簡潔版，不走 AI 仲裁)

        2026-03-30 ogt: 新增，解決 CI/CD 告警被當成事件處理的問題
        2026-03-30 P1: 新增重試機制 (指數退避)

        Args:
            max_retries: 最大重試次數 (預設 3)

        Returns:
            dict: Telegram API 回應
        """
        # OTEL Span: telegram.send_cicd_progress
        with _tracer.start_as_current_span(
            "telegram.send_cicd_progress",
            attributes={
                "telegram.job_name": job_name,
                "telegram.status": status,
                "telegram.stage": stage,
                "telegram.max_retries": max_retries,
            },
        ) as span:
            msg = CICDProgressMessage(
                job_name=job_name,
                status=status,
                stage=stage,
                commit_sha=commit_sha,
                triggered_by=triggered_by,
                duration_seconds=duration_seconds,
                message=message,
                workflow_url=workflow_url,
            )

            payload = {
                "chat_id": self.chat_id,
                "text": msg.format(),
                "parse_mode": "HTML",
                "disable_web_page_preview": True,
            }

            logger.info("telegram_cicd_progress_sending", job=job_name, status=status)

            # 重試機制 (指數退避)
            last_error = None
            for attempt in range(max_retries):
                try:
                    result = await self._send_request("sendMessage", payload)
                    span.set_attribute("telegram.attempts", attempt + 1)
                    span.set_status(trace.Status(trace.StatusCode.OK))
                    logger.info("telegram_cicd_progress_sent", job=job_name, status=status, attempt=attempt + 1)
                    return result
                except TelegramGatewayError as e:
                    last_error = e
                    if attempt < max_retries - 1:
                        delay = 2 ** attempt  # 1, 2, 4 秒
                        logger.warning(
                            "telegram_cicd_progress_retry",
                            job=job_name,
                            attempt=attempt + 1,
                            delay=delay,
                            error=str(e),
                        )
                        await asyncio.sleep(delay)

            # 所有重試都失敗
            span.set_attribute("telegram.attempts", max_retries)
            span.set_status(trace.Status(trace.StatusCode.ERROR))
            span.record_exception(last_error)
            logger.error(
                "telegram_cicd_progress_failed",
                job=job_name,
                status=status,
                max_retries=max_retries,
                error=str(last_error),
            )
            raise last_error

    async def send_deploy_success(
        self,
        commit_sha: str,
        triggered_by: str,
        environment: str = "Production",
        api_version: str = "",
        web_version: str = "",
        worker_version: str = "",
        duration_seconds: int = 0,
        e2e_passed: int = 0,
        e2e_total: int = 0,
        health_check_passed: bool = True,
        workflow_url: str = "",
    ) -> dict:
        """
        發送部署成功通知

        2026-03-29 ogt: 新增

        Returns:
            dict: Telegram API 回應
        """
        message = DeploySuccessMessage(
            commit_sha=commit_sha,
            triggered_by=triggered_by,
            environment=environment,
            api_version=api_version,
            web_version=web_version,
            worker_version=worker_version,
            duration_seconds=duration_seconds,
            e2e_passed=e2e_passed,
            e2e_total=e2e_total,
            health_check_passed=health_check_passed,
            workflow_url=workflow_url,
        )

        payload = {
            "chat_id": self.chat_id,
            "text": message.format(),
            "parse_mode": "HTML",
            "disable_web_page_preview": True,
        }

        logger.info("telegram_deploy_success_sending", commit=commit_sha[:8])
        result = await self._send_request("sendMessage", payload)
        logger.info("telegram_deploy_success_sent", commit=commit_sha[:8])

        return result

    async def send_rate_limit_warning(
        self,
        provider: str,
        daily_usage: int = 0,
        daily_limit: int = 0,
        token_usage: int = 0,
        token_limit: int = 0,
        cost_usd: float = 0.0,
        suggestions: list[str] | None = None,
        reset_time: str = "",
    ) -> dict:
        """
        發送 API 限額警告

        2026-03-29 ogt: 新增

        Returns:
            dict: Telegram API 回應
        """
        message = RateLimitMessage(
            provider=provider,
            daily_usage=daily_usage,
            daily_limit=daily_limit,
            token_usage=token_usage,
            token_limit=token_limit,
            cost_usd=cost_usd,
            suggestions=suggestions,
            reset_time=reset_time,
        )

        payload = {
            "chat_id": self.chat_id,
            "text": message.format(),
            "parse_mode": "HTML",
            "disable_web_page_preview": True,
        }

        logger.info("telegram_rate_limit_warning_sending", provider=provider)
        result = await self._send_request("sendMessage", payload)
        logger.info("telegram_rate_limit_warning_sent", provider=provider)

        return result

    async def handle_callback(
        self,
        callback_query_id: str,
        callback_data: str,
        user_id: int,
        message_id: int,
        original_text: str = "",
        username: str = "",
    ) -> dict:
        """
        處理簽核/調優回調

        Args:
            callback_query_id: Telegram Callback Query ID
            callback_data: Callback Data (包含 nonce)
            user_id: Telegram User ID
            message_id: 原始訊息 ID
            original_text: 原始卡片內容 (用於保留上下文)
            username: 簽核者使用者名稱

        Returns:
            dict: 處理結果 {action, approval_id, user, auto_tuning_result?}
        """
        try:
            # ===================================================================
            # Step 1: 安全驗證 (白名單 + Nonce)
            # ===================================================================
            parsed = self._security.parse_callback_data(callback_data)
            action = parsed["action"]
            approval_id = parsed["approval_id"]
            nonce = parsed["nonce"]

            # 驗證使用者 + Nonce
            user = await self._security.verify_callback(
                user_id=user_id,
                callback_id=callback_query_id,
                nonce=nonce,
            )

            # ===================================================================
            # Step 2: 處理自動調優 (Shadow Mode)
            # ===================================================================
            auto_tuning_result = None
            if action == "tune":
                auto_tuning_result = await self._handle_auto_tuning(
                    approval_id=approval_id,
                    user_id=user_id,
                    username=username,
                )
                # 回應 Callback Query
                await self._answer_callback(
                    callback_query_id,
                    "tune",
                    text="⚡ 調優指令已記錄 (Shadow Mode)",
                )
                # 更新訊息
                await self._update_message_after_action(
                    message_id=message_id,
                    action="tune",
                    username=username,
                    original_text=original_text,
                    extra_info=auto_tuning_result.get("command", ""),
                )

                return {
                    "action": action,
                    "approval_id": approval_id,
                    "user": user,
                    "success": True,
                    "auto_tuning_result": auto_tuning_result,
                }

            # ===================================================================
            # Step 2.5: 處理稍後/靜默 (2026-03-27 P1 優化)
            # ===================================================================
            if action == "snooze":
                snooze_result = await self._handle_snooze(
                    approval_id=approval_id,
                    username=username,
                )
                await self._answer_callback(
                    callback_query_id,
                    "snooze",
                    text="⏰ 30 分鐘後再提醒",
                )
                await self._update_message_after_action(
                    message_id=message_id,
                    action="snooze",
                    username=username,
                    original_text=original_text,
                )
                return {
                    "action": action,
                    "approval_id": approval_id,
                    "user": user,
                    "success": True,
                    "snooze_result": snooze_result,
                }

            if action == "silence":
                silence_result = await self._handle_silence(
                    approval_id=approval_id,
                    username=username,
                    original_text=original_text,
                )
                await self._answer_callback(
                    callback_query_id,
                    "silence",
                    text="🔕 此類告警靜默 1 小時",
                )
                await self._update_message_after_action(
                    message_id=message_id,
                    action="silence",
                    username=username,
                    original_text=original_text,
                    extra_info=silence_result.get("resource_name", ""),
                )
                return {
                    "action": action,
                    "approval_id": approval_id,
                    "user": user,
                    "success": True,
                    "silence_result": silence_result,
                }

            # ===================================================================
            # Step 3: 回應 Callback Query (簽核/拒絕)
            # ===================================================================
            await self._answer_callback(callback_query_id, action)

            # ===================================================================
            # Step 4: 更新訊息 (保留原始內容 + 簽核鋼印)
            # ===================================================================
            await self._update_message_after_action(
                message_id=message_id,
                action=action,
                username=username,
                original_text=original_text,
            )

            logger.info(
                "telegram_callback_processed",
                action=action,
                approval_id=approval_id,
                user_id=user_id,
            )

            return {
                "action": action,
                "approval_id": approval_id,
                "user": user,
                "success": True,
            }

        except UserNotWhitelistedError as e:
            logger.warning("telegram_callback_denied", error=str(e), user_id=user_id)
            await self._answer_callback(
                callback_query_id,
                "denied",
                text="⛔ 您沒有簽核權限",
            )
            return {"success": False, "error": str(e)}

        except NonceReplayError as e:
            logger.warning("telegram_callback_replay", error=str(e))
            await self._answer_callback(
                callback_query_id,
                "replay",
                text="⚠️ 此操作已處理過",
            )
            return {"success": False, "error": str(e)}

        except Exception as e:
            logger.error("telegram_callback_error", error=str(e))
            await self._answer_callback(
                callback_query_id,
                "error",
                text="❌ 處理失敗",
            )
            return {"success": False, "error": str(e)}

    async def _handle_auto_tuning(
        self,
        approval_id: str,
        user_id: int,
        username: str,
    ) -> dict:
        """
        處理自動調優請求 (Shadow Mode)

        統帥鐵律: Shadow Mode 下嚴禁實際執行 K8s 命令

        Args:
            approval_id: 簽核單 ID
            user_id: 執行者 Telegram ID
            username: 執行者名稱

        Returns:
            dict: 調優結果
        """
        try:
            # Shadow Mode: 僅記錄調優請求
            # 實際生產環境需從 ApprovalRecord 取得完整調優指令
            # Shadow Mode: 僅記錄調優請求
            # 實際生產環境需從 ApprovalRecord 取得完整調優指令
            log_message = f"[SHADOW MODE] 自動調優請求 - 簽核單: {approval_id}"

            if settings.SHADOW_MODE_ENABLED:
                logger.info(
                    "shadow_mode_auto_tuning_triggered",
                    approval_id=approval_id,
                    user_id=user_id,
                    username=username,
                    shadow_mode=True,
                )
                print(f"\n{'='*60}")
                print("[SHADOW MODE] AI 生成的調優指令請求")
                print(f"簽核單: {approval_id}")
                print(f"執行者: @{username} (ID: {user_id})")
                print(f"時間: {datetime.now(UTC).isoformat()}")
                print("狀態: 僅記錄，未實際執行")
                print(f"{'='*60}\n")

                return {
                    "executed": False,
                    "shadow_mode": True,
                    "approval_id": approval_id,
                    "triggered_by": username,
                    "command": "kubectl command logged (see server logs)",
                    "log": log_message,
                }
            else:
                logger.warning(
                    "auto_tuning_blocked_not_shadow_mode",
                    approval_id=approval_id,
                    message="Production execution requires multi-sig approval",
                )
                return {
                    "executed": False,
                    "shadow_mode": False,
                    "approval_id": approval_id,
                    "error": "Production execution requires multi-sig approval",
                }

        except Exception as e:
            logger.error("auto_tuning_error", error=str(e), approval_id=approval_id)
            return {
                "executed": False,
                "error": str(e),
            }

    async def _handle_snooze(
        self,
        approval_id: str,
        username: str,
    ) -> dict:
        """
        處理稍後提醒 (2026-03-27 P1 優化)

        功能: 延遲 30 分鐘後再提醒此告警

        Args:
            approval_id: 簽核單 ID
            username: 執行者名稱

        Returns:
            dict: 處理結果
        """
        try:
            redis = get_redis()
            snooze_key = f"{SNOOZE_KEY_PREFIX}{approval_id}"

            # 設置 30 分鐘延遲標記
            await redis.setex(
                snooze_key,
                SNOOZE_TTL_SECONDS,
                f"{username}:{datetime.now(UTC).isoformat()}",
            )

            logger.info(
                "telegram_snooze_set",
                approval_id=approval_id,
                username=username,
                ttl_minutes=SNOOZE_TTL_SECONDS // 60,
            )

            return {
                "snoozed": True,
                "approval_id": approval_id,
                "snooze_until": datetime.now(UTC).isoformat(),
                "ttl_minutes": SNOOZE_TTL_SECONDS // 60,
            }

        except Exception as e:
            logger.error("snooze_error", error=str(e), approval_id=approval_id)
            return {
                "snoozed": False,
                "error": str(e),
            }

    async def _handle_silence(
        self,
        approval_id: str,
        username: str,
        original_text: str,
    ) -> dict:
        """
        處理靜默 1 小時 (2026-03-27 P1 優化)

        功能: 同類告警 (相同資源) 1 小時內不再發送

        Args:
            approval_id: 簽核單 ID
            username: 執行者名稱
            original_text: 原始訊息 (用於解析資源名稱)

        Returns:
            dict: 處理結果
        """
        try:
            redis = get_redis()

            # 從原始訊息解析資源名稱 (格式: 🎯 資源: xxx)
            resource_name = "unknown"
            for line in original_text.split("\n"):
                if "🎯 資源:" in line or "🎯 資源: " in line:
                    resource_name = line.split(":")[-1].strip()
                    break

            silence_key = f"{SILENCE_KEY_PREFIX}{resource_name}"

            # 設置 1 小時靜默標記
            await redis.setex(
                silence_key,
                SILENCE_TTL_SECONDS,
                f"{username}:{datetime.now(UTC).isoformat()}:{approval_id}",
            )

            logger.info(
                "telegram_silence_set",
                approval_id=approval_id,
                resource_name=resource_name,
                username=username,
                ttl_hours=SILENCE_TTL_SECONDS // 3600,
            )

            return {
                "silenced": True,
                "approval_id": approval_id,
                "resource_name": resource_name,
                "silence_until": datetime.now(UTC).isoformat(),
                "ttl_hours": SILENCE_TTL_SECONDS // 3600,
            }

        except Exception as e:
            logger.error("silence_error", error=str(e), approval_id=approval_id)
            return {
                "silenced": False,
                "error": str(e),
            }

    async def _answer_callback(
        self,
        callback_query_id: str,
        action: str,
        text: str | None = None,
    ) -> None:
        """回應 Callback Query"""
        if text is None:
            if action == "approve":
                text = "✅ 已簽核"
            elif action == "reject":
                text = "❌ 已拒絕"
            elif action == "tune":
                text = "⚡ 調優中..."
            elif action == "snooze":
                text = "⏰ 30 分鐘後再提醒"
            elif action == "silence":
                text = "🔕 此類告警靜默 1 小時"
            else:
                text = "✓ 已處理"

        await self._send_request("answerCallbackQuery", {
            "callback_query_id": callback_query_id,
            "text": text,
            "show_alert": False,
        })

    async def _update_message_after_action(
        self,
        message_id: int,
        action: str,
        username: str,
        original_text: str,
        extra_info: str = "",
    ) -> None:
        """
        更新訊息: 保留原始卡片內容 + 簽核/調優鋼印

        UX 要求:
        - 嚴禁覆蓋原始內容
        - 必須在底部加上分隔線與簽核狀態
        - 移除所有按鈕
        """
        # 構建鋼印 (2026-03-27 ogt: 新增 snooze/silence)
        if action == "approve":
            stamp = f"✅ 已由 @{username} 授權執行"
        elif action == "reject":
            stamp = f"❌ 已由 @{username} 拒絕執行"
        elif action == "tune":
            stamp = f"⚡ 已由 @{username} 觸發自動調優 (Shadow Mode)"
            if extra_info:
                stamp += "\n📝 指令已記錄"
        elif action == "snooze":
            stamp = f"⏰ @{username} 已設定 30 分鐘後再提醒"
        elif action == "silence":
            resource_info = f" ({extra_info})" if extra_info else ""
            stamp = f"🔕 @{username} 已靜默此類告警 1 小時{resource_info}"
        else:
            stamp = f"✓ 已由 @{username} 處理"

        # 組合: 原始內容 + 分隔線 + 鋼印
        separator = "──────────────"
        updated_text = f"{original_text}\n{separator}\n{stamp}"

        # 使用 editMessageText 同時更新內容並移除按鈕
        await self._send_request("editMessageText", {
            "chat_id": self.chat_id,
            "message_id": message_id,
            "text": updated_text,
            "parse_mode": "HTML",
            "reply_markup": {"inline_keyboard": []},
            "disable_web_page_preview": True,
        })

    async def send_notification(
        self,
        text: str,
        parse_mode: str = "HTML",
        chat_id: str | int | None = None,
    ) -> dict:
        """
        發送純文字通知

        Args:
            text: 訊息內容
            parse_mode: 解析模式

        Returns:
            dict: API 回應
        """
        payload = {
            "chat_id": chat_id or self.chat_id,
            "text": text[:500],  # SOUL.md 字數限制
            "parse_mode": parse_mode,
        }

        return await self._send_request("sendMessage", payload)

    async def close(self) -> None:
        """關閉 Gateway"""
        # 停止 Long Polling
        self._polling_active = False
        if self._polling_task and not self._polling_task.done():
            self._polling_task.cancel()
            try:
                await self._polling_task
            except asyncio.CancelledError:
                pass
            self._polling_task = None

        if self._http_client:
            await self._http_client.aclose()
            self._http_client = None
        self._initialized = False
        logger.info("telegram_gateway_closed")

    # =========================================================================
    # Long Polling 實作 (Phase 5 內網修復)
    # =========================================================================

    async def start_long_polling(self) -> None:
        """
        啟動 Long Polling 背景任務

        取代 Webhook 模式，適用於內網環境
        統帥鐵律: 內網無法接收外部 Webhook，必須主動輪詢
        """
        if not self._initialized:
            success = await self.initialize()
            if not success:
                logger.error("telegram_long_polling_failed", reason="Gateway not initialized")
                return

        if self._polling_active:
            logger.warning("telegram_long_polling_already_running")
            return

        # 先刪除 Webhook，確保可以使用 getUpdates
        await self._delete_webhook()

        self._polling_active = True
        self._last_update_id = 0
        self._polling_task = asyncio.create_task(self._polling_loop())

        logger.info(
            "telegram_long_polling_started",
            timeout=LONG_POLLING_TIMEOUT,
            chat_id=self.chat_id[:10] + "..." if self.chat_id else "N/A",
        )

    async def _delete_webhook(self) -> None:
        """
        刪除現有 Webhook (切換至 Long Polling 模式)

        統帥鐵律: Webhook 和 Long Polling 不能共存
        必須先刪除 Webhook 才能使用 getUpdates
        """
        if not self._http_client:
            return

        try:
            # Step 1: 刪除 Webhook
            url = f"{self.api_url}/deleteWebhook"
            response = await self._http_client.post(url, json={"drop_pending_updates": True})
            result = response.json()

            if result.get("ok"):
                logger.info(
                    "telegram_webhook_deleted",
                    description=result.get("description", "Webhook deleted"),
                )
            else:
                logger.warning(
                    "telegram_webhook_delete_failed",
                    error=result.get("description"),
                )

            # Step 2: 等待 Telegram 伺服器同步 (避免 409 Conflict)
            await asyncio.sleep(1)

            # Step 3: 驗證 Webhook 狀態
            info_url = f"{self.api_url}/getWebhookInfo"
            info_response = await self._http_client.get(info_url)
            info_result = info_response.json()

            webhook_url = info_result.get("result", {}).get("url", "")
            if webhook_url:
                logger.warning(
                    "telegram_webhook_still_active",
                    url=webhook_url[:50],
                )
            else:
                logger.info("telegram_webhook_confirmed_deleted")

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

    async def _polling_loop(self) -> None:
        """
        Long Polling 主循環

        使用 getUpdates API 持續監聽 Telegram 更新
        """
        logger.info("[Telegram] Long polling started - 神經已接通，等待統帥指令...")

        while self._polling_active:
            try:
                updates = await self._get_updates()

                for update in updates:
                    await self._process_update(update)

            except asyncio.CancelledError:
                logger.info("telegram_long_polling_cancelled")
                break

            except httpx.TimeoutException:
                # Long polling timeout 是正常的，繼續下一輪
                continue

            except httpx.HTTPStatusError as e:
                if e.response.status_code == 409:
                    # 409 Conflict: 另一個 getUpdates session 殘留
                    # 可能是舊 pod/host 的連線未清除，等待後重試
                    logger.warning(
                        "telegram_polling_conflict",
                        status=409,
                        message="409 衝突，等待殘留 session 過期...",
                    )
                    # 2026-03-31 ogt: 侵略性搶佔模式 - 縮短等待以從 .188 手中奪回主權
                    await asyncio.sleep(2)
                else:
                    logger.error("telegram_polling_http_error", status=e.response.status_code)
                    await asyncio.sleep(LONG_POLLING_RETRY_DELAY)

            except Exception as e:
                logger.error("telegram_polling_error", error=str(e))
                # 錯誤後等待再重試
                await asyncio.sleep(LONG_POLLING_RETRY_DELAY)

        logger.info("telegram_long_polling_stopped")

    async def _get_updates(self) -> list[dict]:
        """
        呼叫 Telegram getUpdates API

        Returns:
            list[dict]: 更新列表
        """
        if not self._http_client:
            return []

        url = f"{self.api_url}/getUpdates"
        payload = {
            "offset": self._last_update_id + 1,
            "timeout": LONG_POLLING_TIMEOUT,
            "allowed_updates": ["callback_query", "message"],  # 監聽按鈕與文字訊息
        }

        response = await self._http_client.post(
            url,
            json=payload,
            timeout=LONG_POLLING_TIMEOUT + 10,  # 比 API timeout 多一點
        )
        response.raise_for_status()
        result = response.json()

        if not result.get("ok"):
            raise TelegramGatewayError(f"getUpdates failed: {result.get('description')}")

        updates = result.get("result", [])

        # 更新 offset
        if updates:
            self._last_update_id = updates[-1]["update_id"]

        return updates

    async def _process_update(self, update: dict) -> None:
        """
        處理單個 Telegram Update

        Args:
            update: Telegram Update 物件
        """
        update_id = update.get("update_id")
        callback_query = update.get("callback_query")
        message = update.get("message")

        if not callback_query and not message:
            logger.debug("telegram_update_ignored", update_id=update_id, reason="unsupported update type")
            return

        if callback_query:
            await self._handle_callback_query(update_id, callback_query)
        elif message:
            await self._handle_chat_message(update_id, message)

    async def _handle_callback_query(self, update_id: int, callback_query: dict) -> None:
        """處理按鈕點擊更新"""
        callback_query_id = callback_query.get("id")
        callback_data = callback_query.get("data")
        user = callback_query.get("from", {})
        user_id = user.get("id")

        if not all([callback_query_id, callback_data, user_id]):
            logger.warning("telegram_callback_invalid", update_id=update_id)
            return

        username = user.get("username") or user.get("first_name") or str(user_id)
        original_text = callback_query.get("message", {}).get("text", "")
        message_id = callback_query.get("message", {}).get("message_id")

        logger.info(
            "telegram_callback_received",
            update_id=update_id,
            user_id=user_id,
            username=username,
        )

        # 呼叫現有的 handle_callback 邏輯
        result = await self.handle_callback(
            callback_query_id=callback_query_id,
            callback_data=callback_data,
            user_id=user_id,
            message_id=message_id,
            original_text=original_text,
            username=username,
        )

        if result.get("success"):
            # 執行資料庫更新 (簽核/拒絕)
            await self._execute_approval_action(
                action=result["action"],
                approval_id=result["approval_id"],
                user_id=user_id,
                username=username,
                message_id=message_id,
            )

    async def _handle_chat_message(self, update_id: int, message: dict) -> None:
        """處理统帥的文字訊息"""
        text = message.get("text")
        user = message.get("from", {})
        user_id = user.get("id")
        chat_id = message.get("chat", {}).get("id")
        username = user.get("username") or user.get("first_name") or str(user_id)

        if not text or not user_id:
            return

        logger.info(
            "telegram_chat_received",
            update_id=update_id,
            user_id=user_id,
            username=username,
            text=text[:50],
        )

        # 1. 安全檢查 (ADR-012)
        try:
            interceptor = get_security_interceptor()
            await interceptor.intercept_telegram(user_id)
        except Exception as e:
            logger.warning("telegram_chat_unauthorized", user_id=user_id, error=str(e))
            return

        # 2. 顯示 "正在輸入中..."
        await self._send_chat_action(chat_id, "typing")

        # 3. 呼叫 ChatManager 處理
        chat_manager = get_chat_manager()
        response = await chat_manager.generate_response(
            user_id=user_id,
            username=username,
            message_text=text,
        )

        # 4. 回覆統帥 (定向回傳)
        await self.send_notification(response, parse_mode="HTML", chat_id=chat_id)

    async def _send_chat_action(self, chat_id: int, action: str) -> None:
        """發送聊天狀態 (e.g., typing)"""
        if not self._http_client: return
        try:
            url = f"{self.api_url}/sendChatAction"
            await self._http_client.post(url, json={"chat_id": chat_id, "action": action})
        except: pass

    async def _execute_approval_action(
        self,
        action: str,
        approval_id: str,
        user_id: int,
        username: str,
        message_id: int,
    ) -> None:
        """
        執行簽核動作 (更新資料庫)

        Args:
            action: approve/reject/tune
            approval_id: 簽核單 ID
            user_id: Telegram User ID
            username: 使用者名稱
            message_id: 訊息 ID
        """
        # 2026-03-29 ogt: 修復方法呼叫 - add_signature/reject 不存在
        # 正確方法: sign_approval / reject_approval
        from uuid import UUID

        from src.services.approval_db import get_approval_service

        try:
            service = get_approval_service()

            if action == "approve":
                # 2026-03-29 ogt: 正確呼叫 sign_approval (返回 tuple)
                approval, message, execution_triggered = await service.sign_approval(
                    approval_id=UUID(approval_id),
                    signer_id=f"tg_{user_id}",
                    signer_name=username,
                    comment="Telegram 簽核 (Long Polling)",
                )

                if approval:
                    logger.info(
                        "telegram_approval_signed_via_polling",
                        approval_id=approval_id,
                        user_id=user_id,
                        status=approval.status.value,
                        execution_triggered=execution_triggered,
                    )
                    print(f"\n{'='*60}")
                    print("✅ 統帥已授權執行！")
                    print(f"簽核單: {approval_id}")
                    print(f"簽核者: @{username} (ID: {user_id})")
                    print(f"狀態: {approval.status.value}")
                    print(f"執行觸發: {execution_triggered}")
                    print(f"時間: {datetime.now(UTC).isoformat()}")
                    print(f"{'='*60}\n")

            elif action == "reject":
                # 2026-03-29 ogt: 正確呼叫 reject_approval (返回 tuple)
                approval, message = await service.reject_approval(
                    approval_id=UUID(approval_id),
                    rejector_id=f"tg_{user_id}",
                    rejector_name=username,
                    reason="Telegram 拒絕 (Long Polling)",
                )

                if approval:
                    logger.info(
                        "telegram_approval_rejected_via_polling",
                        approval_id=approval_id,
                        user_id=user_id,
                    )
                    print(f"\n{'='*60}")
                    print("❌ 統帥已拒絕執行！")
                    print(f"簽核單: {approval_id}")
                    print(f"拒絕者: @{username}")
                    print(f"{'='*60}\n")

            elif action == "tune":
                # 自動調優已在 handle_callback 中處理
                logger.info(
                    "telegram_auto_tuning_via_polling",
                    approval_id=approval_id,
                    user_id=user_id,
                )

        except Exception as e:
            logger.error(
                "telegram_approval_action_failed",
                action=action,
                approval_id=approval_id,
                error=str(e),
            )


# =============================================================================
# Phase 6.5: 心跳監控方法
# =============================================================================

    async def send_heartbeat(self) -> bool:
        """
        發送心跳訊息 (系統狀態摘要)

        每 30 分鐘執行一次，證明告警鏈路正常運作
        """
        try:
            if not self._initialized:
                await self.initialize()

            # 心跳訊息 (2026-03-30 ogt: 改用台北時區，符合 feedback_timezone_taipei.md)
            from src.utils.timezone import now_taipei
            taipei_now = now_taipei()
            text = f"""💓 <b>AWOOOI 心跳</b>
⏰ {taipei_now.strftime('%Y-%m-%d %H:%M:%S')} (台北)
📡 告警鏈路: ✅ 正常"""

            await self.send_notification(text)
            self._last_message_time = datetime.now(UTC)

            logger.info("telegram_heartbeat_sent")
            return True

        except Exception as e:
            logger.error("telegram_heartbeat_failed", error=str(e))
            return False

    async def start_heartbeat_monitor(
        self,
        heartbeat_interval_minutes: int = 30,
        silence_threshold_hours: int = 2,
    ) -> None:
        """
        啟動心跳監控背景任務

        Args:
            heartbeat_interval_minutes: 心跳間隔 (預設 30 分鐘)
            silence_threshold_hours: 沉默告警閾值 (預設 2 小時)
        """
        if self._heartbeat_active:
            logger.warning("telegram_heartbeat_already_running")
            return

        self._heartbeat_active = True
        self._heartbeat_task = asyncio.create_task(
            self._heartbeat_loop(heartbeat_interval_minutes, silence_threshold_hours)
        )

        logger.info(
            "telegram_heartbeat_monitor_started",
            interval_minutes=heartbeat_interval_minutes,
            silence_threshold_hours=silence_threshold_hours,
        )

    async def _heartbeat_loop(
        self,
        interval_minutes: int,
        silence_hours: int,
    ) -> None:
        """心跳監控循環"""
        interval_seconds = interval_minutes * 60
        silence_seconds = silence_hours * 3600

        while self._heartbeat_active:
            try:
                # 1. 檢查沉默告警
                if self._last_message_time:
                    silence_duration = (datetime.now(UTC) - self._last_message_time).total_seconds()
                    if silence_duration > silence_seconds:
                        # 發送沉默告警
                        hours = int(silence_duration // 3600)
                        await self.send_notification(
                            f"⚠️ <b>沉默告警</b>\n\n"
                            f"Telegram 已 <b>{hours} 小時</b>沒有收到任何訊息！\n"
                            f"請檢查告警鏈路是否正常運作。"
                        )
                        logger.warning(
                            "telegram_silence_alert",
                            silence_hours=hours,
                        )

                # 2. 發送心跳
                await self.send_heartbeat()

                # 3. 等待下一次
                await asyncio.sleep(interval_seconds)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error("telegram_heartbeat_loop_error", error=str(e))
                await asyncio.sleep(60)  # 錯誤後等待 1 分鐘重試

    async def stop_heartbeat_monitor(self) -> None:
        """停止心跳監控"""
        self._heartbeat_active = False
        if self._heartbeat_task and not self._heartbeat_task.done():
            self._heartbeat_task.cancel()
            try:
                await self._heartbeat_task
            except asyncio.CancelledError:
                pass
        self._heartbeat_task = None
        logger.info("telegram_heartbeat_monitor_stopped")


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

_gateway: TelegramGateway | None = None


def get_telegram_gateway() -> TelegramGateway:
    """取得全域 TelegramGateway 實例"""
    global _gateway
    if _gateway is None:
        _gateway = TelegramGateway()
    return _gateway
