"""
services/telegram_templates.py
Telegram 訊息模板系統 v2

═══ 訊息分類 ═══════════════════════════════════════════════
  🚨 告警類   — price_alert_msg / threat_alert_msg / system_alert_msg
  📊 報告類   — daily_report_msg / weekly_report_msg / monthly_report_msg
  💰 決策類   — price_decision / batch_decision_msg
  🤖 系統類   — deploy_msg / heal_msg / meta_analysis_msg
  💡 洞察類   — triaged_alert / insight_summary_msg
═══════════════════════════════════════════════════════════

規範：
  1. 所有模板使用 HTML parse_mode
  2. 一律繁體中文，Agent 名稱保留英文（Hermes/NemoTron/OpenClaw/EA）
  3. 每則訊息須含：標題行 / 核心數據 / 建議行動（三段式）
  4. 圖文訊息使用 send_photo_with_caption（附說明文字）
"""

import io
import json
import logging
import os
from html import escape
from datetime import datetime
from typing import Any, Dict, List, Optional

sys_log = logging.getLogger("TelegramTpl")

TELEGRAM_BOT_TOKEN_ENV = "TELEGRAM_BOT_TOKEN"
TELEGRAM_CHAT_IDS_ENV  = "TELEGRAM_CHAT_IDS"


# ══════════════════════════════════════════════════════════════════════════════
# 基礎工具
# ══════════════════════════════════════════════════════════════════════════════

def _get_bot_token() -> Optional[str]:
    from dotenv import load_dotenv
    load_dotenv()
    return os.getenv(TELEGRAM_BOT_TOKEN_ENV)


def _get_chat_ids() -> list:
    token = _get_bot_token()
    if not token:
        sys_log.warning("[TelegramTpl] %s 未設定，跳過 Telegram 通知", TELEGRAM_BOT_TOKEN_ENV)
        return []
    raw = os.getenv(TELEGRAM_CHAT_IDS_ENV, "[]")
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        sys_log.warning("[TelegramTpl] %s 格式錯誤，應為 JSON 陣列", TELEGRAM_CHAT_IDS_ENV)
        return []


def _send_telegram_raw(text: str, chat_ids: Optional[list] = None,
                       reply_markup: Optional[Dict[str, Any]] = None,
                       parse_mode: Optional[str] = "HTML") -> bool:
    """發送純文字訊息"""
    import requests
    token = _get_bot_token()
    if not token:
        return False
    if chat_ids is None:
        chat_ids = _get_chat_ids()
    if not chat_ids:
        chat_ids = [-1003940688311]  # fallback

    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = {"chat_id": chat_ids[0], "text": text}
    if parse_mode:
        payload["parse_mode"] = parse_mode
    if reply_markup:
        payload["reply_markup"] = json.dumps(reply_markup, ensure_ascii=False)
    try:
        r = requests.post(url, json=payload, timeout=10)
        if not r.ok:
            sys_log.warning("[TelegramTpl] sendMessage HTTP %s: %s", r.status_code, r.text[:200])
            return False
        return True
    except Exception as e:
        sys_log.error("[TelegramTpl] send 失敗: %s", e)
        return False


def send_telegram_with_result(text: str, chat_ids: Optional[list] = None,
                              reply_markup: Optional[Dict[str, Any]] = None,
                              parse_mode: Optional[str] = "HTML") -> Dict[str, Any]:
    """發送 Telegram 並回傳結果明細，供 EventRouter / AIOps 使用。"""
    token = _get_bot_token()
    if not token:
        return {"ok": False, "sent": 0, "failed": 0, "chat_ids": [], "errors": ["token_missing"]}

    if chat_ids is None:
        chat_ids = _get_chat_ids()
    if not chat_ids:
        chat_ids = [-1003940688311]

    import requests

    url = f"https://api.telegram.org/bot{token}/sendMessage"
    sent = 0
    failed = 0
    errors: List[str] = []

    for chat_id in chat_ids:
        payload = {"chat_id": chat_id, "text": text}
        if parse_mode:
            payload["parse_mode"] = parse_mode
        if reply_markup:
            payload["reply_markup"] = json.dumps(reply_markup, ensure_ascii=False)
        try:
            response = requests.post(url, json=payload, timeout=10)
            if response.ok:
                sent += 1
            else:
                failed += 1
                errors.append(f"{chat_id}:HTTP {response.status_code}")
                sys_log.warning("[TelegramTpl] sendMessage chat=%s HTTP %s: %s",
                                chat_id, response.status_code, response.text[:200])
        except Exception as e:
            failed += 1
            errors.append(f"{chat_id}:{type(e).__name__}")
            sys_log.error("[TelegramTpl] send chat=%s 失敗: %s", chat_id, e)

    return {
        "ok": sent > 0 and failed == 0,
        "sent": sent,
        "failed": failed,
        "chat_ids": list(chat_ids),
        "errors": errors,
    }


