#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""顺丰路由推送回调（RoutePushService）

路径：POST /sf/route-back
- 顺丰推 JSON（配置里选 JSON），返回 {"return_code":"0000","return_msg":"success"}
- 幂等：同一运单 + 同一路由节点 id 只写一次
- 落库：数据/shipment_status.json（按 trackingNo 匹配订单，更新 shipment.routes / lastRoute / status）
- 原始报文留痕：数据/sf_route_push_log.jsonl（只追加，便于事后核对）
- 健康检查：GET /sf/route-back → {"ok":true,...}

启动：python3 脚本/sf_route_push_server.py --port 8892（2026-11-05 从8891迁出，避让BizLab英语工作台）
"""
import argparse
import fcntl
import json
import os
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
DATA = os.path.join(ROOT, '数据')
SHIP = os.path.join(DATA, 'shipment_status.json')
LOG = os.path.join(DATA, 'sf_route_push_log.jsonl')
LOCK = os.path.join(DATA, '.shipment_status.lock')

OP_MAP = {'80': ('delivered', '已签收'), '204': ('delivering', '派送中'),
          '30': ('delivering', '派送中'), '54': ('picked_up', '已揽收')}


def norm_status(op, remark):
    if op in OP_MAP:
        return OP_MAP[op]
    r = remark or ''
    if '签收' in r or '派送成功' in r:
        return 'delivered', '已签收'
    if '派送' in r:
        return 'delivering', '派送中'
    if '揽收' in r:
        return 'picked_up', '已揽收'
    return 'in_transit', '运输中'


def extract_routes(payload):
    """兼容多种包装：{WaybillRoute:[...]}, {WaybillRoute:{...}}, [...], {data:[...]}"""
    if isinstance(payload, list):
        return payload
    if not isinstance(payload, dict):
        return []
    for k in ('WaybillRoute', 'waybillRoute', 'data', 'routes'):
        v = payload.get(k)
        if isinstance(v, list):
            return v
        if isinstance(v, dict):
            return [v]
    # 单节点平铺
    if any(k in payload for k in ('mailno', 'mailNo', 'opCode')):
        return [payload]
    return []


def apply_nodes(nodes):
    """写入 shipment_status.json，返回 (更新单数, 新增节点数)"""
    if not nodes:
        return 0, 0
    with open(LOCK, 'a+') as lk:
        fcntl.flock(lk, fcntl.LOCK_EX)
        try:
            data = json.load(open(SHIP, encoding='utf-8'))
            orders = data.get('orders') or []
            idx = {}
            for o in orders:
                tn = (o.get('trackingNo') or '').strip()
                if tn:
                    idx.setdefault(tn, []).append(o)
            n_orders, n_nodes = 0, 0
            for nd in nodes:
                tn = str(nd.get('mailno') or nd.get('mailNo') or '').strip()
                if not tn:
                    continue
                hit = idx.get(tn) or []
                if not hit:
                    continue
                node_id = str(nd.get('id') or '') or '%s|%s|%s' % (
                    nd.get('acceptTime', ''), nd.get('opCode', ''), nd.get('remark', ''))
                route = {
                    'acceptTime': nd.get('acceptTime', ''),
                    'acceptAddress': nd.get('acceptAddress', ''),
                    'remark': nd.get('remark', ''),
                    'opCode': nd.get('opCode', ''),
                    'firstStatusCode': nd.get('firstStatusCode', ''),
                    'firstStatusName': nd.get('firstStatusName', ''),
                    'secondaryStatusCode': nd.get('secondaryStatusCode', ''),
                    'secondaryStatusName': nd.get('secondaryStatusName', ''),
                    '_id': node_id,
                    'source': 'sf_push',
                }
                for o in hit:
                    sh = o.setdefault('shipment', {}) or {}
                    routes = sh.get('routes') or []
                    if any((r.get('_id') == node_id) for r in routes):
                        continue
                    routes.append(route)
                    routes.sort(key=lambda r: str(r.get('acceptTime') or ''))
                    sh['routes'] = routes
                    sh['routeCount'] = len(routes)
                    sh['lastRoute'] = routes[-1]
                    st, label = norm_status(route.get('opCode'), route.get('remark'))
                    sh['status'], sh['statusLabel'] = st, label
                    sh['source'] = 'sf_push'
                    sh.pop('stale', None)
                    sh.pop('staleReason', None)
                    o['shipment'] = sh
                    n_nodes += 1
                if hit:
                    n_orders += 1
            data['updated_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
            tmp = SHIP + '.tmp'
            json.dump(data, open(tmp, 'w', encoding='utf-8'), ensure_ascii=False)
            os.replace(tmp, SHIP)
            return n_orders, n_nodes
        finally:
            fcntl.flock(lk, fcntl.LOCK_UN)


class Handler(BaseHTTPRequestHandler):
    protocol_version = 'HTTP/1.1'

    def _send(self, code, obj):
        body = json.dumps(obj, ensure_ascii=False).encode('utf-8')
        self.send_response(code)
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        # Tailscale funnel 的 --set-path 会剥掉前缀，所以任何路径都当健康检查
        return self._send(200, {'ok': True, 'service': 'sf-route-back', 'path': self.path,
                                'ts': time.strftime('%Y-%m-%d %H:%M:%S')})

    def do_POST(self):
        try:
            n = int(self.headers.get('Content-Length') or 0)
            raw = self.rfile.read(n).decode('utf-8', 'replace')
        except Exception as e:
            return self._send(200, {'return_code': '1000', 'return_msg': 'read error: %s' % e})

        try:
            payload = json.loads(raw) if raw.strip().startswith(('{', '[')) else {'raw': raw}
        except Exception:
            payload = {'raw': raw}

        # 留痕（只追加，永不覆盖）
        try:
            with open(LOG, 'a', encoding='utf-8') as f:
                f.write(json.dumps({'at': time.strftime('%Y-%m-%d %H:%M:%S'), 'payload': payload}, ensure_ascii=False) + '\n')
        except Exception:
            pass

        try:
            nodes = extract_routes(payload)
            n_orders, n_nodes = apply_nodes(nodes)
            print('[push] %s nodes=%d orders=%d nodes_written=%d' % (
                time.strftime('%H:%M:%S'), len(nodes), n_orders, n_nodes), flush=True)
        except Exception as e:
            print('[push][ERROR]', e, flush=True)
            # 仍返回成功，避免顺丰无限重推；原始报文已在日志里
        return self._send(200, {'return_code': '0000', 'return_msg': 'success'})

    def log_message(self, fmt, *args):
        pass


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--port', type=int, default=8892)
    a = ap.parse_args()
    os.makedirs(DATA, exist_ok=True)
    srv = ThreadingHTTPServer(('127.0.0.1', a.port), Handler)
    print('sf-route-back listening on 127.0.0.1:%d  /sf/route-back' % a.port, flush=True)
    srv.serve_forever()


if __name__ == '__main__':
    main()
