#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成「工作台物流信息缺失清单」自包含 HTML —— 按原因分类 + 责任销售分组 + 合并发出关联。

数据源：数据/shipment_status.json（唯一授权来源）+ 数据/mcp_orders_input.json（补飞书备注）
输出：销售分析/工作台物流信息缺失清单_<日期>.html
"""
import json, io, os, sys, time, re
from collections import defaultdict

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, '脚本'))
import brand_kit as BK

D = json.load(io.open(os.path.join(ROOT, '数据', 'shipment_status.json'), encoding='utf-8'))
O = D['orders']
try:
    _M = json.load(io.open(os.path.join(ROOT, '数据', 'mcp_orders_input.json'), encoding='utf-8'))
    REMARK = {r.get('单据编号'): ((r.get('备注内部') or ''), (r.get('备注客户') or '')) for r in _M}
except Exception:
    REMARK = {}
TODAY = time.strftime('%Y%m%d')
OUT = os.path.join(ROOT, '销售分析', '工作台物流信息缺失清单_%s.html' % TODAY)

# ── 分类 ──
g_merge, g_notn, g_phone, g_noroute = [], [], [], []
for o in O:
    tn = (o.get('trackingNo') or '').strip()
    s = o.get('shipment') or {}
    reason = str(s.get('reason') or '')
    if not tn:
        (g_merge if o.get('mergedInto') else g_notn).append(o)
    elif s.get('status') == 'phone_mismatch' or '手机号校验失败' in reason:
        g_phone.append(o)
    elif not s.get('routes'):
        g_noroute.append(o)

# ── 疑似合并（同客户 + 同发货日，且对方有运单号）—— 只提示，不自动关联 ──
_key = {}
for o in O:
    if (o.get('trackingNo') or '').strip():
        _key.setdefault((o.get('customer') or '', o.get('planShipDate') or ''), []).append(o)
g_suspect = []
for o in g_notn:
    k = (o.get('customer') or '', o.get('planShipDate') or '')
    if k in _key:
        g_suspect.append((o, _key[k][0]))
_suspect_ids = set(x[0]['orderId'] for x in g_suspect)
g_notn = [o for o in g_notn if o['orderId'] not in _suspect_ids]

def esc(x):
    return (str(x if x is not None else '')
            .replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;'))

def rmk(o, n=64):
    ri, rc = REMARK.get(o.get('orderId'), (o.get('remarkInternal') or '', o.get('remarkCustomer') or ''))
    t = re.sub(r'\s+', ' ', (ri or rc or '')).strip()
    return t[:n] + ('…' if len(t) > n else '')

def by_creator(lst):
    d = defaultdict(list)
    for o in lst:
        d[o.get('creator') or '(无下单人)'].append(o)
    return dict(sorted(d.items(), key=lambda kv: -len(kv[1])))

def rows(lst, action, extra=None):
    out = []
    for o in sorted(lst, key=lambda x: (x.get('creator') or '', x.get('orderId') or '')):
        out.append(
            '<tr>'
            '<td><b>%s</b></td><td class="truncate" title="%s">%s</td><td>%s</td><td>%s</td>'
            '<td>%s</td><td class="mono">%s</td><td class="mono">%s</td><td class="rm">%s</td>'
            '<td class="act">%s</td></tr>' % (
                esc(o.get('orderId')), esc(o.get('customer')), esc(o.get('customer')),
                esc(o.get('creator') or '—'), esc(o.get('dept') or '—'),
                esc(o.get('recipient') or '—'), esc(o.get('phone') or '—'),
                esc(o.get('trackingNo') or '—'),
                (extra(o) if extra else ''), esc(action)))
    return ''.join(out)

def section(title, icon, lst, action, color, note, extra=None):
    if not lst:
        return ''
    grp = by_creator(lst)
    grp_rows = ''.join(
        '<tr class="grp"><td colspan="9">👤 %s <span class="cnt">%d 单</span></td></tr>%s'
        % (esc(k), len(v), rows(v, action, extra)) for k, v in grp.items())
    return (
        '<section class="card">'
        '<h2 style="border-left:5px solid %s">%s %s <span class="badge" style="background:%s">%d 单</span></h2>'
        '<p class="note">%s</p>'
        '<table class="dt"><thead><tr><th>订单号</th><th>客户</th><th>下单人</th><th>部门</th>'
        '<th>收货人</th><th>收货手机</th><th>物流单号</th><th>备注 / 关联</th><th>处理动作</th></tr></thead>'
        '<tbody>%s</tbody></table></section>' % (color, icon, title, color, len(lst), note, grp_rows))

def _merge_extra(o):
    mi = o.get('mergedInto') or {}
    return ('<span class="link">🔗 并入 <b>%s</b></span> <span class="mono">%s</span> '
            '<span class="st">%s</span><div class="rmk">%s</div>' % (
                esc(mi.get('orderId')), esc(mi.get('trackingNo') or ''),
                esc(mi.get('statusLabel') or ''), esc(rmk(o))))

def _plain_extra(o):
    return '<div class="rmk">%s</div>' % (esc(rmk(o)) or '<span style="color:#cbd5e1">无备注</span>')

def _suspect_extra(o):
    b = next((p for a, p in g_suspect if a['orderId'] == o['orderId']), {}) or {}
    return ('<span class="link">? 疑似并入 <b>%s</b></span> <span class="mono">%s</span>'
            '<div class="rmk">%s</div>' % (esc(b.get('orderId') or ''),
                                           esc(b.get('trackingNo') or ''), esc(rmk(o))))

n_all = len(O)
total_bad = len(g_merge) + len(g_notn) + len(g_phone) + len(g_noroute) + len(g_suspect)

html = '''<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>工作台物流信息缺失清单</title><style>
:root{--bg:#f5f8fc;--card:#fff;--line:#e3eaf3;--ink:#1a2b45;--sub:#64748b;--muted:#94a3b8}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);
 font:14px/1.65 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
.wrap{max-width:1500px;margin:0 auto;padding:22px 20px 60px}
h1{font-size:25px;margin:0 0 6px;font-weight:900}
.lead{color:var(--sub);font-size:13.5px;margin:0 0 18px}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:12px;margin-bottom:20px}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.kpi .n{font-size:26px;font-weight:900;line-height:1.1}
.kpi .l{font-size:12.5px;color:var(--sub);margin-top:3px}
.card{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:17px 19px;margin-bottom:16px;
 box-shadow:0 1px 3px rgba(16,42,80,.05)}
.card h2{font-size:17px;margin:0 0 6px;padding-left:11px;font-weight:800}
.badge{color:#fff;font-size:12px;border-radius:20px;padding:2px 10px;font-weight:700;margin-left:6px;vertical-align:2px}
.note{color:var(--sub);font-size:12.5px;margin:0 0 12px}
table.dt{width:100%;border-collapse:collapse;font-size:12.5px}
table.dt th{background:#f0f5fb;color:#334155;text-align:left;padding:7px 9px;border-bottom:1.5px solid var(--line);
 white-space:nowrap;font-weight:700}
table.dt td{padding:6px 9px;border-bottom:1px solid #f1f5f9;vertical-align:top}
table.dt tr:not(.grp):hover{background:#f8fbff}
tr.grp td{background:#eaf1fb;font-weight:800;color:#1e3a6d;font-size:12.5px}
.cnt{font-weight:600;color:var(--sub);margin-left:6px}
td.truncate{max-width:215px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
td.mono{font-family:Consolas,Menlo,monospace;font-size:11.5px}
td.act{color:#b45309;font-weight:600;max-width:250px}
td.rm{max-width:340px}
.rmk{font-size:11.5px;color:#64748b;margin-top:3px;line-height:1.5}
.link{color:#6d28d9;font-weight:700}
.st{color:#16a34a;font-size:11.5px;margin-left:4px}
footer{margin-top:26px;padding-top:14px;border-top:1px solid var(--line);color:var(--muted);font-size:12px;text-align:center}
</style></head><body><div class="wrap">
<h1>📦 工作台物流信息缺失清单</h1>
<p class="lead">数据源：<code>数据/shipment_status.json</code>（更新于 __UPD__） ｜ 生成 __NOW__ ｜ 共 __NALL__ 单，其中 <b>__NBAD__ 单</b>在工作台看不到物流状态</p>
<div class="kpis">
 <div class="kpi"><div class="n" style="color:#7c3aed">__NM__</div><div class="l">🔗 合并发出 → 已挂到对方运单</div></div>
 <div class="kpi"><div class="n" style="color:#dc2626">__N1__</div><div class="l">未录物流单号</div></div>
 <div class="kpi"><div class="n" style="color:#0891b2">__N4__</div><div class="l">❓ 疑似合并（同客户同发货日）</div></div>
 <div class="kpi"><div class="n" style="color:#ea580c">__N2__</div><div class="l">手机号校验失败</div></div>
 <div class="kpi"><div class="n" style="color:#ca8a04">__N3__</div><div class="l">顺丰查无路由</div></div>
 <div class="kpi"><div class="n" style="color:#16a34a">__NOK__</div><div class="l">物流信息正常</div></div>
</div>
__SECM____SEC1____SEC4____SEC2____SEC3__
<footer>BIORI BIOTECH · 宝锐生物 ｜ 本清单由销售工作台数据管道自动生成，仅供内部使用，请勿外传</footer>
</div></body></html>'''

html = (html
        .replace('__UPD__', esc(D.get('updated_at') or '—'))
        .replace('__NOW__', time.strftime('%Y-%m-%d %H:%M'))
        .replace('__NALL__', str(n_all)).replace('__NBAD__', str(total_bad))
        .replace('__NM__', str(len(g_merge))).replace('__N1__', str(len(g_notn)))
        .replace('__N4__', str(len(g_suspect)))
        .replace('__N2__', str(len(g_phone))).replace('__N3__', str(len(g_noroute)))
        .replace('__NOK__', str(n_all - total_bad))
        .replace('__SECM__', section('合并发出（运单号记在对方单上）', '🔗', g_merge,
                 '无需处理 —— 工作台已自动显示对方运单号与状态', '#7c3aed',
                 '这些订单与另一张订单合成一个包裹发出，运单号只录在其中一张上。'
                 '工作台已按飞书「备注(内部)#」建立关联，<b>物流单号与物流状态两列直接显示合并运单的信息</b>。',
                 _merge_extra))
        .replace('__SEC1__', section('未录物流单号', '🚫', g_notn,
                 '在飞书销售订单补录「物流单号」（本期暂不追）', '#dc2626',
                 '飞书订单没有物流单号 → 工作台订单页「物流单号」「物流状态」两列都是空。'
                 '多为小件/自送/客户自提/漏录。', _plain_extra))
        .replace('__SEC4__', section('疑似合并（同客户 + 同发货日，待人工确认）', '❓',
                 [x[0] for x in g_suspect],
                 '人工确认后在该单飞书备注写明「与XX单合并发货」，系统下轮自动挂上', '#0891b2',
                 '本单无运单号，但同客户在同一天有一张<b>带运单号</b>的订单。因备注未写明合并，'
                 '<b>故意不自动关联</b>（避免误连）；确认后在备注补一句即可自动生效。', _suspect_extra))
        .replace('__SEC2__', section('手机号校验失败', '📵', g_phone,
                 '核对飞书订单「收货人联系电话」与顺丰运单登记号码是否一致', '#ea580c',
                 '顺丰轨迹接口强制校验「收件人手机后4位」。飞书里填的手机号与顺丰运单登记的号码不一致 → '
                 '接口直接拒绝，永远查不到轨迹。<b>改对手机号即可恢复</b>。', _plain_extra))
        .replace('__SEC3__', section('顺丰查无路由', '🔍', g_noroute,
                 '核对运单号归属（是否本月结账号 / 是否超查询期 / 是否录入有误）', '#ca8a04',
                 '运单号通过了手机号校验，但顺丰返回「暂无路由信息」—— 该单号不属于本月结账号、'
                 '或已超查询期、或录入有误。需向顺丰或发货人核实。', _plain_extra))
        )

html = BK.inject(html)
assert all(BK.verify(html).values()), 'brand_kit 校验未通过'
# 防复发：模板占位符必须全部被替换（曾漏 __BRAND_HEADER__ → 页面上露一行蓝字）
_leak = [t for t in ('__BRAND_HEADER__', '__UPD__', '__NOW__', '__NALL__', '__NBAD__', '__NM__',
                     '__N1__', '__N2__', '__N3__', '__N4__', '__NOK__',
                     '__SECM__', '__SEC1__', '__SEC2__', '__SEC3__', '__SEC4__') if t in html]
assert not _leak, '模板占位符未替换: %s' % _leak
io.open(OUT, 'w', encoding='utf-8').write(html)

print('✅ 已生成 %s' % OUT)
print('   总单 %d ｜ 合并发出 %d ｜ 未录单号 %d ｜ 疑似合并 %d ｜ 手机号失败 %d ｜ 查无路由 %d ｜ 正常 %d'
      % (n_all, len(g_merge), len(g_notn), len(g_suspect), len(g_phone), len(g_noroute), n_all - total_bad))
print('   文件大小 %.0f KB' % (os.path.getsize(OUT) / 1024))