def send_photo(photo_bytes: bytes, caption: str = "",
               chat_ids: Optional[list] = None,
               parse_mode: str = "HTML") -> bool:
    """發送圖片（含說明文字），供圖表報告使用"""
    import requests
    token = _get_bot_token()
    if not token or not photo_bytes:
        return False
    if chat_ids is None:
        chat_ids = _get_chat_ids()
    if not chat_ids:
        chat_ids = [-1003940688311]

    url = f"https://api.telegram.org/bot{token}/sendPhoto"
    try:
        r = requests.post(
            url,
            data={"chat_id": chat_ids[0], "caption": caption[:1024],
                  "parse_mode": parse_mode},
            files={"photo": ("chart.png", photo_bytes, "image/png")},
            timeout=30,
        )
        if not r.ok:
            sys_log.warning("[TelegramTpl] sendPhoto HTTP %s: %s", r.status_code, r.text[:200])
            return False
        return True
    except Exception as e:
        sys_log.error("[TelegramTpl] sendPhoto 失敗: %s", e)
        return False


def send_report_with_charts(text_msg: str, charts: List[Optional[bytes]],
                             chat_ids: Optional[list] = None) -> bool:
    """先發文字報告，再逐張發送圖表"""
    ok = _send_telegram_raw(text_msg, chat_ids=chat_ids)
    for i, chart in enumerate(charts):
        if chart:
            send_photo(chart, caption=f"圖表 {i+1}/{len(charts)}", chat_ids=chat_ids)
    return ok


# ══════════════════════════════════════════════════════════════════════════════
# 🚨 告警類模板
# ══════════════════════════════════════════════════════════════════════════════

def price_alert_msg(threats: List[Dict], analysis_period: str = "近48h") -> str:
    """
    競品削價告警（Hermes + NemoTron 偵測結果）
    ┌─────────────────────────────────┐
    │ 🚨 競品削價告警  ·  N 個 SKU     │
    │ ─────────────────────────────── │
    │ ⚠️ SKU名稱  MOMO $X → 競品 $Y   │
    │    價差 +X%  業績跌幅 -Y%        │
    └─────────────────────────────────┘
    """
    n = len(threats)
    high = [t for t in threats if t.get("risk") == "HIGH"]
    lines = [
        f"🚨 <b>競品削價告警</b>  ·  偵測到 <b>{n}</b> 個 SKU  [{analysis_period}]",
        f"🔴 高危：{len(high)} 個  　🟡 中危：{n - len(high)} 個",
        "─" * 32,
    ]
    for t in threats[:8]:
        risk_icon = "🔴" if t.get("risk") == "HIGH" else "🟡"
        name = str(t.get("name", t.get("sku", "")))[:22]
        gap = float(t.get("gap_pct", 0))
        delta = float(t.get("sales_7d_delta_pct", t.get("sales_delta", 0)))
        momo_p = t.get("momo_price")
        pchome_p = t.get("pchome_price")
        price_str = f"  MOMO <b>${momo_p:,.0f}</b> vs 競品 <b>${pchome_p:,.0f}</b>" \
                    if momo_p and pchome_p else ""
        lines.append(
            f"{risk_icon} <b>{name}</b>\n"
            f"   價差 <b>{gap:+.1f}%</b>  業績週跌 <b>{delta:+.1f}%</b>{price_str}"
        )
    if n > 8:
        lines.append(f"<i>…另有 {n-8} 個 SKU（查看完整報告）</i>")
    lines += ["─" * 32,
              "💡 <b>建議：</b>優先處理紅色高危品項，確認競品是否促銷或長期低價"]
    return "\n".join(lines)


