#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""天隆集团 2025 vs 2026 分季度趋势看板（图形化）生成器。

数据源（权威值，用户已核）：标准销售订单·已审核·非赠品·排除关联方；
季度按合同日期；2026 截止 09-30（Q4 无数据）。
口径与 脚本/统计_天隆_2025vs2026_分季度同比.py 一致。

输出：
  工具/天隆_2025vs2026_趋势看板.html   （自包含单文件，零外部依赖）
  （--png）销售分析/天隆_2025vs2026_趋势看板.png  宽 1440 整页

用法：
  env -u PYTHONPATH /usr/bin/python3 脚本/build_tianlong_trend.py
  env -u PYTHONPATH /usr/bin/python3 脚本/build_tianlong_trend.py --png
"""
import os
import re
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import brand_kit as BK

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
OUT_HTML = os.path.join(ROOT, '工具', '天隆_2025vs2026_趋势看板.html')
OUT_PNG = os.path.join(ROOT, '销售分析', '天隆_2025vs2026_趋势看板.png')

BLUE = '#1852B6'
GREY = '#94A3B8'
GREEN = '#0F9D58'
RED = '#D93025'
AMBER = '#E8830C'

# ── 权威数据（逐字用，不得改动）──────────────────────────────
T25 = [350800, 17900, 1035647, 1189300]
T26 = [572500, 236710, 418850, 0]

# 顺序：25Q1,25Q2,25Q3,25Q4 → 26Q1,26Q2,26Q3,26Q4
SKUS = [
    ('M4211-3',          [63000, 0, 0, 102000],           [195500, 4250, 223550, 0]),
    ('M5134-TL-1',       [181000, 0, 0, 0],               [175000, 0, 0, 0]),
    ('M5134-TL2-1',      [0, 0, 98000, 84000],            [150000, 0, 0, 0]),
    ('MD2071-2',         [70000, 9800, 72200, 57200],     [0, 110000, 0, 0]),
    ('M5244-ZJ4-TP',     [1800, 0, 363000, 180000],       [0, 60000, 34500, 0]),
    ('MD2071-2-NA',      [0, 2100, 10500, 15000],         [0, 30000, 30000, 0]),
    ('M5244-T3-DB',      [0, 0, 0, 0],                    [3000, 900, 43200, 0]),
    ('MD2104-003006-DB', [0, 0, 0, 0],                    [0, 0, 27760, 0]),
    ('PM202',            [12000, 0, 39600, 0],            [0, 25200, 0, 0]),
    ('MD2071-2-1',       [0, 0, 71500, 0],                [22000, 0, 0, 0]),
    ('M5244-043801-1',   [0, 0, 0, 0],                    [0, 0, 19200, 0]),
    ('FM0301-042902',    [0, 0, 0, 0],                    [0, 0, 18000, 0]),
    ('M5244-T1-DB-1',    [0, 0, 9000, 0],                 [15000, 0, 0, 0]),
    ('M5244-ZJ4-TP-1',   [0, 0, 0, 0],                    [12000, 0, 0, 0]),
    ('MD2104-003003-DB', [0, 0, 0, 0],                    [0, 0, 8400, 0]),
]

LOST = ['MD2104-003001', 'PFM4211-3-TL1-S20', 'FMD5104-000814', 'M5244-043801',
        'M5244-T1-DB', 'FM4211-3-TL1', 'FM4211-003001']
CONT = ['M4211-3', 'M5134-TL-1', 'M5134-TL2-1', 'MD2071-2', 'M5244-ZJ4-TP',
        'MD2071-2-NA', 'PM202', 'MD2071-2-1', 'M5244-T1-DB-1']
NEW = ['M5244-T3-DB', 'MD2104-003006-DB', 'M5244-043801-1', 'FM0301-042902',
       'M5244-ZJ4-TP-1', 'MD2104-003003-DB', 'MD2091', 'M5211-3-FP']

HL = {'M4211-3': (GREEN, '唯一增长系'),
      'M5244-ZJ4-TP': (AMBER, '塔方系')}

# ── 业务注解（用户补充，原话逐字入档；不改任何金额）──────────
ANNOTS = {
    'HPV': {
        'label': '❗业务注解', 'color': RED,
        'type': '商机未延续（海外机会性商机）',
        'scope': 'PFM4211* ｜ 2025 全年 → 2026 全年',
        'text': 'PFM4211 的是用于 HPV 海外商机，2025 年的机会性商机，'
                '在 2026 年未能得到延续，主要是客户市场开拓原因',
        'proof': 'PFM4211-3-TL1-S20 2025 = 1,098,000（占西安华伟 66.9%）→ 2026 = 0',
    },
    'ZJ4': {
        'label': '❗业务注解', 'color': AMBER,
        'type': '稳定性客诉·品种替换',
        'scope': 'M5244-ZJ4 / M5244-ZJ4-TP ｜ 26Q2–Q3',
        'text': 'M5244-ZJ4 因为稳定性客诉问题，在 2026 年 7 月开始采购受到影响，'
                '在 8 月底我们用 MD2104-003006 完成替换，因此影响了 Q3 的采购',
        'proof': 'M5244-ZJ4-TP 2025 = 544,800 → 2026 = 94,500；'
                 '替品 MD2104-003006-DB 仅 26Q3 出现 27,760 元（与“8月底完成替换”吻合）',
    },
    'M4211-3': {
        'label': '✔势头好', 'color': GREEN,
        'type': '势头好（HBV 超敏项目）',
        'scope': 'M4211-3 ｜ 26Q1–Q3',
        'text': 'M4211-3 是客户的 HBV 超敏项目，客户反馈今年销售势头非常好',
        'proof': 'M4211-3 2025 = 165,000 → 2026 = 423,300（+156%）',
    },
}


def note_for(code):
    """货号 → (注解 id, 角标文案, 角标底色)；无注解返回 None。"""
    if code.startswith('PFM4211'):
        return ('HPV', ANNOTS['HPV']['label'], RED)
    if code.startswith('M5244-ZJ4'):
        return ('ZJ4', '❗客诉/替换', AMBER)
    if code.startswith('MD2104-003006'):
        return ('ZJ4', '✔替换承接', GREEN)
    if code == 'M4211-3':
        return ('M4211-3', ANNOTS['M4211-3']['label'], GREEN)
    return None


def _badge_html(aid, label, color):
    tip = ANNOTS[aid]['text']
    return ('<span class="note-badge" style="background:%s" data-note="%s" '
            'title="业务注解：%s">%s</span>' % (color, aid, tip, label))


def _chip_html(color, name):
    """结构变迁带里的小卡：命中注解则内嵌一个角标（含悬停全文）。"""
    nb = note_for(name)
    extra = (' ' + _badge_html(*nb)) if nb else ''
    return ('<span class="chip" style="border-color:%s;color:%s">%s%s</span>'
            % (color, color, name, extra))


CONCLUSION = ('全年 −52.7%（−136.6 万）；降幅集中 M5244-ZJ4-TP（−45.0 万）；'
              '唯一亮点 M4211-3（+25.8 万，但脉冲式）；两年都卖的只有 9 个——项目切换型下滑')

CONCLUSION_HTML = ('全年 <b>−52.7%（−136.6 万）</b>；降幅集中 <b>M5244-ZJ4-TP（−45.0 万）</b>；'
                   '唯一亮点 <i>M4211-3（+25.8 万，但脉冲式）</i>；两年都卖的只有 9 个'
                   '——项目切换型下滑。下滑可归因：海外机会性商机未延续（HPV）＋ 客诉导致的'
                   '品种替换（M5244-ZJ4→MD2104-003006），<i>非价格或竞争力问题</i>')


# ── 数字格式化 ────────────────────────────────────────────────
def wan(v, dec=1):
    """万元，四舍五入（ROUND_HALF_UP：572500 → 57.3 万，符合直觉）。"""
    from decimal import Decimal, ROUND_HALF_UP
    q = Decimal(1).scaleb(-dec)
    return str((Decimal(v) / 10000).quantize(q, rounding=ROUND_HALF_UP))


def wan_s(v, dec=1):
    s = '−' if v < 0 else ('+' if v > 0 else '')
    return s + wan(abs(v), dec)


# ── ① 季度总量双线图 ─────────────────────────────────────────
def main_line_chart():
    W, H = 1380, 352
    L, R, T, B = 78, 36, 28, 54
    pw, ph = W - L - R, H - T - B
    ymax = 1200000

    def X(i):
        return L + pw * (i + 0.5) / 4.0

    def Y(v):
        return T + ph * (1 - v / float(ymax))

    xs = [X(i) for i in range(4)]
    o = []
    o.append('<svg class="fig main-line" viewBox="0 0 %d %d" width="100%%" role="img" '
             'aria-label="天隆集团 2025 与 2026 分季度销售额双线对比图">' % (W, H))

    # 横向网格 + y 轴刻度（单位：万元）
    for v in range(0, ymax + 1, 200000):
        y = Y(v)
        o.append('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="%s" stroke-width="1" '
                 '%s/>' % (L, y, W - R, y, '#E8EDF4' if v else '#CBD5E1',
                           '' if v else 'stroke-dasharray="0"'))
        o.append('<text x="%d" y="%.1f" text-anchor="end" font-size="12" fill="#8A94A6">%d</text>'
                 % (L - 10, y + 4, v // 10000))
    o.append('<text x="%d" y="%d" font-size="12.5" fill="#8A94A6">单位：万元</text>' % (L - 58, T - 8))

    # x 轴季度标签
    for i, q in enumerate(['Q1', 'Q2', 'Q3', 'Q4']):
        o.append('<text x="%.1f" y="%d" text-anchor="middle" font-size="14" fill="#4A5568" '
                 'font-weight="600">%s</text>' % (xs[i], H - B + 26, q))

    # 2025 虚线
    p25 = ' '.join('%s%.1f %.1f' % ('M' if i == 0 else 'L', xs[i], Y(T25[i]))
                   for i in range(4))
    o.append('<path d="%s" fill="none" stroke="%s" stroke-width="3" stroke-dasharray="9 6" '
             'stroke-linejoin="round"/>' % (p25, GREY))
    # 2026 实线（Q1–Q3 有数据）
    p26 = ' '.join('%s%.1f %.1f' % ('M' if i == 0 else 'L', xs[i], Y(T26[i]))
                   for i in range(3))
    o.append('<path d="%s" fill="none" stroke="%s" stroke-width="3.5" stroke-linejoin="round" '
             'stroke-linecap="round"/>' % (p26, BLUE))
    # 2026 Q4 无数据：浅灰虚线延伸至 0 + 空心点
    o.append('<path d="M%.1f %.1f L%.1f %.1f" fill="none" stroke="%s" stroke-width="2" '
             'stroke-dasharray="4 5"/>' % (xs[2], Y(T26[2]), xs[3], Y(0), '#C4CDDA'))
    o.append('<circle cx="%.1f" cy="%.1f" r="5.5" fill="#fff" stroke="#C4CDDA" '
             'stroke-width="2"/>' % (xs[3], Y(0)))
    o.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-size="12.5" fill="#8A94A6" '
             'font-weight="600">截止 09-30</text>' % (xs[3], Y(0) - 26))
    o.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-size="12.5" fill="#8A94A6" '
             'font-weight="600">无数据</text>' % (xs[3], Y(0) - 11))

    # 数据点 + 数值标签
    for i in range(4):
        y = Y(T25[i])
        o.append('<circle cx="%.1f" cy="%.1f" r="5" fill="#fff" stroke="%s" stroke-width="2.5"/>'
                 % (xs[i], y, GREY))
        o.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-size="12.5" fill="#64748B" '
                 'font-weight="600">%s万</text>' % (xs[i], y + 20, wan(T25[i])))
    for i in range(3):
        y = Y(T26[i])
        o.append('<circle cx="%.1f" cy="%.1f" r="6" fill="%s" stroke="#fff" stroke-width="2.5"/>'
                 % (xs[i], y, BLUE))
        o.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-size="13.5" fill="%s" '
                 'font-weight="700">%s万</text>' % (xs[i], y - 12, BLUE, wan(T26[i])))
    o.append('</svg>')
    return ''.join(o)


# ── ② 货号趋势小图阵（sparkline）─────────────────────────────
SPARK_YMAX = 400000


def sparkline(v25, v26, hl_color=BLUE, sku=''):
    W, H = 240, 104
    L, R, T, B = 14, 14, 12, 24
    pw, ph = float(W - L - R), float(H - T - B)

    def X(i):
        return L + pw * (i + 0.5) / 4.0

    def Y(v):
        return T + ph * (1 - v / float(SPARK_YMAX))

    xs = [X(i) for i in range(4)]
    o = ['<svg class="spark" data-sku="%s" viewBox="0 0 %d %d" width="100%%" role="img" '
         'aria-label="%s 分季度趋势小图">' % (sku, W, H, sku)]
    o.append('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#EDF1F7" stroke-width="1"/>'
             % (L, Y(SPARK_YMAX / 2), W - R, Y(SPARK_YMAX / 2)))
    o.append('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#DDE3EC" stroke-width="1"/>'
             % (L, Y(0), W - R, Y(0)))
    p25 = ' '.join('%s%.1f %.1f' % ('M' if i == 0 else 'L', xs[i], Y(v25[i])) for i in range(4))
    o.append('<path d="%s" fill="none" stroke="%s" stroke-width="2" stroke-dasharray="5 4"/>'
             % (p25, GREY))
    p26 = ' '.join('%s%.1f %.1f' % ('M' if i == 0 else 'L', xs[i], Y(v26[i])) for i in range(4))
    o.append('<path d="%s" fill="none" stroke="%s" stroke-width="2.4" stroke-linejoin="round" '
             'stroke-linecap="round"/>' % (p26, hl_color))
    for i in range(4):
        o.append('<circle cx="%.1f" cy="%.1f" r="2.6" fill="#fff" stroke="%s" stroke-width="1.6"/>'
                 % (xs[i], Y(v25[i]), GREY))
        o.append('<circle cx="%.1f" cy="%.1f" r="2.8" fill="%s" stroke="#fff" stroke-width="1.4"/>'
                 % (xs[i], Y(v26[i]), hl_color))
    for i, q in enumerate('1234'):
        o.append('<text x="%.1f" y="%d" text-anchor="middle" font-size="9.5" fill="#9AA5B4">%s</text>'
                 % (xs[i], H - 8, q))
    o.append('</svg>')
    return ''.join(o)


def spark_cards():
    rows = sorted(SKUS, key=lambda r: sum(r[2]), reverse=True)
    out = []
    for sku, v25, v26 in rows:
        s25, s26 = sum(v25), sum(v26)
        d = s26 - s25
        color, tag = HL.get(sku, (BLUE, ''))
        border = 'style="border-left-color:%s"' % color if tag else ''
        badge = ('<span class="hl-badge" style="background:%s">%s</span>' % (color, tag)) if tag else ''
        _nb = note_for(sku)
        note_badge = _badge_html(*_nb) if _nb else ''
        dcol = GREEN if d > 0 else (RED if d < 0 else '#8A94A6')
        arrow = '▲' if d > 0 else ('▼' if d < 0 else '—')
        out.append(
            '<div class="%s" %s data-26="%d" data-25="%d">' % ('card hl' if tag else 'card', border, s26, s25)
            + '<div class="card-hd"><span class="sku">%s</span>%s%s</div>' % (sku, note_badge, badge)
            + sparkline(v25, v26, color, sku)
            + '<div class="card-ft">'
            + '<div class="ft2">25年 <b>%s</b>万　·　26年 <b>%s</b>万</div>' % (wan(s25), wan(s26))
            + '<div class="ft1" style="color:%s">Δ %s万 %s</div>' % (dcol, wan_s(d), arrow)
            + '</div></div>')
    return ''.join(out)


# ── ③ 同比增减条形 ───────────────────────────────────────────
def bar_chart():
    rows = sorted(SKUS, key=lambda r: (sum(r[2]) - sum(r[1])), reverse=True)
    W = 1380
    rh = 36
    top = 44
    H = top + rh * len(rows) + 46
    labw = 206
    x0, x1 = labw + 8, W - 34
    zx = (x0 + x1) / 2.0
    DOM = 470000
    span = (x1 - x0) / 2.0

    def pos(v):
        return zx + (v / float(DOM)) * span

    o = ['<svg class="fig bars" viewBox="0 0 %d %d" width="100%%" role="img" '
         'aria-label="各货号 2026 减 2025 同比增减横向条形图">' % (W, H)]
    # 背景分区：正区/负区极淡底色
    o.append('<rect x="%.1f" y="%d" width="%.1f" height="%d" fill="#F3FAF5"/>'
             % (zx, top - 16, x1 - zx, rh * len(rows) + 8))
    o.append('<rect x="%.1f" y="%d" width="%.1f" height="%d" fill="#FDF5F4"/>'
             % (x0, top - 16, zx - x0, rh * len(rows) + 8))
    # 零线
    o.append('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="#334155" stroke-width="1.8"/>'
             % (zx, top - 20, zx, top + rh * len(rows) - 12))
    for i, r in enumerate(rows):
        d = sum(r[2]) - sum(r[1])
        y = top + i * rh
        cy = y + 11
        o.append('<text x="%d" y="%.1f" text-anchor="end" font-size="13" fill="#334155" '
                 'font-weight="600">%s</text>' % (labw, cy + 4.5, r[0]))
        col = GREEN if d > 0 else RED
        bw = abs(d) / float(DOM) * span
        txt = wan_s(d)
        est = len(txt) * 8.0 + 6          # 12.5px 粗体宽度估算（含 − 号）
        vx, anchor, vfill = None, 'start', col
        if d >= 0:
            o.append('<rect x="%.1f" y="%.1f" width="%.1f" height="22" rx="3" fill="%s"/>'
                     % (zx, y, bw, col))
            ex = zx + bw
            if ex + est + 10 <= x1:
                vx, anchor = ex + 8, 'start'
            elif bw >= est + 16:                 # 贴近右边界 → 值标进条内（白字）
                vx, anchor, vfill = x1 - 6, 'end', '#fff'
            else:
                vx, anchor = ex + 8, 'start'
        else:
            o.append('<rect x="%.1f" y="%.1f" width="%.1f" height="22" rx="3" fill="%s"/>'
                     % (zx - bw, y, bw, col))
            ex = zx - bw
            if ex - est - 10 >= labw + 8:
                vx, anchor = ex - 8, 'end'
            elif bw >= est + 16:                 # 长负条 → 值标进条内，避免压住左侧货号
                vx, anchor, vfill = ex + 9, 'start', '#fff'
            else:
                vx, anchor = ex - 8, 'end'
        o.append('<text x="%.1f" y="%.1f" text-anchor="%s" font-size="12.5" fill="%s" '
                 'font-weight="700">%s</text>' % (vx, cy + 4.5, anchor, vfill, txt))
    # 轴刻度
    for v in (-400000, -200000, 0, 200000, 400000):
        x = pos(v)
        o.append('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="#94A3B8" stroke-width="1"/>'
                 % (x, top + rh * len(rows) - 12, x, top + rh * len(rows) - 7))
        o.append('<text x="%.1f" y="%.1f" text-anchor="middle" font-size="12" fill="#8A94A6">%d万</text>'
                 % (x, H - 12, v // 10000))
    o.append('<text x="%.1f" y="%d" text-anchor="middle" font-size="12" fill="#8A94A6" '
             'font-weight="600">← 减少　　Δ（2026 − 2025，万元）　　增加 →</text>'
             % (zx, top - 26))
    o.append('</svg>')
    return ''.join(o)


# ── ④ 结构变迁带 ─────────────────────────────────────────────
def struct_block():
    tot = len(LOST) + len(CONT) + len(NEW)
    segs = [('流失', len(LOST), 'lost', RED), ('延续', len(CONT), 'cont', BLUE),
            ('新增', len(NEW), 'new', GREEN)]
    bar = ''.join('<div class="sb-seg" style="width:%.4f%%;background:%s">%s %d</div>'
                  % (n * 100.0 / tot, c, t, n) for t, n, k, c in segs)
    cols = []
    for t, n, k, c in segs:
        names = LOST if k == 'lost' else (CONT if k == 'cont' else NEW)
        chips = ''.join(_chip_html(c, x) for x in names)
        sub = {'lost': '仅 2025 有销售', 'cont': '两年都有销售', 'new': '仅 2026 有销售'}[k]
        cols.append('<div class="scol" style="border-top-color:%s">'
                    '<div class="scol-hd"><span class="scol-n" style="color:%s">%d</span>'
                    '<span class="scol-t">%s</span></div>'
                    '<div class="scol-sub">%s</div>'
                    '<div class="chips">%s</div></div>' % (c, c, n, t, sub, chips))
    return '<div class="struct-bar">%s</div><div class="struct-cols">%s</div>' % (bar, ''.join(cols))


# ── ⑤ 业务注解与数据印证 ─────────────────────────────────────
def annot_block():
    order = ['HPV', 'ZJ4', 'M4211-3']
    rows = []
    for aid in order:
        a = ANNOTS[aid]
        rows.append(
            '<div class="annot-row" style="border-left-color:%s">'
            '<div class="annot-hd"><span class="annot-type" style="color:%s">%s</span>'
            '<span class="annot-scope">%s</span></div>'
            '<div class="annot-text">「%s」</div>'
            '<div class="annot-proof"><span class="annot-proof-t">数据印证</span>%s</div>'
            '</div>' % (a['color'], a['color'], a['type'], a['scope'], a['text'], a['proof']))
    return '<div class="annot-rows">%s</div>' % ''.join(rows)


# ── 页面 ─────────────────────────────────────────────────────
def build_html():
    s25 = sum(T25)
    s26 = sum(T26)
    d = s26 - s25
    yoy = d / float(s25) * 100.0
    import json
    data = {'total25': s25, 'total26': s26, 'diff': d, 'yoy': round(yoy, 1),
            'q25': T25, 'q26': T26,
            'skus': [{'sku': s, 'v25': a, 'v26': b, 's25': sum(a), 's26': sum(b)}
                     for s, a, b in SKUS],
            'lost': LOST, 'cont': CONT, 'new': NEW}
    dj = json.dumps(data, ensure_ascii=False).replace('</', '<\\/')

    css = """
