"""
services/ppt_generator.py
OpenClaw 簡報生成器 — 精品深色主題 + 原生圖表版 (2026-04-20 v2)

函數清單（與 openclaw_bot_routes.py 呼叫約定一致）:
  check_pptx_available()
  generate_daily_ppt(date_str, db_data, ai_text) -> str       # 4頁
  generate_weekly_ppt(db_data, ai_text) -> str                # 5頁
  generate_monthly_ppt(yr, mo, db_data, ai_text) -> str       # 5頁
  generate_strategy_ppt(date_str, db_data, ai_text) -> str    # 5頁
  generate_competitor_ppt(period_label, db_data, ai_text) -> str  # 4頁
  generate_promo_ppt(promo_label, data, ai_text) -> str       # 5頁

設計規格：
  - 16:9  (33.87cm × 19.05cm)
  - 封面：深海藍背景 #0D1B2A、橘色品牌條 #FF5722、白色大標
  - 數據頁：白底 KPI 卡 + python-pptx 原生圖表
  - AI 頁：深色背景 + 白色文字
  - 頁眉：橘色標題帶 #FF5722
  - 頁腳：♥ Powered by OpenClaw（深灰 #37474F）

圖表對應（來源：web/templates）:
  daily     → 近7日業績柱狀圖（參考 daily_sales.html trendChart）
  weekly    → 7日業績柱狀圖 + TOP10 商品表
  monthly   → 品類橫條圖（參考 monthly_summary_analysis.html）+ KPI + TOP10
  strategy  → 策略矩陣分佈柱狀圖 + 行動清單
  competitor → 橫條圖 + 比較表（維持現有）
  promo     → 促銷 vs 對比期雙柱圖 + KPI 對比 + TOP商品
"""
import os
import uuid
from collections import defaultdict
from datetime import datetime
from pathlib import Path

REPORTS_DIR = Path(os.environ.get("REPORTS_DIR", "/app/data/reports"))
REPORTS_DIR.mkdir(parents=True, exist_ok=True)

# ── 調色盤 ────────────────────────────────────────────────────────────────────
_BG_DARK    = "0D1B2A"
_BRAND_OG   = "FF5722"
_BRAND_OG2  = "E65100"
_WHITE      = "FFFFFF"
_LIGHT_GRAY = "F5F5F5"
_DARK_TEXT  = "212121"
_SUBTEXT    = "757575"
_FOOTER_BG  = "37474F"
_BLUE_KPI   = "1565C0"
_GREEN_KPI  = "2E7D32"
_RED_WARN   = "C62828"
_BAR_PCHOME = "EF5350"
_BAR_MOMO   = "66BB6A"
_BAR_TIE    = "FFA726"
_BAR_MISS   = "9E9E9E"

_STRAT_COLORS = {
    '加碼': _BRAND_OG,
    '機會': _BLUE_KPI,
    '收割': _GREEN_KPI,
    '觀察': _FOOTER_BG,
    '持穩': "90A4AE",
    '其他': _SUBTEXT,
}
_STRAT_ORDER = ['加碼', '機會', '收割', '觀察', '持穩']


def check_pptx_available() -> bool:
    try:
        import pptx  # noqa: F401
        return True
    except ImportError:
        return False


def _new_path(kind: str) -> str:
    rid = uuid.uuid4().hex[:8]
    return str(REPORTS_DIR / f"ocbot_{kind}_{rid}.pptx")


# ── 基礎繪圖工具 ──────────────────────────────────────────────────────────────
def _rgb(hex6: str):
    from pptx.dml.color import RGBColor
    return RGBColor(int(hex6[0:2], 16), int(hex6[2:4], 16), int(hex6[4:6], 16))


def _emu(cm: float):
    from pptx.util import Cm
    return Cm(cm)


def _fill_solid(shape, hex6: str):
    shape.fill.solid()
    shape.fill.fore_color.rgb = _rgb(hex6)


def _add_rect(slide, l, t, w, h, fill_hex, line_hex=None):
    from pptx.util import Pt
    s = slide.shapes.add_shape(1, _emu(l), _emu(t), _emu(w), _emu(h))
    _fill_solid(s, fill_hex)
    if line_hex:
        s.line.color.rgb = _rgb(line_hex)
        s.line.width = Pt(0.5)
    else:
        s.line.fill.background()
    return s