def threat_alert_msg(sku: str, name: str, momo_price: float,
                     comp_price: float, gap_pct: float,
                     sales_delta: float, action: str, confidence: float) -> str:
    """單品威脅告警（NemoTron 派發）"""
    risk = "🔴 高危" if gap_pct > 15 else "🟡 中危" if gap_pct > 5 else "🟢 低危"
    return (
        f"{risk} <b>競品威脅通報</b>\n"
        f"━━━━━━━━━━━━━━━━━━━━\n"
        f"🏷️ <b>{name}</b>  <code>{sku}</code>\n\n"
        f"💴 MOMO　　<b>NT${momo_price:,.0f}</b>\n"
        f"💴 競品（PChome）<b>NT${comp_price:,.0f}</b>\n"
        f"📉 價差　　<b>{gap_pct:+.1f}%</b>（我方較貴）\n"
        f"📊 業績週變 <b>{sales_delta:+.1f}%</b>\n\n"
        f"🤖 <b>AI 建議：</b>{action}\n"
        f"📈 信心度：{confidence:.0%}\n"
        f"━━━━━━━━━━━━━━━━━━━━"
    )


def system_alert_msg(level: str, title: str, detail: str, source: str = "") -> str:
    """系統級告警（AIOps / 部署失敗等）"""
    icons = {"critical": "🚨", "error": "❌", "warning": "⚠️", "info": "ℹ️"}
    icon = icons.get(level, "⚠️")
    src = f"  [{source}]" if source else ""
    return (
        f"{icon} <b>{title}</b>{src}\n"
        f"━━━━━━━━━━━━━━━━━━━━\n"
        f"{detail[:600]}\n"
        f"━━━━━━━━━━━━━━━━━━━━\n"
        f"🕐 {datetime.now().strftime('%m/%d %H:%M')}"
    )


# ══════════════════════════════════════════════════════════════════════════════
# 📊 報告類模板（三種週期）
# ══════════════════════════════════════════════════════════════════════════════

def daily_report_header(date_str: str, revenue: float, wow: float,
                         threat_count: int, opportunity_count: int) -> str:
    """
    日報標題卡（附圖前發送的文字說明）
    """
    wow_icon = "📈" if wow >= 0 else "📉"
    wow_color = "+" if wow >= 0 else ""
    return (
        f"📊 <b>EwoooC 電商日報</b>  ·  {date_str}\n"
        f"══════════════════════════\n"
        f"💰 今日業績　　<b>NT${revenue/10000:.1f} 萬</b>\n"
        f"{wow_icon} 週同比　　　<b>{wow_color}{wow:.1f}%</b>\n"
        f"🔴 競品威脅　　<b>{threat_count}</b> 個 SKU\n"
        f"🟢 市場機會　　<b>{opportunity_count}</b> 個 SKU\n"
        f"══════════════════════════\n"
        f"🤖 <i>Hermes + NemoTron + OpenClaw 聯合分析</i>"
    )


def weekly_report_header(period: str, curr_rev: float, prev_rev: float,
                          wow: float, top_category: str) -> str:
    """週報標題卡"""
    wow_icon = "📈" if wow >= 0 else "📉"
    return (
        f"📊 <b>EwoooC 電商週報</b>  ·  {period}\n"
        f"══════════════════════════\n"
        f"💰 本週業績　　<b>NT${curr_rev/10000:.1f} 萬</b>\n"
        f"📦 前週業績　　<b>NT${prev_rev/10000:.1f} 萬</b>\n"
        f"{wow_icon} 週成長率　　<b>{wow:+.1f}%</b>\n"
        f"🏆 最強品類　　<b>{top_category}</b>\n"
        f"══════════════════════════\n"
        f"🤖 <i>OpenClaw × MCP 全景策略分析</i>"
    )


