#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
顺丰物流轨迹数据管道
用法: python3 shipment_pipeline.py          # 从MCP拉数据+顺丰API
      python3 shipment_pipeline.py --mock   # 用mock数据测试

输出: 数据/shipment_status.json
"""
import json, hashlib, base64, time, uuid, urllib.request, urllib.parse, os, sys, re

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(ROOT, '数据')
OUTPUT = os.path.join(DATA_DIR, 'shipment_status.json')

# 顺丰凭证
SF_PARTNER_ID = "BRSWK5CQ08HV"
SF_SECRET = "V7JDT0azGX93pnIXRyZe9zvlzY6fk1rC"
SF_TOKEN_URL = "https://sfapi.sf-express.com/oauth2/accessToken"
SF_API_URL = "https://bspgw.sf-express.com/std/service"

def sf_get_token():
    """获取顺丰 accessToken"""
    body = urllib.parse.urlencode({
        "partnerID": SF_PARTNER_ID,
        "secret": SF_SECRET,
        "grantType": "password"
    }).encode("utf-8")
    req = urllib.request.Request(SF_TOKEN_URL, data=body,
        headers={"Content-Type": "application/x-www-form-urlencoded"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        result = json.loads(resp.read().decode("utf-8"))
    if result.get("apiResultCode") != "A1000":
        raise Exception(f"SF token failed: {result}")
    return result["accessToken"]

def sf_query_routes(tracking_numbers, access_token, check_phone=None):
    """查询物流轨迹。check_phone = 收件人手机后4位，必传（否则返回"月结卡号不匹配"）；
    ⚠️ 批量查询只校验首个单号 → 必须逐单调用（见技能 sf-logistics-debug）。"""
    msg = {
        "language": "zh-CN",
        "trackingType": 1,
        "trackingNumber": tracking_numbers,
        "methodType": 1
    }
    if check_phone:
        msg["checkPhoneNo"] = check_phone
    msg_data = json.dumps(msg, ensure_ascii=False)

    timestamp = str(int(time.time()))
    request_id = str(uuid.uuid4()).replace("-", "")

    body = urllib.parse.urlencode({
        "partnerID": SF_PARTNER_ID,
        "requestID": request_id,
        "serviceCode": "EXP_RECE_SEARCH_ROUTES",
        "timestamp": timestamp,
        "accessToken": access_token,
        "msgData": msg_data
    }).encode("utf-8")

    req = urllib.request.Request(SF_API_URL, data=body,
        headers={"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        result = json.loads(resp.read().decode("utf-8"))

    if result.get("apiResultCode") != "A1000":
        return {"error": result.get("apiErrorMsg", "unknown"), "code": result.get("apiResultCode")}

    inner = json.loads(result.get("apiResultData", "{}"))
    return inner

def parse_routes(routes_data):
    """解析路由信息，提取关键状态"""
    if not routes_data.get("success"):
        return {"routes": [], "status": "error", "error": routes_data.get("errorMsg", "")}

    route_resps = routes_data.get("msgData", {}).get("routeResps", [])
    results = {}
    for resp in route_resps:
        mail_no = resp.get("mailNo", "")
        routes = resp.get("routes", [])
        reason = resp.get("reasonRemark", "")

        # 判断状态
        status = "unknown"
        last_route = None
        if routes:
            last_route = routes[-1]
            remark = last_route.get("remark", "")
            op = str(last_route.get("opCode", ""))
            # opCode 优先（最可靠）：80=已签收, 204/30=派送/运送中, 54=已揽收
            if op == "80" or "签收" in remark or "已派送至" in remark or "派送成功" in remark:
                status = "delivered"
            elif op in ("204", "30") or "派送" in remark:
                status = "delivering"
            elif op == "54" or "揽收" in remark:
                status = "picked_up"
            else:
                status = "in_transit"
        elif reason:
            status = "no_data"  # 可能是旧单号或权限问题

        results[mail_no] = {
            "mailNo": mail_no,
            "status": status,
            "statusLabel": {
                "delivered": "已签收",
                "delivering": "派送中",
                "picked_up": "已揽收",
                "in_transit": "运输中",
                "no_data": "暂无数据",
                "unknown": "未知",
                "error": "查询异常"
            }.get(status, "未知"),
            "lastRoute": last_route,
            "routes": routes,
            "reason": reason,
            "routeCount": len(routes)
        }
    return results

# 离职人员订单归属映射：离职销售的历史订单划转到接替人
RESIGN_REASSIGN = {
    "张立娅": ("刘子研", "liuziyan@biori.com"),
    "黄明月": ("刘子研", "liuziyan@biori.com"),
}

def reassign_creator(o):
    """把离职人员的创建者/邮箱映射到接替人"""
    creator = o.get("创建者", "") or ""
    email = o.get("创建者邮箱", "") or ""
    if creator in RESIGN_REASSIGN:
        o["创建者"], o["创建者邮箱"] = RESIGN_REASSIGN[creator]
    elif email.lower() in ("zhangliya@biori.com", "huangmingyue@biori.com"):
        o["创建者"], o["创建者邮箱"] = RESIGN_REASSIGN["张立娅"] if email.lower() == "zhangliya@biori.com" else RESIGN_REASSIGN["黄明月"]
    return o


ERP_SHIP_DATES = os.path.join(DATA_DIR, 'erp_ship_dates.json')
_MCP_SHIP_CACHE = None


def _mcp_ship_dates():
    """飞书销售订单自带「发货日期」(field_98f9e8) 的「单号→日期」表 —— ERP 导出滞后时的兜底源。

    从 数据/mcp_orders_input.json 读（由 数据/pull_orders.py --months N 生成，已按单号取非空）。
    """
    global _MCP_SHIP_CACHE
    if _MCP_SHIP_CACHE is None:
        _MCP_SHIP_CACHE = {}
        try:
            with open(os.path.join(DATA_DIR, 'mcp_orders_input.json'), encoding='utf-8') as f:
                for x in json.load(f):
                    d = (x.get('发货日期') or '')[:10] if isinstance(x.get('发货日期'), str) else ''
                    if x.get('单据编号') and d:
                        _MCP_SHIP_CACHE[x['单据编号']] = d
        except Exception as e:
            print(f'⚠️ 读取飞书发货日期失败: {e}')
    return _MCP_SHIP_CACHE


def attach_erp_ship_dates(output):
    """给每个订单挂 ERP 订单级 `发货日期`（= 预计发货日期）+ 出库进度。

    来源：数据/erp_ship_dates.json（由 脚本/build_erp_ship_dates.py 生成）。
    语义：未出库的单 = 计划/预计发货日；已出库的单 ≈ 实际发货日（该日期为滚动计划，改期会往后推）。
    """
    try:
        with open(ERP_SHIP_DATES, encoding='utf-8') as f:
            m = json.load(f).get('orders') or {}
    except Exception as e:
        print(f'⚠️ 未读到 ERP 发货日期索引（{ERP_SHIP_DATES}）: {e}')
        for o in output:
            o.setdefault('planShipDate', '')
        return output
    hit = hit_mcp = 0
    for o in output:
        e = m.get(o.get('orderId'))
        if e:
            o['planShipDate'] = e.get('d', '')
            o['shippedQty'] = e.get('out', 0)
            o['notifyQty'] = e.get('notify', 0)
            o['shipped'] = (e.get('out') or 0) > 0
            o['planShipFrom'] = 'erp'
            # 订单类型（标准/测试/预测销售订单）——MCP 侧没有这个字段，只能从 ERP 带过来
            if e.get('ty'):
                o['orderType'] = e['ty']
            hit += 1
        elif o.get('mcpShipDate') or _mcp_ship_dates().get(o.get('orderId')):
            # ERP 导出还没含这张单（导出有截止日）→ 用飞书销售订单自带的发货日期兜底
            o['planShipDate'] = o.get('mcpShipDate') or _mcp_ship_dates().get(o.get('orderId'), '')
            o['shipped'] = None          # 未知，不参与「未发货/超期」判定
            o['planShipFrom'] = 'mcp'
            hit_mcp += 1
        else:
            o['planShipDate'] = ''
            o['planShipFrom'] = ''
    print(f'预计发货日期：ERP {hit} 单 + 飞书兜底 {hit_mcp} 单 = {hit + hit_mcp}/{len(output)} 单')
    return output


# ── 合并发出关联 ──────────────────────────────────────────────────
# 背景：客户常把多张订单合成一个包裹发出，运单号只录在其中一张上，其余订单在
# 工作台就显示「物流信息缺失」。飞书「备注(内部)#」会写「与XX单合并发货」
# 「合并订单发货：XX」—— 据此把无单号订单挂到对方运单上，工作台可一键跳轨迹。
_ORDER_ID_RE = re.compile(r'(ZHBR\d{6,}|FO\d{6,}|CGDD\d+)')
_MERGE_RE = re.compile(r'合并|一起发|一并发|同单发|并单发|上一单|上一个订单|同[^。；\n]{0,20}?(?:单|订单)[^。；\n]{0,12}?发')

def attach_merge_links(output):
    """为无运单号订单建立「并入对方运单」的关联（仅采信备注里的显式指向）。"""
    byid = {o['orderId']: o for o in output}
    fwd, back = {}, {}
    for o in output:
        txt = ((o.get('remarkInternal') or '') + ' ' + (o.get('remarkCustomer') or '')).strip()
        if not txt or not _MERGE_RE.search(txt):
            continue
        tg = set(x for x in _ORDER_ID_RE.findall(txt) if x != o['orderId'])
        if tg:
            fwd[o['orderId']] = tg
            for t in tg:
                back.setdefault(t, set()).add(o['orderId'])
    n = 0
    for o in output:
        if (o.get('trackingNo') or '').strip():
            continue
        cand = sorted(set(list(fwd.get(o['orderId'], ())) + list(back.get(o['orderId'], ()))))
        picks = [byid[c] for c in cand
                 if c in byid and (byid[c].get('trackingNo') or '').strip()]
        if not picks:
            continue
        # 一个运单号可能被多张订单共用（如 032/033 同号）→ 取号最集中的那组，
        # 并按单号排序，保证同一份数据每次跑出的关联结果完全一致（曾出现随机漂移）。
        _cnt = {}
        for p in picks:
            _cnt[p['trackingNo']] = _cnt.get(p['trackingNo'], 0) + 1
        best_tn = sorted(_cnt.items(), key=lambda kv: (-kv[1], kv[0]))[0][0]
        same = sorted([p for p in picks if p['trackingNo'] == best_tn], key=lambda x: x['orderId'])
        pick = same[0]
        sh = pick.get('shipment') or {}
        o['mergedInto'] = {
            'orderId': pick['orderId'], 'trackingNo': pick['trackingNo'],
            'status': sh.get('status'), 'statusLabel': sh.get('statusLabel'),
            'lastRoute': sh.get('lastRoute'), 'customer': pick.get('customer'),
            'shipDate': pick.get('planShipDate') or pick.get('mcpShipDate') or '',
            'coMerged': [{'orderId': p['orderId'], 'trackingNo': p['trackingNo']} for p in same],
        }
        n += 1
    if n:
        print(f"合并发出关联: {n} 单无运单号订单已挂到对方运单上")
    return output

def process_orders(orders_data, token):
    """处理所有订单（含无物流单号的），批量查轨迹"""
    # 收集全部订单+有物流单号的去重
    all_orders = []
    seen_tn = set()
    tracking_orders = []  # 只用于SF批量查询
    for o in orders_data:
        o = reassign_creator(o)  # 离职人员划转
        tn = (o.get("物流单号") or "").strip()
        all_orders.append(o)
        if tn and tn not in seen_tn:
            seen_tn.add(tn)
            tracking_orders.append(o)

    print(f"全量: {len(all_orders)}条, 有物流单号: {len(tracking_orders)}条")

    # ★ 降级保护：读上一轮结果作底本 —— 查询失败时绝不用「暂无数据」覆盖已有的好轨迹
    #   起因（2026-09-30 用户报障「订单已到货但工作台物流信息缺失」）：本管道每轮对全部
    #   1300+ 单号做全量重查，任何一次限流/手机号不匹配都会把已经签收的单写成 no_data，
    #   直接把好数据覆盖掉 → 工作台显示「物流信息缺失」，与实际到货矛盾。
    prev_good = {}
    if os.path.exists(OUTPUT):
        try:
            with open(OUTPUT, encoding='utf-8') as f:
                _pv = json.load(f)
            for _o in (_pv.get('orders') or []):
                _t = (_o.get('trackingNo') or '').strip()
                _s = _o.get('shipment') or {}
                if _t and _s.get('routes'):
                    prev_good[_t] = _s
            print(f"降级底本: 历史有效轨迹 {len(prev_good)} 单")
        except Exception as _e:
            print(f"⚠️ 读取历史物流失败，降级保护本轮不生效: {_e}")

    # 逐单查询 SF —— checkPhoneNo（收件人手机后4位）必须传，且批量只校验首个单号 → 必须逐单
    all_results = {}
    n_kept = n_retry = n_phone_bad = 0
    for idx, o in enumerate(tracking_orders):
        tn = o["物流单号"].strip()
        # 号码规范化：全角→半角、去空格/横杠/括号、去国际前缀 0086/86
        import unicodedata as _ud
        _raw = _ud.normalize("NFKC", str(o.get("收货人联系电话#") or ""))
        digits = "".join(c for c in _raw if c.isdigit())
        if digits.startswith("0086"):
            digits = digits[4:]
        elif len(digits) == 13 and digits.startswith("86"):
            digits = digits[2:]
        phone4 = digits[-4:] if len(digits) >= 4 else None
        got, reason, phone_bad = None, "", False
        for attempt in range(3):                    # ★ 失败自动重试 2 次（限流多为瞬时）
            try:
                result = sf_query_routes([tn], token, phone4)
                if isinstance(result, dict) and "error" not in result:
                    pr = parse_routes(result)
                    info = pr.get(tn) if isinstance(pr, dict) else None
                    if info and info.get("routes"):
                        got = info
                        break
                    reason = (info or {}).get("reason") or "暂无路由信息"
                    if "手机号校验失败" in str(reason) or "月结卡号" in str(reason):
                        phone_bad = True
                        break                       # 凭证类错误，重试无意义
                else:
                    reason = str(result)
            except Exception as e:
                reason = str(e)
            if attempt < 2:
                n_retry += 1
                time.sleep(1.2 * (attempt + 1))

        if got:
            all_results[tn] = got
        else:
            old = prev_good.get(tn)
            if old:                                 # ★ 有历史好数据 → 沿用，不降级
                keep = dict(old)
                keep["stale"] = True
                keep["staleReason"] = reason
                keep["staleAt"] = time.strftime("%Y-%m-%d %H:%M:%S")
                all_results[tn] = keep
                n_kept += 1
            elif phone_bad:                         # 手机号不匹配 → 单独标记，便于修正而不是笼统「暂无数据」
                n_phone_bad += 1
                all_results[tn] = {"mailNo": tn, "status": "phone_mismatch", "statusLabel": "手机号校验失败",
                                   "lastRoute": None, "routes": [], "reason": reason,
                                   "reasonCode": "20026", "checkedPhone4": phone4, "routeCount": 0}
            else:
                all_results[tn] = {"mailNo": tn, "status": "no_data", "statusLabel": "暂无数据",
                                   "lastRoute": None, "routes": [], "reason": reason, "routeCount": 0}

        if idx + 1 < len(tracking_orders):
            time.sleep(0.15)

    n_ok = sum(1 for v in all_results.values() if v.get("routes"))
    rate = n_ok / max(len(tracking_orders), 1) * 100
    print(f"SF查询: {len(tracking_orders)}单, 有轨迹 {n_ok}单 ({rate:.0f}%)"
          f" ｜ 沿用历史 {n_kept} 单 ｜ 触发重试 {n_retry} 次 ｜ 手机号校验失败 {n_phone_bad} 单")
    if tracking_orders and rate < 50:
        print(f"⚠️ 顺丰查询成功率偏低({rate:.0f}%)，请检查 checkPhoneNo / 月结卡号 / API 配额")

    # 输出全部订单
    output = []
    for o in all_orders:
        tn = (o.get("物流单号") or "").strip()
        route_info = all_results.get(tn, {}) if tn else {}
        output.append({
            "orderId": o.get("单据编号", ""),
            "customer": o.get("购货单位#", ""),
            "mcpCustomer": o.get("销售订单关联客户", ""),
            "dept": o.get("销售部门", ""),
            "createDate": o.get("创建时间", ""),
            "recipient": o.get("收货人姓名", ""),
            "phone": o.get("收货人联系电话#", ""),
            "address": o.get("收货地址", ""),
            "trackingNo": tn,
            "amount": o.get("金额", ""),
            "taxTotal": o.get("金额", ""),
            "creator": o.get("创建者", ""),
            "creatorEmail": o.get("创建者邮箱", ""),
            "contractNo": o.get("合同编号#", ""),
            # ★ 备注：判断「无物流单号」是否为合并发出的唯一依据（飞书侧填写）
            "remarkInternal": o.get("备注内部", ""),
            "remarkCustomer": o.get("备注客户", ""),
            # 飞书销售订单的「发货日期」(MQL field_98f9e8)：ERP 导出缺失/滞后时兜底
            "mcpShipDate": (o.get("发货日期") or "")[:10],
            "shipment": route_info
        })
    output = apply_erp_amounts(output)
    # 出口处统一挂 ERP 预计发货日期 —— 放在这里而不是 main()，任何调用方（cron/watchdog/别的脚本）都覆盖得到
    return attach_merge_links(attach_erp_ship_dates(output))


# ================= P0：金额以 ERP 为准 =================
def load_erp_totals():
    """ERP 明细合计（orders_detail_2026.json）→ {单据编号: (价税合计, 行数)}
    原因：MCP 订单「金额」字段大量为 0 或只含首行，直接展示会低估/清零。"""
    import glob
    cands = sorted(glob.glob(os.path.join(DATA_DIR, 'orders_detail_*.json')), reverse=True)
    if not cands:
        return {}
    try:
        d = json.load(open(cands[0], encoding='utf-8'))
    except Exception as e:
        print(f"⚠️ ERP 明细读取失败: {e}")
        return {}
    out = {}
    for oid, v in (d.get('orders') or {}).items():
        lines = v.get('lines') or []
        out[str(oid)] = (float(v.get('total') or 0), len(lines), str(v.get('date') or '')[:10])
    print(f"ERP 金额索引: {len(out)} 单（{os.path.basename(cands[0])}）")
    return out


def apply_erp_amounts(orders, erp=None):
    """金额覆盖：ERP 有该单则以 ERP 价税合计为准；无则保留 MCP 并标 mcp。"""
    erp = erp if erp is not None else load_erp_totals()
    n_erp = n_mcp = n_zero = 0
    for o in orders:
        oid = str(o.get('orderId') or '')
        mcp = o.get('amount', o.get('taxTotal', ''))
        o['mcpAmount'] = mcp
        hit = erp.get(oid)
        if hit and hit[2]:
            o['contractDate'] = hit[2]      # ERP 合同日期：月份归属以此为准（MCP createDate 仅兜底）
        if hit and hit[0] > 0:
            o['amount'] = o['taxTotal'] = round(hit[0], 2)
            o['amountSource'] = 'ERP'
            o['erpLines'] = hit[1]
            n_erp += 1
        else:
            o['amountSource'] = 'MCP'
            n_mcp += 1
            try:
                if float(mcp or 0) == 0:
                    n_zero += 1
            except Exception:
                pass
    print(f"💰 金额来源：ERP {n_erp} 单 ｜ 仅MCP {n_mcp} 单（其中 MCP=0 仍为0的 {n_zero} 单）")
    return orders

def main():
    if "--amounts-only" in sys.argv:
        # 只重算金额（ERP 为准），不重查顺丰 —— 秒级
        with open(OUTPUT, encoding='utf-8') as f:
            data = json.load(f)
        data['orders'] = apply_erp_amounts(data.get('orders') or [])
        data['updated_at'] = time.strftime("%Y-%m-%d %H:%M:%S")
        data['amount_source_note'] = '金额=ERP价税合计；MCP原值见 mcpAmount'
        with open(OUTPUT, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"✅ 已重算金额 {OUTPUT}（{len(data['orders'])} 条，未查顺丰）")
        return

    if "--enrich-only" in sys.argv:
        # 只补挂 ERP 预计发货日期（不重查顺丰，秒级）——回填/日常增量用
        with open(OUTPUT, encoding='utf-8') as f:
            data = json.load(f)
        data['orders'] = attach_erp_ship_dates(data.get('orders') or [])
        data['updated_at'] = time.strftime("%Y-%m-%d %H:%M:%S")
        with open(OUTPUT, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"✅ 已回填 {OUTPUT}（{len(data['orders'])} 条，未查顺丰）")
        return

    if "--mock" in sys.argv:
        # Mock数据测试
        mock = [{
            "单据编号": "ZHBR260724027",
            "购货单位#": "圣湘生物",
            "销售订单关联客户": "圣湘生物科技股份有限公司",
            "物料名称": "2×FastAmpli Premix-UNG Ⅳ",
            "货号#": "M2181-4-NA-095203",
            "出货批号#": "20260417",
            "销售部门": "诊断原料大客户销售部",
            "单据类型": "标准销售订单",
            "状态": "未开始",
            "创建时间": "2026-07-24",
            "收货人姓名": "蔡琦",
            "收货人联系电话#": "17610556866",
            "收货地址": "湖南省长沙市岳麓区",
            "物流单号": "SF5112860018518"
        }]
        token = sf_get_token()
        output = process_orders(mock, token)
        print(f"Mock结果: {json.dumps(output, ensure_ascii=False, indent=2)[:500]}")
        return

    # 实际运行: 从MCP输出文件读取
    input_file = os.path.join(DATA_DIR, 'mcp_orders_input.json')
    if not os.path.exists(input_file):
        print(f"请先运行MCP拉取数据: 将订单JSON放入 {input_file}")
        print("或者使用 --mock 测试")
        sys.exit(1)

    with open(input_file, encoding='utf-8') as f:
        orders_data = json.load(f)

    token = sf_get_token()
    output = process_orders(orders_data, token)
    output = attach_erp_ship_dates(output)

    os.makedirs(DATA_DIR, exist_ok=True)
    with open(OUTPUT, 'w', encoding='utf-8') as f:
        json.dump({
            "updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
            "total": len(output),
            "orders": output
        }, f, ensure_ascii=False, indent=2)

    print(f"✅ 写入 {OUTPUT} ({len(output)} 条)")

if __name__ == '__main__':
    main()