def _add_text(slide, text, l, t, w, h,
              bold=False, size=14, color=_WHITE,
              align="left", valign="top", wrap=True):
    from pptx.util import Pt
    from pptx.enum.text import PP_ALIGN
    from pptx.dml.color import RGBColor

    txb = slide.shapes.add_textbox(_emu(l), _emu(t), _emu(w), _emu(h))
    tf  = txb.text_frame
    tf.word_wrap = wrap
    if valign == "middle":
        from pptx.enum.text import MSO_ANCHOR
        tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    p = tf.paragraphs[0]
    p.text = text
    run = p.runs[0]
    run.font.bold = bold
    run.font.size = Pt(size)
    run.font.color.rgb = RGBColor(
        int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
    if align == "center":
        p.alignment = PP_ALIGN.CENTER
    elif align == "right":
        p.alignment = PP_ALIGN.RIGHT
    return txb


def _add_footer(slide, prs_w_cm=33.87):
    _add_rect(slide, 0, 13.5, prs_w_cm, 0.79, _FOOTER_BG)
    _add_text(slide, "♥  Powered by OpenClaw",
              prs_w_cm - 6, 13.55, 5.8, 0.7,
              size=8, color=_BRAND_OG, align="right")


def _add_header(slide, title_text, prs_w_cm=33.87):
    _add_rect(slide, 0, 0, prs_w_cm, 1.5, _BRAND_OG)
    _add_text(slide, title_text, 0.5, 0.1, prs_w_cm - 1, 1.3,
              bold=True, size=20, color=_WHITE, valign="middle")


def _kpi_card(slide, l, t, w, h, fill, label, value, sub=""):
    _add_rect(slide, l, t, w, h, fill)
    _add_text(slide, label, l + 0.2, t + 0.2, w - 0.4, 0.7, size=10, color=_WHITE)
    _add_text(slide, value, l + 0.2, t + 0.7, w - 0.4, 1.3,
              bold=True, size=28, color=_WHITE, align="center")
    if sub:
        _add_text(slide, sub, l + 0.2, t + h - 0.65, w - 0.4, 0.6, size=9, color=_WHITE)


def _horiz_bar(slide, l, t, h_row, label, value, total, fill_hex, max_w=14.0):
    pct   = value / total if total else 0
    bar_w = max(0.2, pct * max_w)
    _add_text(slide, label, l, t + 0.05, 3.0, h_row - 0.1, size=10, color=_DARK_TEXT)
    _add_rect(slide, l + 3.1, t + 0.1, bar_w, h_row - 0.2, fill_hex)
    _add_text(slide, f"{value}件 ({pct*100:.0f}%)",
              l + 3.2 + bar_w, t + 0.05, 4.0, h_row - 0.1, size=10, color=_DARK_TEXT)


# ── 原生圖表工具 ──────────────────────────────────────────────────────────────
def _add_column_chart(slide, l, t, w, h, categories, series_list,
                      bar_colors=None, title="", raw_values=False):
    """垂直柱狀圖（時間趨勢、分佈）。
    series_list: [(name, [values]), ...]
    raw_values=False 時自動將值除以 10000（萬元顯示）
    """
    from pptx.chart.data import ChartData
    from pptx.enum.chart import XL_CHART_TYPE
    from pptx.util import Pt
    try:
        from pptx.enum.chart import XL_LEGEND_POSITION
    except ImportError:
        XL_LEGEND_POSITION = None

    if not categories or not series_list:
        return None

    cd = ChartData()
    cd.categories = [str(c) for c in categories]
    for name, vals in series_list:
        if raw_values:
            cd.add_series(name, tuple(float(v) if v else 0 for v in vals))
        else:
            cd.add_series(name, tuple(round(float(v) / 10000, 1) if v else 0 for v in vals))

    cf    = slide.shapes.add_chart(
        XL_CHART_TYPE.COLUMN_CLUSTERED,
        _emu(l), _emu(t), _emu(w), _emu(h), cd
    )
    chart = cf.chart

    try:
        chart.plot_area.format.fill.background()
        chart.chart_area.format.fill.background()
    except Exception:
        pass

    chart.has_title = bool(title)
    if title:
        chart.chart_title.text_frame.text = title
        try:
            chart.chart_title.text_frame.paragraphs[0].runs[0].font.size = Pt(12)
        except Exception:
            pass

    chart.has_legend = len(series_list) > 1
    if chart.has_legend and XL_LEGEND_POSITION:
        try:
            chart.legend.position = XL_LEGEND_POSITION.BOTTOM
            chart.legend.include_in_layout = False
        except Exception:
            pass

    try:
        chart.value_axis.number_format = '#,##0'
        chart.value_axis.has_major_gridlines = True
        chart.category_axis.has_major_gridlines = False
    except Exception:
        pass

    if bar_colors:
        for i, series in enumerate(chart.series):
            if i < len(bar_colors):
                try:
                    series.format.fill.solid()
                    series.format.fill.fore_color.rgb = _rgb(bar_colors[i])
                except Exception:
                    pass

    return chart


def _add_horiz_chart(slide, l, t, w, h, categories, series_list,
                     bar_colors=None, title="", raw_values=False):
    """水平柱狀圖（排行榜、品類比較）。"""
    from pptx.chart.data import ChartData
    from pptx.enum.chart import XL_CHART_TYPE
    from pptx.util import Pt
    try:
        from pptx.enum.chart import XL_LEGEND_POSITION
    except ImportError:
        XL_LEGEND_POSITION = None

    if not categories or not series_list:
        return None

    cd = ChartData()
    cd.categories = [str(c)[:20] for c in categories]
    for name, vals in series_list:
        if raw_values:
            cd.add_series(name, tuple(float(v) if v else 0 for v in vals))
        else:
            cd.add_series(name, tuple(round(float(v) / 10000, 1) if v else 0 for v in vals))

    cf    = slide.shapes.add_chart(
        XL_CHART_TYPE.BAR_CLUSTERED,
        _emu(l), _emu(t), _emu(w), _emu(h), cd
    )
    chart = cf.chart

    try:
        chart.plot_area.format.fill.background()
        chart.chart_area.format.fill.background()
    except Exception:
        pass

    chart.has_title = bool(title)
    if title:
        chart.chart_title.text_frame.text = title
        try:
            chart.chart_title.text_frame.paragraphs[0].runs[0].font.size = Pt(12)
        except Exception:
            pass

    chart.has_legend = len(series_list) > 1
    if chart.has_legend and XL_LEGEND_POSITION:
        try:
            chart.legend.position = XL_LEGEND_POSITION.BOTTOM
            chart.legend.include_in_layout = False
        except Exception:
            pass

    try:
        chart.value_axis.number_format = '#,##0'
    except Exception:
        pass

    if bar_colors:
        for i, series in enumerate(chart.series):
            if i < len(bar_colors):
                try:
                    series.format.fill.solid()
                    series.format.fill.fore_color.rgb = _rgb(bar_colors[i])
                except Exception:
                    pass

    return chart


# ── 封面頁 ────────────────────────────────────────────────────────────────────
def _cover_slide(prs, big_title: str, sub1: str, sub2: str = ""):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    W = 33.87

    _add_rect(slide, 0, 0, W, 19.05, _BG_DARK)
    _add_rect(slide, 0, 0, 0.6, 19.05, _BRAND_OG)
    _add_rect(slide, 1.2, 1.2, 3.5, 0.7, _BRAND_OG)
    _add_text(slide, "OPENCLAW", 1.3, 1.22, 3.3, 0.66,
              bold=True, size=11, color=_WHITE, align="center", valign="middle")
    _add_text(slide, big_title, 1.2, 2.3, 20, 3.5, bold=True, size=38, color=_WHITE)
    _add_text(slide, sub1, 1.2, 6.0, 22, 0.9, size=14, color="BDBDBD")
    if sub2:
        _add_text(slide, sub2, 1.2, 7.0, 22, 0.8, size=12, color="9E9E9E")
    _add_rect(slide, W - 0.8, 0, 0.8, 19.05, _BRAND_OG2)
    _add_footer(slide, W)
    return slide


# ── 通用商品表格投影片 ─────────────────────────────────────────────────────────
def _product_table_slide(prs, header_text, products, W=33.87):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(slide, 0, 0, W, 19.05, _WHITE)
    _add_header(slide, header_text)

    _add_rect(slide, 0.5, 1.7, W - 1, 0.65, _BRAND_OG)
    _add_text(slide, "#",    0.6,       1.73, 1.5,      0.59, bold=True, size=10, color=_WHITE, align="center")
    _add_text(slide, "商品名稱", 2.3,   1.73, W - 11.5, 0.59, bold=True, size=10, color=_WHITE)
    _add_text(slide, "業績", W - 8.5,   1.73, 5.5,      0.59, bold=True, size=10, color=_WHITE, align="right")
    _add_text(slide, "訂單", W - 2.8,   1.73, 2.2,      0.59, bold=True, size=9,  color=_WHITE, align="right")

    for i, p in enumerate(products[:10]):
        bg    = _LIGHT_GRAY if i % 2 == 0 else _WHITE
        row_t = 2.5 + i * 0.88
        _add_rect(slide, 0.5, row_t, W - 1, 0.85, bg)
        _add_text(slide, str(i + 1),
                  0.6, row_t + 0.1, 1.5, 0.7, size=10, color=_DARK_TEXT, align="center")
        _add_text(slide, str(p.get('name', ''))[:50],
                  2.3, row_t + 0.1, W - 11.5, 0.7, size=10, color=_DARK_TEXT)
        _add_text(slide, f"NT${float(p.get('revenue', 0)):,.0f}",
                  W - 8.5, row_t + 0.1, 5.5, 0.7, size=10, color=_DARK_TEXT, align="right")
        ord_v = p.get('orders', p.get('order_count', ''))
        if ord_v:
            _add_text(slide, f"{int(ord_v):,}",
                      W - 2.8, row_t + 0.1, 2.2, 0.7, size=9, color=_SUBTEXT, align="right")

    _add_footer(slide, W)
    return slide


# ── 日報 PPT（4頁）────────────────────────────────────────────────────────────
def generate_daily_ppt(date_str: str, db_data, ai_text: str) -> str:
    """P1封面 P2 KPI+TOP5 P3 近7日業績走勢圖 P4 AI洞察"""
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    s  = db_data.get("sales", {})       if isinstance(db_data, dict) else {}
    tp = db_data.get("top_products", []) if isinstance(db_data, dict) else []
    wk = db_data.get("weekly", [])       if isinstance(db_data, dict) else []

    rev  = float(s.get("revenue", 0))
    ord_ = int(s.get("orders", 0))
    gm   = float(s.get("gross_margin", 0))
    aov  = float(s.get("avg_order", 0))

    # P1: 封面
    _cover_slide(prs,
        f"日報  {date_str}",
        f"業績 NT${rev:,.0f}（{rev/10000:.1f}萬）｜訂單 {ord_:,} 筆",
        f"毛利率 {gm:.1f}%　客單價 NT${aov:,.0f}　生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}",
    )

    # P2: KPI 卡 + TOP5
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"業績 KPI — {date_str}")
    kpis = [
        (_BLUE_KPI,  "總業績", f"NT${rev/10000:.1f}萬", ""),
        (_GREEN_KPI, "總訂單", f"{ord_:,} 筆",           ""),
        (_BRAND_OG2, "毛利率", f"{gm:.1f}%",             ""),
        (_FOOTER_BG, "客單價", f"NT${aov:,.0f}",         ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(kpis):
        _kpi_card(s2, i * (7.4 + 0.4) + 0.5, 1.8, 7.4, 3.0, col, lbl, val, sub)
    _add_rect(s2, 0.5, 5.2, W - 1, 0.7, _BRAND_OG)
    _add_text(s2, "🏆 TOP 5 熱銷商品", 0.7, 5.25, W - 1.4, 0.6,
              bold=True, size=13, color=_WHITE)
    for i, p in enumerate(tp[:5]):
        bg    = _LIGHT_GRAY if i % 2 == 0 else _WHITE
        rev_p = float(p.get("revenue", 0))
        _add_rect(s2, 0.5, 6.0 + i * 0.85, W - 1, 0.82, bg)
        _add_text(s2, f"{i+1}. {str(p.get('name',''))[:45]}",
                  0.7, 6.05 + i * 0.85, W - 9, 0.72, size=10, color=_DARK_TEXT)
        _add_text(s2, f"NT${rev_p:,.0f}",
                  W - 7.5, 6.05 + i * 0.85, 5, 0.72, size=10, color=_DARK_TEXT, align="right")
    _add_footer(s2, W)

    # P3: 近7日業績走勢圖（來源：daily_sales.html trendChart）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, "近 7 日業績走勢（萬元）")
    if wk:
        dates = [str(w.get('date', ''))[-5:] for w in wk]
        revs  = [float(w.get('revenue', 0)) for w in wk]
        _add_column_chart(s3, 0.8, 1.8, W - 1.6, 11.0,
                          dates, [("日業績（萬元）", revs)],
                          bar_colors=[_BRAND_OG])
        # 加總標注
        total_7d = sum(revs)
        _add_text(s3, f"近7日合計：NT${total_7d/10000:.1f}萬",
                  0.8, 13.0, 12, 0.5, size=10, color=_SUBTEXT)
    else:
        _add_text(s3, "（近 7 日業績資料不足）",
                  2, 7, 20, 2, size=14, color=_SUBTEXT, align="center")
    _add_footer(s3, W)

    # P4: AI 洞察
    s4 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s4, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s4, "AI 洞察分析")
    _add_text(s4, ai_text or "（AI 分析生成中）",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s4, W)

    path = _new_path("daily")
    prs.save(path)
    return path


# ── 週報 PPT（5頁）────────────────────────────────────────────────────────────
def generate_weekly_ppt(db_data, ai_text: str) -> str:
    """P1封面 P2 KPI摘要 P3 7日業績柱狀圖 P4 TOP10商品 P5 AI洞察"""
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    wk  = db_data.get('weekly', [])       if isinstance(db_data, dict) else []
    tp  = db_data.get('top_products', []) if isinstance(db_data, dict) else []

    total_rev = sum(float(w.get('revenue', 0)) for w in wk)
    total_ord = sum(int(w.get('orders', 0))    for w in wk)
    best_day  = max(wk, key=lambda w: float(w.get('revenue', 0)), default={})
    avg_daily = total_rev / len(wk) if wk else 0

    # P1: 封面
    _cover_slide(prs, "週報分析",
                 f"近 7 日業績 NT${total_rev:,.0f}（{total_rev/10000:.1f}萬）",
                 f"生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}")

    # P2: KPI 摘要
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, "週報 KPI 摘要 — 近 7 日")
    kpis = [
        (_BLUE_KPI,  "週業績",   f"NT${total_rev/10000:.1f}萬",         ""),
        (_GREEN_KPI, "週訂單",   f"{total_ord:,} 筆" if total_ord else "—", ""),
        (_BRAND_OG2, "日均業績", f"NT${avg_daily/10000:.1f}萬",          ""),
        (_FOOTER_BG, "最佳單日", f"NT${float(best_day.get('revenue',0))/10000:.1f}萬",
         best_day.get('date', '')[-5:] if best_day else ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(kpis):
        _kpi_card(s2, i * 7.8 + 0.5, 1.8, 7.4, 3.5, col, lbl, val, sub)
    _add_footer(s2, W)

    # P3: 7日業績柱狀圖（參考 daily_sales.html trendChart）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, "近 7 日業績走勢圖（萬元）")
    if wk:
        dates = [str(w.get('date', ''))[-5:] for w in wk]
        revs  = [float(w.get('revenue', 0)) for w in wk]
        _add_column_chart(s3, 0.8, 1.8, W - 1.6, 11.0,
                          dates, [("日業績（萬元）", revs)],
                          bar_colors=[_BLUE_KPI])
        # 逐日數值標注（文字輔助）
        max_rev = max(revs) if revs else 1
        for j, (d, r) in enumerate(zip(dates, revs)):
            color = _BRAND_OG if r == max_rev else _SUBTEXT
            _add_text(s3, f"{r/10000:.1f}",
                      0.8 + j * ((W - 1.6) / max(len(dates), 1)),
                      12.9, (W - 1.6) / max(len(dates), 1) - 0.1, 0.5,
                      size=8, color=color, align="center")
    else:
        _add_text(s3, "（近 7 日業績資料不足）",
                  2, 7, 20, 2, size=14, color=_SUBTEXT, align="center")
    _add_footer(s3, W)

    # P4: TOP10 商品表
    _product_table_slide(prs, "週報 TOP 10 熱銷商品", tp)

    # P5: AI 洞察
    s5 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s5, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s5, "週報 AI 洞察")
    _add_text(s5, ai_text or "(暫無 AI 分析)",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s5, W)

    path = _new_path("weekly")
    prs.save(path)
    return path


# ── 月報 PPT（5頁）────────────────────────────────────────────────────────────
def generate_monthly_ppt(yr, mo, db_data, ai_text: str) -> str:
    """P1封面 P2 KPI P3 品類橫條圖 P4 TOP10商品 P5 AI洞察"""
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    ms       = db_data.get('monthly', {}) if isinstance(db_data, dict) else {}
    rev      = float(ms.get('revenue', 0))
    ord_     = int(ms.get('orders', 0))
    gm       = float(ms.get('gross_margin', 0))
    aov      = float(ms.get('avg_order', rev / ord_ if ord_ else 0))
    top_cats = ms.get('top_categories', [])
    top_prod = ms.get('top_products', [])

    # P1: 封面
    _cover_slide(prs, f"月報  {yr}/{mo:02d}", f"{yr} 年 {mo} 月業績月報",
                 f"業績 NT${rev:,.0f}（{rev/10000:.1f}萬）｜生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}")

    # P2: KPI 卡
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"月報 KPI — {yr}/{mo:02d}")
    kpis = [
        (_BLUE_KPI,  "月業績", f"NT${rev/10000:.1f}萬", ""),
        (_GREEN_KPI, "總訂單", f"{ord_:,} 筆",           ""),
        (_BRAND_OG2, "毛利率", f"{gm:.1f}%",             ""),
        (_FOOTER_BG, "客單價", f"NT${aov:,.0f}",         ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(kpis):
        _kpi_card(s2, i * 7.8 + 0.5, 1.8, 7.4, 3.5, col, lbl, val, sub)
    _add_footer(s2, W)

    # P3: 品類業績橫條圖（參考 monthly_summary_analysis.html vendorRankingChart）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, f"品類業績排行 TOP 8 — {yr}/{mo:02d}（萬元）")
    if top_cats:
        cats = [c.get('cat', '')[:15] for c in top_cats[:8]]
        revs = [float(c.get('revenue', 0)) for c in top_cats[:8]]
        _add_horiz_chart(s3, 0.5, 1.8, W - 1, 11.3,
                         cats, [("業績（萬元）", revs)],
                         bar_colors=[_BLUE_KPI])
        # 最高品類業績備注
        if top_cats:
            best = top_cats[0]
            _add_text(s3,
                      f"最高：{best.get('cat','')}  NT${float(best.get('revenue',0)):,.0f}",
                      0.8, 13.15, 18, 0.5, size=10, color=_SUBTEXT)
    else:
        _add_text(s3, "（本月無品類分佈資料）",
                  2, 7, 20, 2, size=14, color=_SUBTEXT, align="center")
    _add_footer(s3, W)

    # P4: TOP10 商品
    _product_table_slide(prs, f"熱銷商品 TOP 10 — {yr}/{mo:02d}", top_prod)

    # P5: AI 洞察
    s5 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s5, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s5, f"月報 AI 洞察 — {yr}/{mo:02d}")
    _add_text(s5, ai_text or "(暫無 AI 分析)",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s5, W)

    path = _new_path("monthly")
    prs.save(path)
    return path


# ── 策略報告 PPT（5頁）────────────────────────────────────────────────────────
def generate_strategy_ppt(date_str: str, db_data, ai_text: str) -> str:
    """P1封面 P2 KPI+TOP5 P3 策略矩陣柱狀圖+說明 P4 行動清單 P5 AI洞察"""
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    s      = db_data.get("sales", {})       if isinstance(db_data, dict) else {}
    tp     = db_data.get("top_products", []) if isinstance(db_data, dict) else []
    strat  = db_data.get("strategy", [])     if isinstance(db_data, dict) else []
    period = db_data.get("period_label", date_str) if isinstance(db_data, dict) else date_str

    rev  = float(s.get("revenue", 0))
    ord_ = int(s.get("orders", 0))
    gm   = float(s.get("gross_margin", 0))
    aov  = float(s.get("avg_order", 0))

    # 策略分佈統計
    strat_agg = defaultdict(lambda: {'count': 0, 'revenue': 0.0})
    for item in strat:
        k = item.get('strategy', '其他')
        strat_agg[k]['count']   += 1
        strat_agg[k]['revenue'] += float(item.get('revenue', 0))

    # P1: 封面
    _cover_slide(prs, f"策略報告\n{period}",
                 f"業績 NT${rev:,.0f}（{rev/10000:.1f}萬）",
                 f"生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}")

    # P2: KPI + TOP5
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"策略 KPI — {period}")
    kpis = [
        (_BLUE_KPI,  "總業績", f"NT${rev/10000:.1f}萬", ""),
        (_GREEN_KPI, "總訂單", f"{ord_:,} 筆",           ""),
        (_BRAND_OG2, "毛利率", f"{gm:.1f}%",             ""),
        (_FOOTER_BG, "客單價", f"NT${aov:,.0f}",         ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(kpis):
        _kpi_card(s2, i * 7.8 + 0.5, 1.8, 7.4, 3.0, col, lbl, val, sub)
    _add_rect(s2, 0.5, 5.2, W - 1, 0.65, _BRAND_OG)
    _add_text(s2, "🏆 TOP 熱銷商品", 0.7, 5.25, W - 1.4, 0.55,
              bold=True, size=12, color=_WHITE)
    for i, p in enumerate(tp[:5]):
        bg = _LIGHT_GRAY if i % 2 == 0 else _WHITE
        _add_rect(s2, 0.5, 6.0 + i * 0.8, W - 1, 0.78, bg)
        _add_text(s2, f"{i+1}. {str(p.get('name',''))[:45]}",
                  0.7, 6.04 + i * 0.8, W - 9, 0.7, size=9, color=_DARK_TEXT)
        _add_text(s2, f"NT${float(p.get('revenue',0)):,.0f}",
                  W - 7.5, 6.04 + i * 0.8, 5, 0.7, size=9, color=_DARK_TEXT, align="right")
    _add_footer(s2, W)

    # P3: 策略矩陣分佈（參考 monthly_summary_analysis.html BCG Matrix 概念）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, f"策略矩陣分佈 — {period}")
    if strat_agg:
        order  = [k for k in _STRAT_ORDER if k in strat_agg] + \
                 [k for k in strat_agg if k not in _STRAT_ORDER]
        cnts   = [strat_agg[k]['count'] for k in order]
        colors = [_STRAT_COLORS.get(k, _SUBTEXT) for k in order]

        # 左側柱狀圖（商品件數）
        _add_column_chart(s3, 0.8, 1.8, W * 0.55 - 0.8, 10.8,
                          order, [("商品數（件）", cnts)],
                          bar_colors=colors, raw_values=True)

        # 右側文字說明清單
        _add_text(s3, "策略說明", W * 0.55 + 0.3, 1.8, W * 0.44 - 0.5, 0.6,
                  bold=True, size=12, color=_DARK_TEXT)
        desc_t = 2.55
        for k in order:
            v     = strat_agg[k]
            color = _STRAT_COLORS.get(k, _SUBTEXT)
            _add_rect(s3, W * 0.55 + 0.3, desc_t + 0.05, 0.35, 0.45, color)
            _add_text(s3,
                      f"{k}  {v['count']} 件  NT${v['revenue']/10000:.1f}萬",
                      W * 0.55 + 0.75, desc_t, W * 0.44 - 0.85, 0.55,
                      size=11, color=_DARK_TEXT)
            desc_t += 0.68
    else:
        _add_text(s3, "（無策略分析資料）",
                  2, 7, 20, 2, size=14, color=_SUBTEXT, align="center")
    _add_footer(s3, W)

    # P4: 策略行動清單（依策略優先序排列）
    s4 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s4, 0, 0, W, 19.05, _WHITE)
    _add_header(s4, f"策略行動清單 — {period}")
    _add_rect(s4, 0.5, 1.7, W - 1, 0.65, _BRAND_OG)
    _add_text(s4, "#",    0.6,      1.73, 1.2,      0.59, bold=True, size=10, color=_WHITE, align="center")
    _add_text(s4, "商品名稱", 2.0,  1.73, W - 14,   0.59, bold=True, size=10, color=_WHITE)
    _add_text(s4, "策略",    W - 11, 1.73, 3.5,      0.59, bold=True, size=10, color=_WHITE, align="center")
    _add_text(s4, "業績",    W - 7,  1.73, 5,        0.59, bold=True, size=10, color=_WHITE, align="right")
    _add_text(s4, "毛利率",  W - 2,  1.73, 1.5,      0.59, bold=True, size=9,  color=_WHITE, align="right")

    sorted_strat = sorted(
        strat,
        key=lambda x: (
            _STRAT_ORDER.index(x.get('strategy', '')) if x.get('strategy') in _STRAT_ORDER else 99,
            -float(x.get('revenue', 0))
        )
    )
    for i, item in enumerate(sorted_strat[:10]):
        bg  = _LIGHT_GRAY if i % 2 == 0 else _WHITE
        sk  = item.get('strategy', '其他')
        sc  = _STRAT_COLORS.get(sk, _SUBTEXT)
        row_t = 2.5 + i * 0.88
        _add_rect(s4, 0.5, row_t, W - 1, 0.85, bg)
        _add_text(s4, str(i + 1),
                  0.6, row_t + 0.1, 1.2, 0.7, size=10, color=_DARK_TEXT, align="center")
        _add_text(s4, str(item.get('name', ''))[:48],
                  2.0, row_t + 0.1, W - 14, 0.7, size=9, color=_DARK_TEXT)
        _add_rect(s4, W - 11, row_t + 0.15, 3.0, 0.55, sc)
        _add_text(s4, sk, W - 11, row_t + 0.15, 3.0, 0.55,
                  bold=True, size=9, color=_WHITE, align="center")
        _add_text(s4, f"NT${float(item.get('revenue', 0)):,.0f}",
                  W - 7, row_t + 0.1, 5, 0.7, size=10, color=_DARK_TEXT, align="right")
        gm_v = item.get('gross_margin', item.get('margin', ''))
        if gm_v:
            _add_text(s4, f"{float(gm_v):.1f}%",
                      W - 2, row_t + 0.1, 1.5, 0.7, size=9, color=_SUBTEXT, align="right")
    _add_footer(s4, W)

    # P5: AI 策略洞察
    s5 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s5, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s5, "AI 策略洞察")
    _add_text(s5, ai_text or "(暫無 AI 分析)",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s5, W)

    path = _new_path("strategy")
    prs.save(path)
    return path