def monthly_report_header(month_str: str, revenue: float, mom: float,
                           yoy: float, top3_categories: List[str]) -> str:
    """月報標題卡"""
    mom_icon = "📈" if mom >= 0 else "📉"
    yoy_icon = "🚀" if yoy >= 10 else "📈" if yoy >= 0 else "📉"
    cats = " / ".join(top3_categories[:3])
    return (
        f"📅 <b>EwoooC 電商月報</b>  ·  {month_str}\n"
        f"══════════════════════════\n"
        f"💰 月度業績　　<b>NT${revenue/10000:.0f} 萬</b>\n"
        f"{mom_icon} 月成長率　　<b>{mom:+.1f}%</b>  {yoy_icon} 年成長 <b>{yoy:+.1f}%</b>\n"
        f"🏆 TOP 3 品類  <b>{cats}</b>\n"
        f"══════════════════════════\n"
        f"🤖 <i>OpenClaw × Hermes × MCP 月度全景洞察</i>"
    )


def report_section(icon: str, title: str, lines: List[str]) -> str:
    """通用報告節段（供日/週/月報各節使用）"""
    body = "\n".join(f"  {l}" for l in lines)
    return f"\n{icon} <b>{title}</b>\n{body}"


# ══════════════════════════════════════════════════════════════════════════════
# 💰 決策類模板
# ══════════════════════════════════════════════════════════════════════════════

def price_decision(product_name: str, product_sku: str,
                   current_price: float, suggested_price: float,
                   reason: str, insight_id: Optional[int] = None) -> tuple:
    """降 / 提價決策通知（含 Inline Keyboard）"""
    diff = current_price - suggested_price
    action_text = f"降價 NT${diff:,.0f}" if diff > 0 else \
                  f"提價 NT${-diff:,.0f}" if diff < 0 else "維持現價"
    direction = "📉" if diff > 0 else "📈" if diff < 0 else "➡️"

    message = (
        f"💰 <b>AI 定價決策建議</b>\n"
        f"━━━━━━━━━━━━━━━━━━━━\n"
        f"🏷️ <b>{product_name}</b>  <code>{product_sku}</code>\n\n"
        f"現價：<b>NT${current_price:,.0f}</b>\n"
        f"建議：<b>NT${suggested_price:,.0f}</b>  {direction} {action_text}\n\n"
        f"💡 <b>依據：</b>{reason}\n"
    )
    if insight_id:
        message += f"🔗 洞察 ID：<code>{insight_id}</code>\n"
    message += f"━━━━━━━━━━━━━━━━━━━━"

    # ADR-012: callback_data 採短 prefix（momo:pa/pr:{insight_id}）— 64-byte 安全、與 L2 handler 一致
    # 降級：若無 insight_id（不應發生，但保底），以 sku 做後綴 — sku 仍可能超長，呼叫者有責任提供 insight_id
    _cb_id = str(insight_id) if insight_id else f"sku_{product_sku}"[:40]
    keyboard = {"inline_keyboard": [[
        {"text": "✅ 確認執行", "callback_data": f"momo:pa:{_cb_id}"},
        {"text": "❌ 拒絕",   "callback_data": f"momo:pr:{_cb_id}"},
    ]]}
    return message, keyboard


def batch_decision_msg(items: List[Dict], batch_id: str) -> tuple:
    """批次定價決策（多 SKU 一次確認）"""
    lines = [f"💰 <b>批次定價決策</b>  ·  共 {len(items)} 個 SKU\n━━━━━━━━━━━━━━━━━━━━"]
    for i, item in enumerate(items[:6], 1):
        diff = item.get("current_price", 0) - item.get("suggested_price", 0)
        direction = "📉" if diff > 0 else "📈"
        lines.append(
            f"{i}. {direction} <b>{item.get('name','')[:20]}</b>\n"
            f"   ${item.get('current_price',0):,.0f} → <b>${item.get('suggested_price',0):,.0f}</b>"
        )
    if len(items) > 6:
        lines.append(f"<i>…另有 {len(items)-6} 個 SKU</i>")
    lines.append("━━━━━━━━━━━━━━━━━━━━")
    # ADR-012: 短 prefix bpa=batch_price_approve / bpr=batch_price_reject，預留 64-byte buffer
    _bid = str(batch_id)[:48]
    keyboard = {"inline_keyboard": [[
        {"text": f"✅ 全部確認（{len(items)}項）",
         "callback_data": f"momo:bpa:{_bid}"},
        {"text": "❌ 取消",
         "callback_data": f"momo:bpr:{_bid}"},
    ]]}
    return "\n".join(lines), keyboard


