#!/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

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


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)}条")

    # 逐单查询 SF —— checkPhoneNo（收件人手机后4位）必须传，且批量只校验首个单号 → 必须逐单
    all_results = {}
    for idx, o in enumerate(tracking_orders):
        tn = o["物流单号"].strip()
        digits = "".join(c for c in str(o.get("收货人联系电话#") or "") if c.isdigit())
        phone4 = digits[-4:] if len(digits) >= 4 else None
        try:
            result = sf_query_routes([tn], token, phone4)
            if isinstance(result, dict) and "error" not in result:
                routes = parse_routes(result)
                if routes:
                    all_results.update(routes)
                else:
                    all_results[tn] = {"mailNo": tn, "status": "no_data", "statusLabel": "暂无数据",
                                       "lastRoute": None, "routes": [], "reason": "", "routeCount": 0}
            else:
                all_results[tn] = {"mailNo": tn, "status": "error", "statusLabel": "查询异常",
                                   "lastRoute": None, "routes": [], "reason": str(result), "routeCount": 0}
        except Exception as e:
            all_results[tn] = {"mailNo": tn, "status": "error", "statusLabel": "查询异常",
                               "lastRoute": None, "routes": [], "reason": str(e), "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}%)")
    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("合同编号#", ""),
            # 飞书销售订单的「发货日期」(MQL field_98f9e8)：ERP 导出缺失/滞后时兜底
            "mcpShipDate": (o.get("发货日期") or "")[:10],
            "shipment": route_info
        })
    # 出口处统一挂 ERP 预计发货日期 —— 放在这里而不是 main()，任何调用方（cron/watchdog/别的脚本）都覆盖得到
    return attach_erp_ship_dates(output)

def main():
    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()