# ── 促銷報告 PPT（5頁）────────────────────────────────────────────────────────
def generate_promo_ppt(promo_label: str, data, ai_text: str) -> str:
    """P1封面 P2 促銷vs對比期KPI P3 業績對比柱狀圖 P4 TOP商品 P5 AI洞察"""
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    pd_      = data.get('promo', {}) if isinstance(data, dict) else {}
    prev     = data.get('pre', {})   if isinstance(data, dict) else {}
    rev_lift = float(data.get('rev_lift', 0)) if isinstance(data, dict) else 0
    ord_lift = float(data.get('ord_lift', 0)) if isinstance(data, dict) else 0
    top_prod = pd_.get('top_products', [])

    promo_rev  = float(pd_.get('revenue', 0))
    promo_ord  = int(pd_.get('orders', 0))
    promo_gm   = float(pd_.get('margin', 0))
    promo_days = max(int(pd_.get('days', 1)), 1)

    pre_rev   = float(prev.get('revenue', 0))
    pre_ord   = int(prev.get('orders', 0))
    pre_gm    = float(prev.get('margin', 0))
    pre_start = prev.get('start', '')
    pre_end   = prev.get('end', '')

    lift_icon  = "📈" if rev_lift > 0 else "📉"
    lift_color = _GREEN_KPI if rev_lift > 0 else _RED_WARN

    # P1: 封面
    _cover_slide(prs, f"促銷報告\n{promo_label}",
                 f"業績 NT${promo_rev:,.0f}（{promo_rev/10000:.1f}萬）  {lift_icon} {rev_lift:+.1f}%",
                 f"活動 {promo_days} 天　生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}")

    # P2: 促銷 vs 對比期 KPI 對比
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"促銷效益對比 — {promo_label}")

    # 促銷期 KPI（橘色）
    _add_rect(s2, 0.5, 1.7, W - 1, 0.5, _BRAND_OG)
    _add_text(s2, f"▶ 促銷期（{promo_label}，{promo_days}天）",
              0.7, 1.73, W - 1.4, 0.44, bold=True, size=11, color=_WHITE)
    promo_kpis = [
        (_BRAND_OG2, "活動期業績",  f"NT${promo_rev/10000:.1f}萬", ""),
        (_BRAND_OG2, "活動期訂單",  f"{promo_ord:,} 筆",            ""),
        (_BRAND_OG2, "活動期毛利率", f"{promo_gm:.1f}%",            ""),
        (_BRAND_OG2, "日均業績",    f"NT${promo_rev/promo_days/10000:.1f}萬", ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(promo_kpis):
        _kpi_card(s2, i * 7.8 + 0.5, 2.3, 7.4, 2.8, col, lbl, val, sub)

    # 對比期 KPI（深灰）
    _add_rect(s2, 0.5, 5.3, W - 1, 0.5, _FOOTER_BG)
    _add_text(s2, f"◀ 對比期（{pre_start} ~ {pre_end}）",
              0.7, 5.33, W - 1.4, 0.44, bold=True, size=11, color=_WHITE)
    pre_kpis = [
        (_FOOTER_BG, "對比期業績",  f"NT${pre_rev/10000:.1f}萬", ""),
        (_FOOTER_BG, "對比期訂單",  f"{pre_ord:,} 筆",            ""),
        (_FOOTER_BG, "對比期毛利率", f"{pre_gm:.1f}%",            ""),
        (_FOOTER_BG, "日均業績",    f"NT${pre_rev/promo_days/10000:.1f}萬", ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(pre_kpis):
        _kpi_card(s2, i * 7.8 + 0.5, 5.9, 7.4, 2.8, col, lbl, val, sub)

    # 升降幅橫幅
    _add_rect(s2, 0.5, 9.0, W - 1, 0.85, lift_color)
    _add_text(s2, f"{lift_icon} 業績成長 {rev_lift:+.1f}%　　訂單成長 {ord_lift:+.1f}%",
              0.7, 9.05, W - 1.4, 0.75, bold=True, size=16, color=_WHITE, align="center")
    _add_footer(s2, W)

    # P3: 業績對比柱狀圖（參考 growth_analysis.html revenueChart 雙柱概念）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, "促銷期 vs 對比期 — 業績比較（萬元）")
    _add_column_chart(
        s3, 1.0, 1.8, W - 2.0, 11.0,
        ["對比期", "促銷期"],
        [("業績（萬元）", [pre_rev, promo_rev])],
        bar_colors=[_FOOTER_BG, _BRAND_OG]
    )
    # 毛利率文字對比
    gm_diff = promo_gm - pre_gm
    gm_color = _GREEN_KPI if gm_diff >= 0 else _RED_WARN
    _add_text(s3,
              f"毛利率：對比期 {pre_gm:.1f}%  →  促銷期 {promo_gm:.1f}%  "
              f"({'↑' if gm_diff >= 0 else '↓'}{abs(gm_diff):.1f}pp)",
              1.0, 13.05, W - 2, 0.55, size=11, color=gm_color)
    _add_footer(s3, W)

    # P4: 活動期熱銷商品
    _product_table_slide(prs, f"促銷期熱銷商品 TOP 10 — {promo_label}", top_prod)

    # P5: AI 洞察
    s5 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s5, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s5, f"促銷 AI 洞察 — {promo_label}")
    _add_text(s5, ai_text or "(暫無 AI 分析)",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s5, W)

    path = _new_path("promo")
    prs.save(path)
    return path


# ── 競品比較 PPT（4頁，維持原有架構）────────────────────────────────────────────
def generate_competitor_ppt(period_label: str, db_data: dict, ai_text: str) -> str:
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    results    = db_data.get("results", [])
    found      = [r for r in results if r.get("found")]
    pc_wins    = [r for r in found if r.get("price_diff", 0) > 10]
    mo_wins    = [r for r in found if r.get("price_diff", 0) < -10]
    tie        = [r for r in found if abs(r.get("price_diff", 0)) <= 10]
    not_found  = [r for r in results if not r.get("found")]
    total      = len(results)
    match_rate = len(found) / total * 100 if total else 0
    avg_pct    = (sum(r.get("price_diff_pct", 0) for r in found) / len(found)
                  if found else 0)
    momo_rev   = db_data.get("momo_revenue", 0)

    # P1: 封面
    _cover_slide(
        prs,
        f"momo vs PChome\n競品比較分析",
        f"掃描 {total} 件熱銷商品｜比對成功 {len(found)} 件",
        f"分析週期：{period_label}　　平均價差：{avg_pct:+.1f}%　　生成時間：{datetime.now().strftime('%Y/%m/%d %H:%M')}",
    )

    # P2: KPI 摘要 + 橫條圖
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"競品比較 KPI 摘要 — {period_label}")
    cards = [
        (_BLUE_KPI,  "掃描商品數",  str(total),             f"{total} 件熱銷商品"),
        ("1565C0",   "比對成功率",  f"{match_rate:.0f}%",   f"{len(found)} 件成功"),
        (_GREEN_KPI, "PChome 優勢", f"{len(pc_wins)}/{len(found)}", f"{len(pc_wins)} 件 PChome 更便宜"),
        (_BRAND_OG2, "momo 優勢",   f"{len(mo_wins)}/{len(found)}", f"{len(mo_wins)} 件 momo 更便宜"),
    ]
    card_w, card_gap, card_t = 7.4, 0.4, 1.8
    for i, (col, lbl, val, sub) in enumerate(cards):
        _kpi_card(s2, i * (card_w + card_gap) + 0.5, card_t, card_w, 3.2, col, lbl, val, sub)

    trend_color = _RED_WARN if avg_pct < -3 else (_BRAND_OG if avg_pct > 3 else "FFA726")
    trend_icon  = "⚠️ momo 整體偏貴" if avg_pct < -3 else ("✅ momo 整體具優勢" if avg_pct > 3 else "➖ 整體持平")
    _add_rect(s2, 0.5, 5.3, W - 1, 0.85, trend_color)
    _add_text(s2, f"整體定價態勢：{trend_icon}    平均價差 {avg_pct:+.1f}%（正值=momo偏貴）",
              0.7, 5.35, W - 1.4, 0.75, bold=True, size=13, color=_WHITE)

    bar_data = [
        ("PChome 更便宜", len(pc_wins),   total, _BAR_PCHOME),
        ("momo 更便宜",   len(mo_wins),   total, _BAR_MOMO),
        ("價格相近",      len(tie),       total, _BAR_TIE),
        ("未找到對應",    len(not_found), total, _BAR_MISS),
    ]
    row_t = 6.4
    for label, val, tot, col in bar_data:
        _horiz_bar(s2, 0.8, row_t, 0.9, label, val, tot, col, max_w=16.0)
        row_t += 1.0
    if momo_rev:
        _add_text(s2, f"本期 momo 掃描商品總業績：NT$ {momo_rev:,.0f}（{momo_rev/10000:.1f}萬）",
                  0.8, 11.8, W - 1.6, 0.7, size=10, color=_SUBTEXT)
    _add_footer(s2, W)

    # P3: 商品比較表
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, "PChome 業績優勢 — 比 momo 熱銷 TOP10")
    cols  = ["#", "momo 商品", "momo 定價", "PChome 商品", "PChome 定價", "價差", "趨勢"]
    col_w = [0.8, 8.5, 2.8, 8.5, 2.8, 2.5, 2.0]
    x = 0.3
    for c, w in zip(cols, col_w):
        _add_rect(s3, x, 1.7, w, 0.7, _BRAND_OG)
        _add_text(s3, c, x + 0.05, 1.72, w - 0.1, 0.66,
                  bold=True, size=9, color=_WHITE, align="center")
        x += w + 0.1
    rows = (pc_wins + mo_wins)[:10]
    for ri, r in enumerate(rows):
        bg     = _LIGHT_GRAY if ri % 2 == 0 else _WHITE
        x      = 0.3
        diff   = r.get("price_diff", 0)
        pct    = r.get("price_diff_pct", 0)
        diff_c = _RED_WARN if diff < -10 else (_GREEN_KPI if diff > 10 else _DARK_TEXT)
        trend  = "📈 PChome貴" if diff > 10 else ("📉 momo貴" if diff < -10 else "➖")
        cells  = [
            (str(ri + 1),                     _DARK_TEXT),
            (r.get("momo_name", "")[:24],     _DARK_TEXT),
            (f"NT${r.get('momo_price',0):,.0f}", _DARK_TEXT),
            (r.get("pc_name", "")[:24],       _DARK_TEXT),
            (f"NT${r.get('pc_price',0):,.0f}",   _DARK_TEXT),
            (f"{diff:+,.0f} ({pct:+.1f}%)",   diff_c),
            (trend,                            diff_c),
        ]
        row_t = 2.5 + ri * 0.9
        for (txt, tc), w in zip(cells, col_w):
            _add_rect(s3, x, row_t, w, 0.85, bg)
            _add_text(s3, txt, x + 0.05, row_t + 0.05, w - 0.1, 0.75,
                      size=8, color=tc, align="center")
            x += w + 0.1
    _add_footer(s3, W)

    # P4: AI 洞察
    s4 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s4, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s4, "AI 競品洞察 — 策略建議")
    _add_text(s4, ai_text or "（AI 分析生成中）",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s4, W)

    path = _new_path("competitor")
    prs.save(path)
    return path