# ══════════════════════════════════════════════════════════════════════════════
# 🧠 Phase 11 RAG 反饋（v5.0 護欄 #1：強制晉升門檻 — Stage 4 人工驗收）
# ══════════════════════════════════════════════════════════════════════════════

def rag_feedback_keyboard(rag_query_log_id: int) -> dict:
    """產生 RAG 命中後的 👍/👎 反饋鍵盤（callback prefix 'rag_fb:'）。

    callback_data 設計（與 ADR-012 短 prefix 規範一致，<= 64 byte）:
      rag_fb:{log_id}:5  → 👍（feedback_score=5）
      rag_fb:{log_id}:1  → 👎（feedback_score=1）

    使用範例（caller 拿 RAGResult 後）:
        from services.telegram_templates import rag_feedback_keyboard
        send_message(chat_id, rag.synthesize(),
                     keyboard=rag_feedback_keyboard(rag.log_id))
    """
    _id = int(rag_query_log_id) if rag_query_log_id else 0
    return {
        'inline_keyboard': [[
            {'text': '👍 有用', 'callback_data': f'rag_fb:{_id}:5'},
            {'text': '👎 沒用', 'callback_data': f'rag_fb:{_id}:1'},
        ]],
    }


def promotion_review_keyboard(episode_id: int) -> dict:
    """蒸餾池高權重晉升人工驗收鍵盤（PromotionGate Stage 4）。

    callback_data:
      pg_ok:{episode_id} → 通過 → 寫 ai_insights
      pg_no:{episode_id} → 駁回 → rejected_human
    """
    _id = int(episode_id) if episode_id else 0
    return {
        'inline_keyboard': [[
            {'text': '✅ 通過晉升', 'callback_data': f'pg_ok:{_id}'},
            {'text': '🚫 駁回',     'callback_data': f'pg_no:{_id}'},
        ]],
    }


# ══════════════════════════════════════════════════════════════════════════════
# 🤖 系統類模板
# ══════════════════════════════════════════════════════════════════════════════

def deploy_msg(status: str, branch: str, commit: str, duration: str = "") -> str:
    """CI/CD 部署通知"""
    icons = {"success": "✅", "failed": "❌", "started": "🚀"}
    icon = icons.get(status, "ℹ️")
    dur = f"  耗時 {duration}" if duration else ""
    return (
        f"{icon} <b>部署{status.upper()}</b>{dur}\n"
        f"━━━━━━━━━━━━━━━━━━━━\n"
        f"🌿 分支：<code>{branch}</code>\n"
        f"🔖 Commit：<code>{commit[:12]}</code>\n"
        f"🕐 {datetime.now().strftime('%m/%d %H:%M')}"
    )


def heal_msg(status: str, error_type: str, target_file: str,
             commit_sha: str = "", diff_lines: int = 0) -> str:
    """AiderHeal 自動修復通知"""
    if status == "started":
        icon, title = "🔧", "AiderHeal 啟動"
    elif status == "success":
        icon, title = "✅", "AiderHeal 修復完成"
    elif status == "reverted":
        icon, title = "🔄", "AiderHeal 自動回滾"
    else:
        icon, title = "❌", "AiderHeal 失敗"

    lines = [f"{icon} <b>{title}</b>", "━━━━━━━━━━━━━━━━━━━━",
             f"🐛 錯誤類型：<code>{error_type}</code>",
             f"📄 目標檔案：<code>{target_file}</code>"]
    if commit_sha:
        lines.append(f"🔖 Commit：<code>{commit_sha[:12]}</code>")
    if diff_lines:
        lines.append(f"📝 修改行數：{diff_lines} 行")
    lines += ["━━━━━━━━━━━━━━━━━━━━",
              f"🕐 {datetime.now().strftime('%m/%d %H:%M')}"]
    return "\n".join(lines)