:root{--blue:#1852B6;--grey:#94A3B8;--green:#0F9D58;--red:#D93025;--amber:#E8830C;
  --ink:#1F2937;--sub:#5F6B7C;--line:#E3E8F0}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:#EEF2F7}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
  color:var(--ink);-webkit-font-smoothing:antialiased}
.wrap{width:1440px;margin:0 auto;background:#fff;padding:0 0 26px}
.pad{padding:0 34px}
.title{padding:22px 34px 14px;border-bottom:1px solid var(--line)}
.title h1{margin:0;font-size:27px;letter-spacing:.3px}
.title h1 span{color:var(--blue)}
.title .sub{margin-top:7px;font-size:13px;color:var(--sub);line-height:1.6}
.concl{margin:14px 34px 0;border-left:6px solid var(--blue);background:#F3F7FE;
  border-radius:0 8px 8px 0;padding:11px 16px;font-size:14.5px;line-height:1.55;font-weight:600;color:#143A7E}
.concl b{color:var(--red)}.concl i{color:var(--green);font-style:normal}
.sec{margin:24px 34px 0;border:1px solid var(--line);border-radius:12px;overflow:hidden;
  box-shadow:0 1px 3px rgba(24,82,182,.06)}
.sec-hd{display:flex;align-items:center;gap:14px;padding:12px 18px;background:#F7F9FD;
  border-bottom:1px solid var(--line);flex-wrap:wrap}
.sec-hd .no{width:24px;height:24px;border-radius:6px;background:var(--blue);color:#fff;font-size:13px;
  font-weight:700;display:flex;align-items:center;justify-content:center;flex:none}
.sec-hd h2{margin:0;font-size:16.5px;font-weight:700}
.sec-hd .note{font-size:12.5px;color:#7B8798;font-weight:400}
.sec-bd{padding:14px 16px 16px}
.bignum{margin-left:auto;display:flex;align-items:center;gap:9px}
.bignum .v{font-size:31px;font-weight:800;color:var(--red);letter-spacing:-.5px;line-height:1}
.bignum .l{font-size:11.5px;color:#7B8798;line-height:1.35}
.legend{margin-left:auto;display:flex;align-items:center;gap:16px;font-size:12.5px;color:#4A5568}
.legend i{display:inline-block;width:26px;height:0;border-top-width:3px;border-top-style:solid;
  vertical-align:middle;margin-right:6px}
.grid{display:grid;grid-template-columns:repeat(5,1fr);gap:13px}
.card{border:1px solid var(--line);border-left:4px solid var(--blue);border-radius:9px;
  padding:9px 7px 8px;background:#fff}
.card.hl{background:#FCFDFF;box-shadow:0 1px 5px rgba(24,82,182,.10)}
.card-hd{display:flex;align-items:center;gap:6px;padding:0 4px 3px;min-height:20px;flex-wrap:wrap}
.card-hd .sku{font-size:12.5px;font-weight:700;color:#1B2A41;letter-spacing:-.2px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hl-badge{margin-left:auto;font-size:9.5px;color:#fff;border-radius:9px;padding:1.5px 7px;
  font-weight:700;white-space:nowrap;flex:none}
.note-badge{display:inline-block;font-size:9px;font-weight:700;color:#fff;border-radius:8px;
  padding:1px 6px;white-space:nowrap;flex:none;cursor:help;line-height:1.5;vertical-align:middle}
.card-ft{padding:4px 4px 0;line-height:1.45}
.ft2{font-size:11px;color:#5F6B7C;white-space:nowrap}
.ft2 b{color:#1B2A41}
.ft1{font-size:12px;font-weight:700}
.struct-bar{display:flex;height:34px;border-radius:8px;overflow:hidden;margin-bottom:14px}
.sb-seg{display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;
  font-weight:700;letter-spacing:.5px}
.struct-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
.scol{border:1px solid var(--line);border-top:4px solid var(--blue);border-radius:9px;padding:11px 13px 13px}
.scol-hd{display:flex;align-items:baseline;gap:7px}
.scol-n{font-size:26px;font-weight:800;line-height:1}
.scol-t{font-size:14px;font-weight:700;color:#334155}
.scol-sub{font-size:11.5px;color:#8A94A6;margin:4px 0 9px}
.chips{display:flex;flex-wrap:wrap;gap:5px}
.chip{font-size:11.5px;border:1px solid;border-radius:6px;padding:2.5px 7px;background:#fff;
  font-weight:600}
.annot-rows{display:flex;flex-direction:column;gap:10px}
.annot-row{border:1px solid var(--line);border-left:4px solid var(--blue);border-radius:8px;
  padding:9px 13px 11px;background:#fff}
.annot-hd{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.annot-type{font-size:14px;font-weight:700}
.annot-scope{font-size:11.5px;color:#8A94A6}
.annot-text{font-size:13px;color:#334155;line-height:1.65;margin-top:5px}
.annot-proof{font-size:12.5px;color:#1B2A41;margin-top:7px;background:#F7F9FD;border-radius:6px;
  padding:6px 10px;line-height:1.55}
.annot-proof-t{display:inline-block;font-size:11px;font-weight:700;color:#1852B6;
  border:1px solid #C7D7F2;border-radius:5px;padding:0 6px;margin-right:8px}
.foot{margin:22px 34px 0;font-size:11.5px;color:#8A94A6;line-height:1.7;border-top:1px solid var(--line);
  padding-top:12px}
"""

    h = []
    h.append('<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">')
    h.append('<meta name="viewport" content="width=1440">')
    h.append('<title>天隆集团 2025 vs 2026 分季度趋势看板</title>')
    h.append('<style>%s</style></head><body><div class="wrap">' % css)

    h.append('<div class="title"><h1>天隆集团 · <span>2025 vs 2026</span> 分季度趋势看板</h1>'
             '<div class="sub">口径：标准销售订单 · 已审核 · 非赠品 · 排除关联方 ｜ 季度按合同日期 ｜ '
             '2026 数据截止 2026-09-30（Q4 无数据）</div></div>')

    h.append('<div class="concl">%s</div>' % CONCLUSION_HTML)

    # ① 季度总量趋势
    h.append('<div class="sec" id="blk1"><div class="sec-hd"><div class="no">1</div>'
             '<h2>季度总量趋势</h2>'
             '<span class="note">2025 虚线 · 2026 实线 ｜ y 轴 0–120 万</span>'
             '<div class="legend"><span><i style="border-top-color:%s;border-top-style:dashed"></i>2025</span>'
             '<span><i style="border-top-color:%s;border-top-style:solid"></i>2026</span></div>'
             '<div class="bignum"><div class="v" id="kpi-yoy">−52.7%%</div>'
             '<div class="l">同比<br>（2026 vs 2025）</div></div></div>'
             '<div class="sec-bd">%s</div></div>' % (GREY, BLUE, main_line_chart()))

    # ② 货号趋势小图阵
    h.append('<div class="sec" id="blk2"><div class="sec-hd"><div class="no">2</div>'
             '<h2>货号分季度趋势小图阵</h2>'
             '<span class="note">虚线=2025 / 实线=2026 ｜ 同一 y 轴比例（0–40 万），跨货号可比 ｜ '
             '按 2026 金额降序</span>'
             '<div class="legend"><span><i style="border-top-color:%s;border-top-style:dashed"></i>2025</span>'
             '<span><i style="border-top-color:%s;border-top-style:solid"></i>2026</span></div></div>'
             '<div class="sec-bd"><div class="grid" id="grid">%s</div></div></div>'
             % (GREY, BLUE, spark_cards()))

    # ③ 同比增减条形
    h.append('<div class="sec" id="blk3"><div class="sec-hd"><div class="no">3</div>'
             '<h2>同比增减条形</h2>'
             '<span class="note">Δ = 2026 − 2025 ｜ 正绿负红 ｜ 降幅集中 M5244-ZJ4-TP / MD2071-2</span>'
             '<div class="legend"><span><i style="border-top-color:%s;border-top-style:solid"></i>增长</span>'
             '<span><i style="border-top-color:%s;border-top-style:solid"></i>下滑</span></div></div>'
             '<div class="sec-bd">%s</div></div>' % (GREEN, RED, bar_chart()))

    # ④ 结构变迁带
    h.append('<div class="sec" id="blk4"><div class="sec-hd"><div class="no">4</div>'
             '<h2>结构变迁带</h2>'
             '<span class="note">按货号级「两年是否有销售」分类 ｜ 流失 7 / 延续 9 / 新增 8 '
             '（共 24 个 SKU）</span></div>'
             '<div class="sec-bd">%s</div></div>' % struct_block())

    # ⑤ 业务注解与数据印证
    h.append('<div class="sec" id="blk5"><div class="sec-hd"><div class="no">5</div>'
             '<h2>业务注解与数据印证</h2>'
             '<span class="note">用户补充业务注解（原话逐字入档）＋ 权威数据印证 ｜ '
             '仅补充归因，不改动任何金额</span></div>'
             '<div class="sec-bd">%s</div></div>' % annot_block())

    h.append('<div class="foot">数据来源：<b>刘子研三季度数据（截止至 9.30）</b> + 全公司 ERP 销售订单'
             '（2026-01-01 ~ 2026-09-30）交叉核对；生成脚本 '
             '<b>脚本/build_tianlong_trend.py</b>，验收脚本 <b>脚本/verify_tianlong_trend.py</b>。<br>'
             '全年合计：2025 <b id="kpi-t25">259.4</b> 万 ｜ 2026 <b id="kpi-t26">122.8</b> 万 ｜ '
             '差额 <b id="kpi-diff">−136.6</b> 万；2026 未含 Q4，同比不宜按完整年度解读。</div>')

    js = ('<script>var DATA=' + dj + ';'
          '(function(){var D=DATA;var dl=D.total26-D.total25;'
          'var y=dl/D.total25*100;'
          'function w(v){return (v/10000).toFixed(1);}'
          'function s(v){return (v<0?"\\u2212":(v>0?"+":""))+(Math.abs(v)/10000).toFixed(1);}'
          'document.getElementById("kpi-yoy").textContent=(y<0?"\\u2212":"")+Math.abs(y).toFixed(1)+"%";'
          'document.getElementById("kpi-t25").textContent=w(D.total25);'
          'document.getElementById("kpi-t26").textContent=w(D.total26);'
          'document.getElementById("kpi-diff").textContent=s(dl);'
          'window.__recalc={s25:D.total25,s26:D.total26,diff:dl,yoy:Math.round(y*10)/10};'
          '})();</script>')
    h.append(js)

    h.append('</div></body></html>')
    html = ''.join(h)
    html = BK.inject(html, theme='light', tag='经营分析 · 天隆集团')
    return html


# ── 守卫 ─────────────────────────────────────────────────────
def guard(html):
    svgs = re.findall(r'<svg\b.*?</svg>', html, re.S)
    assert len(svgs) >= 17, 'SVG 数量异常：%d' % len(svgs)
    for s in svgs:
        assert '<title' not in s, 'SVG 内出现 <title>（会静默吞图形）'
        assert '<br' not in s, 'SVG 内出现 <br>（第二行会丢）'
        for t in ('<b>', '</b>', '<span', '<i>', '<div', '<p>', '<ul'):
            assert t not in s, 'SVG 内混入 HTML 标签 %s（会 breakout）' % t
    nspark = len(re.findall(r'class="spark"', html))
    assert nspark == len(SKUS) == 15, 'sparkline 数量 %d != 15' % nspark
    nrect = sum(s.count('<rect') for s in svgs)
    assert nrect > 10, 'rect 太少：%d' % nrect
    # 零外链（标签级依赖）
    for p in (r'<link[^>]+href="https?://', r'<script[^>]+src=', r'<img[^>]+src="https?://', r'@import'):
        assert not re.findall(p, html), '外部依赖：%s' % p
    assert 'data:image/png;base64' in html, 'Logo 未内联'
    assert '守护生命健康' in html, 'slogan 缺失'
    assert not re.findall(r'@@\w+@@|__[A-Z_]+__', html), '残留占位符'
    return {'svg': len(svgs), 'spark': nspark, 'rect': nrect}


def export_png(html_path, png_path, width=1440):
    from playwright.sync_api import sync_playwright
    from PIL import Image
    with sync_playwright() as p:
        b = p.chromium.launch(channel='chrome')
        pg = b.new_page(viewport={'width': width, 'height': 1000}, device_scale_factor=1)
        pg.goto('file://' + html_path)
        pg.wait_for_timeout(700)
        pg.screenshot(path=png_path, full_page=True)
        b.close()
    im = Image.open(png_path)
    print('[png] %s  %dx%d' % (png_path, im.size[0], im.size[1]))
    return im.size


def main():
    html = build_html()
    info = guard(html)
    with open(OUT_HTML, 'w', encoding='utf-8') as f:
        f.write(html)
    print('✅ 生成 %s（%.1f KB）' % (OUT_HTML, os.path.getsize(OUT_HTML) / 1024.0))
    print('   守卫：SVG %d 个 / sparkline %d 个 / rect %d 个 / 零外链 / Logo+slogan OK'
          % (info['svg'], info['spark'], info['rect']))
    s25, s26 = sum(T25), sum(T26)
    print('   复算：2025 %d（%.1f万） ｜ 2026 %d（%.1f万） ｜ 差额 %d ｜ 同比 %.1f%%'
          % (s25, s25 / 10000, s26, s26 / 10000, s26 - s25, (s26 - s25) / float(s25) * 100))
    if '--png' in sys.argv:
        export_png(OUT_HTML, OUT_PNG)


if __name__ == '__main__':
    main()