# ── 成長趨勢報告 PPT（6頁）────────────────────────────────────────────────────
def generate_growth_ppt(db_data, ai_text: str) -> str:
    """P1封面 P2 YTD KPI P3 月營收柱狀圖 P4 MoM月增率 P5 AOV+毛利率 P6 AI洞察
    db_data: {chart_data: {labels, revenue, mom, yoy, aov, margin_rate},
              kpi: {ytd_revenue, ytd_growth, current_year, recent_aov, total_orders}}
    對應 growth_analysis.html: revenueChart / momChart / aovChart / marginChart
    """
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    cd    = db_data.get('chart_data', {}) if isinstance(db_data, dict) else {}
    kpi   = db_data.get('kpi', {})        if isinstance(db_data, dict) else {}

    labels       = cd.get('labels', [])
    revenue      = cd.get('revenue', [])
    mom          = cd.get('mom', [])
    yoy          = cd.get('yoy', [])
    aov_list     = cd.get('aov', [])
    margin_list  = cd.get('margin_rate', [])

    ytd_rev    = float(kpi.get('ytd_revenue', 0))
    ytd_growth = float(kpi.get('ytd_growth', 0))
    curr_yr    = kpi.get('current_year', datetime.now().year)
    recent_aov = float(kpi.get('recent_aov', 0))
    total_ord  = int(kpi.get('total_orders', 0))

    # P1: 封面
    growth_icon = "📈" if ytd_growth >= 0 else "📉"
    _cover_slide(prs, f"成長趨勢報告\n{curr_yr} 年",
                 f"YTD 累計業績 NT${ytd_rev:,.0f}（{ytd_rev/10000:.1f}萬）",
                 f"{growth_icon} 年增率 {ytd_growth:+.1f}%　　生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}")

    # P2: YTD KPI 卡
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"成長趨勢 KPI — {curr_yr} 年 YTD")
    ytd_color = _GREEN_KPI if ytd_growth >= 0 else _RED_WARN
    kpis = [
        (_BLUE_KPI,  "YTD 累計業績", f"NT${ytd_rev/10000:.1f}萬",  f"{curr_yr} 年初至今"),
        (ytd_color,  "年增率 (YTD)", f"{ytd_growth:+.1f}%",         "vs 去年同期"),
        (_BRAND_OG2, "近30日客單價", f"NT${recent_aov:,.0f}",        ""),
        (_FOOTER_BG, "累計訂單",     f"{total_ord:,} 筆",            ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(kpis):
        _kpi_card(s2, i * 7.8 + 0.5, 1.8, 7.4, 3.5, col, lbl, val, sub)
    # 最近月份 MoM 快速摘要
    if mom and labels:
        last_mom   = float(mom[-1])
        last_label = labels[-1]
        mom_color  = _GREEN_KPI if last_mom >= 0 else _RED_WARN
        _add_rect(s2, 0.5, 5.6, W - 1, 0.7, mom_color)
        _add_text(s2, f"最近月份 {last_label}  MoM {last_mom:+.1f}%  |  YoY {float(yoy[-1]) if yoy else 0:+.1f}%",
                  0.8, 5.65, W - 1.6, 0.6, bold=True, size=13, color=_WHITE, align="center")
    _add_footer(s2, W)

    # P3: 月營收柱狀圖（對應 revenueChart）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, f"月營收趨勢 — {curr_yr} 年（萬元）")
    if labels and revenue:
        short_labels = [lb[-7:] if len(lb) > 4 else lb for lb in labels]
        _add_column_chart(s3, 0.8, 1.8, W - 1.6, 11.0,
                          short_labels, [("月業績（萬元）", revenue)],
                          bar_colors=[_BLUE_KPI])
        # YoY 備注
        if yoy:
            avg_yoy = sum(float(v) for v in yoy if v) / max(len([v for v in yoy if v]), 1)
            _add_text(s3, f"平均 YoY：{avg_yoy:+.1f}%",
                      0.8, 13.05, 10, 0.5, size=10, color=_SUBTEXT)
    _add_footer(s3, W)

    # P4: MoM 月增率柱狀圖（對應 momChart）
    s4 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s4, 0, 0, W, 19.05, _WHITE)
    _add_header(s4, "月增率分析 (MoM) — 正值綠 / 負值請注意")
    if labels and mom:
        short_labels = [lb[-7:] if len(lb) > 4 else lb for lb in labels]
        pos_vals = [float(v) if float(v) >= 0 else 0 for v in mom]
        neg_vals = [abs(float(v)) if float(v) < 0 else 0 for v in mom]
        _add_column_chart(s4, 0.8, 1.8, W - 1.6, 8.0,
                          short_labels,
                          [("成長（%）", pos_vals), ("衰退（%）", neg_vals)],
                          bar_colors=[_GREEN_KPI, _RED_WARN], raw_values=True)
        # 最近3個月摘要
        _add_rect(s4, 0.5, 10.2, W - 1, 0.6, _FOOTER_BG)
        _add_text(s4, "近3月 MoM：" + "  |  ".join(
            f"{labels[i][-7:]} {float(mom[i]):+.1f}%" for i in range(max(0, len(mom)-3), len(mom))),
            0.7, 10.25, W - 1.4, 0.5, size=11, color=_WHITE, align="center")
    _add_footer(s4, W)

    # P5: AOV 客單價 + 毛利率走勢（對應 aovChart + marginChart）
    s5 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s5, 0, 0, W, 19.05, _WHITE)
    _add_header(s5, "客單價 & 毛利率走勢")
    if labels and aov_list:
        short_labels = [lb[-7:] if len(lb) > 4 else lb for lb in labels]
        _add_text(s5, "▶ 客單價走勢（NT$）", 0.5, 1.7, 16, 0.5, size=11, color=_DARK_TEXT, bold=True)
        _add_column_chart(s5, 0.5, 2.2, W * 0.48, 5.0,
                          short_labels, [("客單價（元）", aov_list)],
                          bar_colors=[_BRAND_OG2], raw_values=True)
    if labels and margin_list:
        short_labels = [lb[-7:] if len(lb) > 4 else lb for lb in labels]
        _add_text(s5, "▶ 毛利率走勢（%）", W * 0.52, 1.7, 16, 0.5, size=11, color=_DARK_TEXT, bold=True)
        _add_column_chart(s5, W * 0.52, 2.2, W * 0.46, 5.0,
                          short_labels, [("毛利率（%）", margin_list)],
                          bar_colors=[_GREEN_KPI], raw_values=True)
    # 摘要數字
    if aov_list:
        avg_aov = sum(float(v) for v in aov_list if v) / max(len([v for v in aov_list if v]), 1)
        _add_rect(s5, 0.5, 7.5, W * 0.48, 0.6, _BRAND_OG2)
        _add_text(s5, f"平均客單價：NT${avg_aov:,.0f}", 0.7, 7.55, W * 0.46, 0.5,
                  size=11, color=_WHITE, align="center")
    if margin_list:
        avg_mg = sum(float(v) for v in margin_list if v) / max(len([v for v in margin_list if v]), 1)
        _add_rect(s5, W * 0.52, 7.5, W * 0.46, 0.6, _GREEN_KPI)
        _add_text(s5, f"平均毛利率：{avg_mg:.1f}%", W * 0.52 + 0.2, 7.55, W * 0.44, 0.5,
                  size=11, color=_WHITE, align="center")
    _add_footer(s5, W)

    # P6: AI 洞察
    s6 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s6, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s6, "AI 成長趨勢洞察")
    _add_text(s6, ai_text or "(暫無 AI 分析)",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s6, W)

    path = _new_path("growth")
    prs.save(path)
    return path