def meta_analysis_msg(period: str, content: str) -> str:
    """AI 系統自我審視報告"""
    return (
        f"🤖 <b>AI 系統效能自我審視</b>  ·  {period}\n"
        f"══════════════════════════\n"
        f"{content[:1200]}\n"
        f"══════════════════════════\n"
        f"<i>由 OpenClaw 定期分析 · {datetime.now().strftime('%m/%d %H:%M')}</i>"
    )


# ══════════════════════════════════════════════════════════════════════════════
# 💡 洞察類模板
# ══════════════════════════════════════════════════════════════════════════════

def triaged_alert(base_event: Dict[str, Any], tier_label: str,
                  ai_summary: str, ai_cause: Optional[str] = None,
                  ai_actions: Optional[list] = None,
                  ai_executed: Optional[list] = None) -> tuple:
    """EA L1/L2 自主執行通知（保留原有介面，升級排版）"""
    event_type = base_event.get("event_type", "alert")
    title = escape(str(base_event.get("title", "")))
    summary = escape(str(base_event.get("summary", "")))
    event_id = base_event.get("id", "unknown")
    safe_ai_summary = escape(str(ai_summary or ""))
    safe_ai_cause = escape(str(ai_cause or "")) if ai_cause else None
    safe_actions = [escape(str(a)) for a in (ai_actions or [])]
    safe_executed = [escape(str(a)) for a in (ai_executed or [])]

    lines = [
        f"⚡ <b>{tier_label} · {event_type}</b>",
        f"📌 {title}",
        "",
    ]
    if summary:
        lines += [f"🔍 <b>概要：</b>{summary}", ""]
    if safe_ai_summary:
        lines += [f"🧠 <b>AI 摘要：</b>{safe_ai_summary[:400]}", ""]
    if safe_ai_cause:
        lines += [f"💡 <b>可能原因：</b>{safe_ai_cause}", ""]
    if safe_actions:
        lines += ["<b>📋 建議行動：</b>"] + [f"  • {a}" for a in safe_actions] + [""]
    if safe_executed:
        lines += ["<b>✅ 已執行：</b>"] + [f"  • {a}" for a in safe_executed] + [""]

    trace = base_event.get("trace")
    if trace:
        lines.append(f"<pre>{trace[-400:]}</pre>")

    # ADR-012: eig=event_ignore，event_id 截斷確保 ≤ 60 bytes（留 buffer）
    _eid = str(event_id)[:52]
    keyboard = {"inline_keyboard": [
        [{"text": "🛑 忽略此事件",
          "callback_data": f"momo:eig:{_eid}"}],
    ]}
    return "\n".join(lines), keyboard


def insight_summary_msg(insights: List[Dict], period: str = "近24h") -> str:
    """AI 洞察摘要彙整（供定期推播）"""
    n = len(insights)
    lines = [
        f"💡 <b>AI 洞察摘要</b>  ·  {period}  共 {n} 筆",
        "━━━━━━━━━━━━━━━━━━━━",
    ]
    type_icons = {
        "price_alert": "🔴", "recommendation": "💰", "weekly_strategy": "📊",
        "meta_analysis": "🤖", "market_opportunity": "🟢", "mcp_cache": "🌐",
    }
    for ins in insights[:6]:
        icon = type_icons.get(ins.get("insight_type", ""), "💡")
        content_preview = str(ins.get("content", ""))[:80].replace("\n", " ")
        conf = float(ins.get("confidence", 0))
        lines.append(f"{icon} {content_preview}…  <i>({conf:.0%})</i>")
    if n > 6:
        lines.append(f"<i>…另有 {n-6} 筆洞察</i>")
    lines.append("━━━━━━━━━━━━━━━━━━━━")
    return "\n".join(lines)


# ══════════════════════════════════════════════════════════════════════════════
# 舊版相容介面（保留供現有程式碼調用）
# ══════════════════════════════════════════════════════════════════════════════