# ── 廠商業績報告 PPT（5頁）────────────────────────────────────────────────────
def generate_vendor_ppt(yr, mo, db_data, ai_text: str) -> str:
    """P1封面 P2 KPI P3 廠商橫條圖(2年對比) P4 廠商明細表 P5 AI洞察
    db_data: {vendor_ranking: [{name, sales, sales_2024, sales_2025, profit, margin}],
              kpis: {total_sales, total_profit, avg_margin, vendor_count}}
    對應 monthly_summary_analysis.html: vendorRankingChart
    """
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    vendors    = db_data.get('vendor_ranking', []) if isinstance(db_data, dict) else []
    kpis       = db_data.get('kpis', {})           if isinstance(db_data, dict) else {}
    period_lbl = db_data.get('period_label', f"{yr}/{mo:02d}") if isinstance(db_data, dict) else f"{yr}/{mo:02d}"

    total_sales  = float(kpis.get('total_sales', sum(v.get('sales', 0) for v in vendors)))
    total_profit = float(kpis.get('total_profit', sum(v.get('profit', 0) for v in vendors)))
    avg_margin   = total_profit / total_sales * 100 if total_sales else 0
    vcount       = len(vendors)

    # P1: 封面
    _cover_slide(prs, f"廠商業績報告\n{period_lbl}",
                 f"合計 {vcount} 家廠商  |  總業績 NT${total_sales/10000:.1f}萬",
                 f"平均毛利率 {avg_margin:.1f}%　生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}")

    # P2: KPI 卡
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"廠商業績 KPI — {period_lbl}")
    top1 = vendors[0] if vendors else {}
    kpi_cards = [
        (_BLUE_KPI,  "廠商總數",     f"{vcount} 家",               ""),
        (_GREEN_KPI, "合計業績",     f"NT${total_sales/10000:.1f}萬", ""),
        (_BRAND_OG2, "合計毛利",     f"NT${total_profit/10000:.1f}萬", ""),
        (_FOOTER_BG, "平均毛利率",   f"{avg_margin:.1f}%",           ""),
    ]
    for i, (col, lbl, val, sub) in enumerate(kpi_cards):
        _kpi_card(s2, i * 7.8 + 0.5, 1.8, 7.4, 3.5, col, lbl, val, sub)
    if top1:
        _add_rect(s2, 0.5, 5.6, W - 1, 0.7, _BRAND_OG)
        _add_text(s2, f"🏆 業績第一：{top1.get('name','')[:20]}  NT${float(top1.get('sales',0)):,.0f}  毛利率 {top1.get('margin',0):.1f}%",
                  0.7, 5.65, W - 1.4, 0.6, bold=True, size=12, color=_WHITE, align="center")
    _add_footer(s2, W)

    # P3: TOP 20 廠商橫條圖 — 2024 vs 2025（對應 vendorRankingChart）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, f"TOP 20 廠商業績排行 — {period_lbl}（萬元）")
    if vendors:
        top20     = vendors[:20]
        names     = [v.get('name', '')[:15] for v in top20]
        s24_vals  = [float(v.get('sales_2024', 0)) for v in top20]
        s25_vals  = [float(v.get('sales_2025', 0)) for v in top20]
        has_both  = any(s24_vals) and any(s25_vals)
        if has_both:
            _add_horiz_chart(s3, 0.5, 1.8, W - 1, 11.3,
                             names,
                             [("2024", s24_vals), ("2025", s25_vals)],
                             bar_colors=[_FOOTER_BG, _BLUE_KPI])
        else:
            revs = [float(v.get('sales', 0)) for v in top20]
            _add_horiz_chart(s3, 0.5, 1.8, W - 1, 11.3,
                             names, [("業績（萬元）", revs)],
                             bar_colors=[_BLUE_KPI])
    _add_footer(s3, W)

    # P4: 廠商明細表
    s4 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s4, 0, 0, W, 19.05, _WHITE)
    _add_header(s4, f"廠商業績明細 TOP 15 — {period_lbl}")
    _add_rect(s4, 0.5, 1.7, W - 1, 0.65, _BRAND_OG)
    hdrs = ["#", "廠商名稱", "總業績", "毛利額", "毛利率"]
    col_ws = [1.2, 13.0, 6.5, 6.5, 3.5]
    x = 0.6
    for h, cw in zip(hdrs, col_ws):
        _add_text(s4, h, x, 1.73, cw, 0.59, bold=True, size=10, color=_WHITE,
                  align="center" if h != "廠商名稱" else "left")
        x += cw + 0.1
    for i, v in enumerate(vendors[:15]):
        bg    = _LIGHT_GRAY if i % 2 == 0 else _WHITE
        row_t = 2.5 + i * 0.66
        _add_rect(s4, 0.5, row_t, W - 1, 0.63, bg)
        x = 0.6
        cells = [
            (str(i+1),                                        "center"),
            (str(v.get('name', ''))[:30],                     "left"),
            (f"NT${float(v.get('sales',0)):,.0f}",            "right"),
            (f"NT${float(v.get('profit',0)):,.0f}",           "right"),
            (f"{v.get('margin',0):.1f}%",                     "right"),
        ]
        for (txt, al), cw in zip(cells, col_ws):
            _add_text(s4, txt, x, row_t + 0.06, cw, 0.52,
                      size=9, color=_DARK_TEXT, align=al)
            x += cw + 0.1
    _add_footer(s4, W)

    # P5: AI 洞察
    s5 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s5, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s5, f"AI 廠商洞察 — {period_lbl}")
    _add_text(s5, ai_text or "(暫無 AI 分析)",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s5, W)

    path = _new_path("vendor")
    prs.save(path)
    return path


# ── BCG 品牌矩陣報告 PPT（5頁）───────────────────────────────────────────────
def generate_bcg_ppt(yr, mo, db_data, ai_text: str) -> str:
    """P1封面 P2 BCG象限KPI P3 BCG策略分類表 P4 區域業績橫條圖 P5 AI洞察
    db_data: {bcg_data: [{name, qty, margin, sales}],
              division_dist: [{name, sales, sales_2024, sales_2025}],
              kpis: {total_sales, total_profit, avg_margin}}
    對應 monthly_summary_analysis.html: bcgMatrixChart + divisionDistChart
    """
    from pptx import Presentation
    from pptx.util import Cm

    prs = Presentation()
    prs.slide_width  = Cm(33.87)
    prs.slide_height = Cm(19.05)
    W = 33.87

    bcg_data  = db_data.get('bcg_data', [])    if isinstance(db_data, dict) else []
    div_dist  = db_data.get('division_dist', []) if isinstance(db_data, dict) else []
    kpis      = db_data.get('kpis', {})          if isinstance(db_data, dict) else {}
    period_lbl = db_data.get('period_label', f"{yr}/{mo:02d}") if isinstance(db_data, dict) else f"{yr}/{mo:02d}"

    total_sales = float(kpis.get('total_sales', 0))
    avg_margin  = float(kpis.get('avg_margin', 0))

    # BCG 分類（以 avg_margin 為毛利門檻，以 中位業績 為業績門檻）
    if bcg_data:
        sales_vals    = [float(b.get('sales', 0)) for b in bcg_data]
        median_sales  = sorted(sales_vals)[len(sales_vals) // 2] if sales_vals else 1
        margin_thresh = avg_margin if avg_margin > 5 else 20.0

        BCG_LABELS = {'star': '明星', 'cow': '金牛', 'q': '問號', 'dog': '瘦狗'}
        BCG_COLORS = {'star': _GREEN_KPI, 'cow': _BLUE_KPI, 'q': _BRAND_OG, 'dog': _RED_WARN}

        def _bcg_class(b):
            s = float(b.get('sales', 0))
            m = float(b.get('margin', 0))
            if m >= margin_thresh and s >= median_sales:  return 'star'
            if m >= margin_thresh and s <  median_sales:  return 'cow'
            if m <  margin_thresh and s >= median_sales:  return 'q'
            return 'dog'

        classified = [dict(b, _cls=_bcg_class(b)) for b in bcg_data]
        star_list  = [b for b in classified if b['_cls'] == 'star']
        cow_list   = [b for b in classified if b['_cls'] == 'cow']
        q_list     = [b for b in classified if b['_cls'] == 'q']
        dog_list   = [b for b in classified if b['_cls'] == 'dog']
    else:
        classified = star_list = cow_list = q_list = dog_list = []
        BCG_LABELS = {'star': '明星', 'cow': '金牛', 'q': '問號', 'dog': '瘦狗'}
        BCG_COLORS = {'star': _GREEN_KPI, 'cow': _BLUE_KPI, 'q': _BRAND_OG, 'dog': _RED_WARN}
        margin_thresh = 20.0

    # P1: 封面
    _cover_slide(prs, f"品牌 BCG 矩陣報告\n{period_lbl}",
                 f"分析 {len(bcg_data)} 個品牌×區域組合",
                 f"毛利率門檻 {margin_thresh:.1f}%　生成 {datetime.now().strftime('%Y/%m/%d %H:%M')}")

    # P2: BCG 象限 KPI 卡
    s2 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s2, 0, 0, W, 19.05, _WHITE)
    _add_header(s2, f"BCG 矩陣象限分佈 — {period_lbl}")
    bcg_kpis = [
        (_GREEN_KPI, "⭐ 明星",  f"{len(star_list)} 個", "高毛利 + 高業績"),
        (_BLUE_KPI,  "🐄 金牛",  f"{len(cow_list)} 個",  "高毛利 + 低業績"),
        (_BRAND_OG,  "❓ 問號",  f"{len(q_list)} 個",    "低毛利 + 高業績"),
        (_RED_WARN,  "🐕 瘦狗",  f"{len(dog_list)} 個",  "低毛利 + 低業績"),
    ]
    for i, (col, lbl, val, sub) in enumerate(bcg_kpis):
        _kpi_card(s2, i * 7.8 + 0.5, 1.8, 7.4, 3.5, col, lbl, val, sub)
    # 判斷門檻說明
    _add_rect(s2, 0.5, 5.6, W - 1, 0.65, _FOOTER_BG)
    _add_text(s2, f"毛利率門檻：{margin_thresh:.1f}%（高/低）  |  業績門檻：NT${median_sales/10000:.1f}萬（中位數）",
              0.7, 5.65, W - 1.4, 0.55, size=11, color=_WHITE, align="center")
    _add_footer(s2, W)

    # P3: BCG 策略分類表（4象限各顯示 TOP 5）
    s3 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s3, 0, 0, W, 19.05, _WHITE)
    _add_header(s3, f"BCG 策略矩陣清單 — {period_lbl}")
    quadrants = [
        ('star', star_list,  0.5,   1.7),
        ('cow',  cow_list,   W/2+0.3, 1.7),
        ('q',    q_list,     0.5,   8.5),
        ('dog',  dog_list,   W/2+0.3, 8.5),
    ]
    for cls, items, lx, ty in quadrants:
        col  = BCG_COLORS[cls]
        lbl  = BCG_LABELS[cls]
        pw   = W / 2 - 0.8
        _add_rect(s3, lx, ty, pw, 0.55, col)
        _add_text(s3, f"{lbl} ({len(items)} 個)", lx + 0.2, ty + 0.05, pw - 0.4, 0.45,
                  bold=True, size=11, color=_WHITE)
        for j, b in enumerate(items[:5]):
            bg = _LIGHT_GRAY if j % 2 == 0 else _WHITE
            rt = ty + 0.6 + j * 0.65
            _add_rect(s3, lx, rt, pw, 0.62, bg)
            _add_text(s3, str(b.get('name', ''))[:22],
                      lx + 0.2, rt + 0.06, pw * 0.55, 0.52, size=8, color=_DARK_TEXT)
            _add_text(s3, f"{b.get('margin',0):.1f}%",
                      lx + pw * 0.58, rt + 0.06, pw * 0.2, 0.52, size=8, color=_SUBTEXT, align="right")
            _add_text(s3, f"NT${float(b.get('sales',0))/10000:.1f}M",
                      lx + pw * 0.8, rt + 0.06, pw * 0.18, 0.52, size=8, color=_DARK_TEXT, align="right")
    _add_footer(s3, W)

    # P4: 區域業績橫條圖（對應 divisionDistChart，2024 vs 2025）
    s4 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s4, 0, 0, W, 19.05, _WHITE)
    _add_header(s4, f"區域業績分佈 — {period_lbl}（萬元）")
    if div_dist:
        names    = [d.get('name', '')[:15] for d in div_dist[:12]]
        s24_vals = [float(d.get('sales_2024', 0)) for d in div_dist[:12]]
        s25_vals = [float(d.get('sales_2025', 0)) for d in div_dist[:12]]
        has_both = any(s24_vals) and any(s25_vals)
        if has_both:
            _add_horiz_chart(s4, 0.5, 1.8, W - 1, 11.3,
                             names,
                             [("2024", s24_vals), ("2025", s25_vals)],
                             bar_colors=[_FOOTER_BG, _BRAND_OG])
        else:
            revs = [float(d.get('sales', 0)) for d in div_dist[:12]]
            _add_horiz_chart(s4, 0.5, 1.8, W - 1, 11.3,
                             names, [("業績（萬元）", revs)],
                             bar_colors=[_BRAND_OG])
    else:
        _add_text(s4, "（無區域分佈資料）", 2, 7, 20, 2, size=14, color=_SUBTEXT, align="center")
    _add_footer(s4, W)

    # P5: AI 洞察
    s5 = prs.slides.add_slide(prs.slide_layouts[6])
    _add_rect(s5, 0, 0, W, 19.05, _BG_DARK)
    _add_header(s5, f"AI BCG 品牌策略洞察 — {period_lbl}")
    _add_text(s5, ai_text or "(暫無 AI 分析)",
              1.0, 2.0, W - 2.0, 11.0, size=13, color=_WHITE, wrap=True)
    _add_footer(s5, W)

    path = _new_path("bcg")
    prs.save(path)
    return path
    return path