def alert(title: str, content: str, actions: Optional[list] = None) -> str:
    msg = system_alert_msg("error", title, content)
    if actions:
        msg += "\n" + "\n".join(f"  • {a}" for a in actions)
    return msg


def warning(title: str, summary: str, details: Optional[dict] = None) -> str:
    detail_str = "\n".join(f"  {k}: {v}" for k, v in (details or {}).items())
    return system_alert_msg("warning", title, summary + ("\n" + detail_str if detail_str else ""))


def info(title: str, module: str, content: str, time: Optional[Any] = None) -> str:
    t_str = f" · {time}" if time else ""
    return f"ℹ️ <b>{title}</b> [{module}]{t_str}\n\n{content}"


def success(title: str, module: str, stats: str = "") -> str:
    return f"✅ <b>{title}</b> [{module}]\n{stats}"


def report(title: str, report_type: str, period: str, content_md: str) -> str:
    icons = {"weekly_strategy": "📊", "daily": "📅", "monthly": "📆", "meta_analysis": "🤖"}
    icon = icons.get(report_type, "📋")
    return (
        f"{icon} <b>{title}</b>  ({report_type})\n"
        f"📅 期間：{period}\n"
        f"══════════════════════════\n"
        f"{content_md}"
    )


def _send_telegram(msg: str, chat_ids: Optional[list] = None,
                   reply_markup: Optional[Dict[str, Any]] = None) -> bool:
    return _send_telegram_raw(msg, chat_ids=chat_ids, reply_markup=reply_markup)


# ══════════════════════════════════════════════════════════════════════════════
# LLM Token 日報模板（Operation Ollama-First v5.0 — Phase 1 收尾）
# 對應 services/token_report_service.py
# ══════════════════════════════════════════════════════════════════════════════

# Telegram 單訊息上限（保險絲，sendMessage HTML 上限為 4096 字元）
_DAILY_TOKEN_REPORT_MAX_CHARS = 4096


def daily_token_report(report_html: str,
                       footer_url: Optional[str] = None) -> str:
    """LLM Token 日報訊息包裝（HTML parse_mode）。

    本函數只負責「附上 footer + 截斷至 Telegram 上限」；報表本體由
    services/token_report_service.generate_daily_report() 產出，已含 HTML escape。

    Args:
        report_html: 已 escape 的 HTML 報表字串
        footer_url:  選填 admin 後台連結，會自動 escape

    Returns:
        ≤ 4096 字元的 HTML 字串，安全送 Telegram
    """
    body = report_html or ""
    if footer_url:
        body = f"{body}\n📎 <a href=\"{escape(footer_url)}\">詳細日誌</a>"

    if len(body) <= _DAILY_TOKEN_REPORT_MAX_CHARS:
        return body

    # 超長 → 截斷並加省略尾（保留 80 字給 trailing notice）
    truncated = body[: _DAILY_TOKEN_REPORT_MAX_CHARS - 80]
    return truncated + "\n\n... <i>(訊息過長已截斷；完整內容存於 ai_insights)</i>"


# ══════════════════════════════════════════════════════════════════════════════
# 決策回執模板（L2 / L3 按鈕點擊後編輯原訊息使用）
# ADR-012 Phase 4：審計留痕 — 操作者、時間、action、結果
# ══════════════════════════════════════════════════════════════════════════════

# Asia/Taipei（UTC+8）— 容器可能跑 UTC，這裡手動偏移避免依賴 tzdata
_TAIPEI_UTC_OFFSET_HOURS = 8


def _now_taipei_hhmm() -> str:
    """回傳 Asia/Taipei 的 HH:MM（容器 TZ 不可靠，手動加 8h）。"""
    try:
        from datetime import timedelta, timezone
        tz = timezone(timedelta(hours=_TAIPEI_UTC_OFFSET_HOURS))
        return datetime.now(tz).strftime("%H:%M")
    except Exception:
        return datetime.now().strftime("%H:%M")


def _html_escape(s: Any) -> str:
    """最小 HTML 轉義（Telegram HTML parse_mode 允許 <b><i><code><pre><a>，
    故只轉 < > & 避免使用者輸入破版）。"""
    text = "" if s is None else str(s)
    return (text.replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;"))


def decision_result(original_text: str, action: str, operator: str,
                    note: Optional[str] = None, **extras: Any) -> str:
    """L2 單品定價決策回執（approve / reject 後編輯原訊息）。

    Args:
        original_text: 原 Telegram 訊息文字（query.message.text）
        action: "approve" / "reject"
        operator: 操作者顯示名稱（user.full_name 或 id_<tg_id>）
        note: 選填補充說明（例：訓練保守策略）
        **extras: 前向相容預留（目前忽略）

    Returns:
        原訊息 + 分隔線 + 稽核區塊（HTML parse_mode 安全）
    """
    act = (action or "").lower()
    if act == "approve":
        icon, label = "✅", "已執行"
    elif act == "reject":
        icon, label = "❌", "已拒絕"
    else:
        icon, label = "ℹ️", f"已處理（{_html_escape(action)}）"

    ts = _now_taipei_hhmm()
    op_esc = _html_escape(operator or "unknown")
    base = original_text or ""
    lines = [
        base,
        "",
        "━━━━━━━━━━━━━━━━━━━━",
        f"{icon} <b>{label}</b>  by <b>{op_esc}</b>  at {ts}",
    ]
    if note:
        lines.append(f"📝 {_html_escape(note)}")
    return "\n".join(lines)


def ops_action_result(original_text: str, action: str, operator: str,
                      result: Optional[Dict[str, Any]] = None,
                      **extras: Any) -> str:
    """L3 運維決策回執（pause1h / pause6h / retry / resume 執行後編輯原訊息）。

    Args:
        original_text: 原 Telegram 訊息文字
        action: "pause1h" / "pause6h" / "retry" / "resume" / 其他
        operator: 操作者顯示名稱
        result: OPS_ACTIONS 函數回傳 dict，常見欄位：
                  status: "ok" / "success" / "error" / "skipped"
                  error: 錯誤訊息（若 status=error）
                  message / detail: 成功訊息
                  task_name, duration_min 等
        **extras: 前向相容預留

    Returns:
        原訊息 + 分隔線 + 運維執行結果區塊
    """
    action_labels = {
        "pause1h": "暫停 1 小時",
        "pause6h": "暫停 6 小時",
        "retry":   "立即重試",
        "resume":  "恢復執行",
        "execute": "執行",
        "skip":    "略過",
        "rollback": "回滾",
    }
    act_label = action_labels.get((action or "").lower(), _html_escape(action or "未知動作"))

    result = result or {}
    status = str(result.get("status", "")).lower()
    if status in ("ok", "success", "done"):
        status_icon, status_text = "✅", "成功"
    elif status in ("error", "failed", "fail"):
        status_icon, status_text = "❌", "失敗"
    elif status == "skipped":
        status_icon, status_text = "⏭️", "已略過"
    else:
        status_icon, status_text = "ℹ️", status or "已提交"

    ts = _now_taipei_hhmm()
    op_esc = _html_escape(operator or "unknown")

    # 擷取結果訊息（err > message > detail）
    detail_msg: Optional[str] = None
    for key in ("error", "message", "detail"):
        if result.get(key):
            detail_msg = str(result[key])[:300]
            break

    lines = [
        original_text or "",
        "",
        "━━━━━━━━━━━━━━━━━━━━",
        f"🛠️ <b>運維動作：</b>{act_label}",
        f"👤 操作員：<b>{op_esc}</b>  🕐 {ts}",
        f"{status_icon} <b>執行結果：</b>{_html_escape(status_text)}",
    ]
    if detail_msg:
        lines.append(f"📋 {_html_escape(detail_msg)}")

    # 額外關鍵欄位（task_name / duration_min 等）
    task_name = result.get("task_name")
    if task_name:
        lines.append(f"📌 任務：<code>{_html_escape(task_name)}</code>")
    duration_min = result.get("duration_min")
    if duration_min:
        lines.append(f"⏱️ 時長：{_html_escape(duration_min)} 分鐘")

    return "\n".join(lines)
