#!/usr/bin/env python3
"""
机票查询与下单脚本：支持航班查询（返回全部航班，不截断）、创建订单、支付校验与出票、订单状态查询、取消订单、退票。
乘机人信息从环境变量读取，用户需提前配置。
"""

import argparse
import json
import os
import sys
import time

import requests

# API 配置（新版网关，Bearer API_KEY 鉴权）

GATEWAY_BASE_URL = "https://www.sophnet.com/api"

TICKET_BASE = f"{GATEWAY_BASE_URL}/open-apis/ticket"
def _resolve_api_key() -> str:
    key = os.environ.get("SOPH_API_KEY", "").strip()
    if key:
        return key
    openclaw_path = os.path.expanduser("~/.openclaw/openclaw.json")
    try:
        with open(openclaw_path, "r", encoding="utf-8") as f:
            cfg = json.load(f)
        key = cfg.get("models", {}).get("providers", {}).get("sophnet", {}).get("apiKey", "")
        if key:
            return key
    except (OSError, json.JSONDecodeError):
        pass
    raise RuntimeError("未找到 SOPH_API_KEY：请设置环境变量 SOPH_API_KEY ")

API_KEY = _resolve_api_key()

# 乘机人信息从环境变量读取（用户需提前设置）
ENV_PASSENGER_NAME = "PASSENGER_NAME"
ENV_PASSENGER_MOBILE = "PASSENGER_MOBILE"
ENV_PASSENGER_CREDENTIAL_NO = "PASSENGER_CREDENTIAL_NO"
ENV_PASSENGER_GENDER = "PASSENGER_GENDER"

# 最近一次航班查询结果保存路径（用于 create-order 仅传航班号时读取）
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(os.path.expanduser("~"), ".openclaw", "flight-booking")
os.makedirs(DATA_DIR, exist_ok=True)
LAST_SEARCH_FILE = os.path.join(DATA_DIR, ".last_search.json")
ORDERS_CACHE_FILE = os.path.join(DATA_DIR, ".orders_cache.json")
PASSENGERS_FILE = os.path.join(DATA_DIR, ".passengers.json")

# ---------- 基础 API 工具 ----------

def _headers() -> dict:
    return {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {API_KEY}",
    }


def _extract_ota(body: dict) -> dict:
    """从网关响应 {status, message, result} 中提取 OTA 层 {code, msg, data}"""
    result = body.get("result")
    if result is None:
        return {"code": str(body.get("status", "-1")), "msg": body.get("message", "unknown")}
    return result


def _post(path: str, data: dict | None = None) -> dict:
    url = f"{TICKET_BASE}{path}"
    resp = requests.post(url, headers=_headers(), json=data or {}, timeout=60)
    return _extract_ota(resp.json())


def _get(path: str, params: dict | None = None) -> dict:
    url = f"{TICKET_BASE}{path}"
    resp = requests.get(url, headers=_headers(), params=params, timeout=60)
    return _extract_ota(resp.json())


def encrypt_fields(fields: dict[str, str]) -> dict[str, str]:
    """调用网关加密接口，将明文字段加密后返回。fields 如 {"credentialNo": "...", "mobile": "..."}"""
    url = f"{TICKET_BASE}/encrypt"
    resp = requests.post(url, headers=_headers(), json={"fields": fields}, timeout=30)
    body = resp.json()
    if body.get("status") != 0:
        raise RuntimeError(f"加密接口调用失败: {body.get('message', 'unknown')}")
    return body.get("result", {})


# ---------- 航班查询 ----------

# 常用城市名 -> 机场三字码（出发/到达城市解析用）
CITY_NAME_TO_CODE = {
    "杭州": "HGH", "北京": "BJS", "上海": "PVG", "广州": "CAN", "深圳": "SZX",
    "成都": "CTU", "西安": "XIY", "重庆": "CKG", "南京": "NKG", "武汉": "WUH",
    "青岛": "TAO", "厦门": "XMN", "昆明": "KMG", "哈尔滨": "HRB", "大连": "DLC",
    "沈阳": "SHE", "长沙": "CSX", "郑州": "CGO", "天津": "TSN", "海口": "HAK",
    "三亚": "SYX", "乌鲁木齐": "URC", "贵阳": "KWE", "济南": "TNA", "福州": "FOC",
    "南昌": "KHN", "合肥": "HFE", "石家庄": "SJW", "太原": "TYN", "长春": "CGQ",
    "兰州": "LHW", "南宁": "NNG", "呼和浩特": "HET", "拉萨": "LXA", "银川": "INC",
    "西宁": "XNN", "无锡": "WUX", "宁波": "NGB", "温州": "WNZ", "珠海": "ZUH",
    "首都": "PEK", "大兴": "PKX", "浦东": "PVG", "虹桥": "SHA", "萧山": "HGH",
}


def resolve_city(name_or_code: str) -> str:
    """将城市名或三字码转为 API 使用的三字码。若已是三字码则原样返回大写。"""
    if not name_or_code or not isinstance(name_or_code, str):
        return ""
    s = name_or_code.strip()
    if len(s) == 3 and s.isalpha():
        return s.upper()
    return CITY_NAME_TO_CODE.get(s) or ""


def flight_search(
    from_city: str,
    to_city: str,
    from_date: str,
    flight_no: str | None = None,
    all_data: bool | None = None,
) -> dict:
    search_data = {"fromCityCode": from_city, "toCityCode": to_city, "fromDate": from_date}
    if flight_no:
        search_data["flightNo"] = flight_no
    if all_data is not None:
        search_data["allData"] = all_data
    return _post("/flight/search", search_data)


def _baggage_desc(cabin: dict | None) -> str:
    """从舱位的 refundChange 中提取行李额描述，优先用 baggageKg/baggageNum，其次用 baggage 文本。"""
    if not cabin:
        return "-"
    rc = cabin.get("refundChange") or {}
    kg = rc.get("baggageKg")
    num = rc.get("baggageNum")
    if kg is not None and kg > 0 and num is not None and num > 0:
        return f"{kg}kg×{num}件"
    if kg is not None and kg > 0:
        return f"{kg}kg"
    if num is not None and num > 0:
        return f"{num}件"
    text = (rc.get("baggage") or "").strip()
    return text if text else "-"


def _extract_hhmm(time_str: str) -> str:
    """从 'YYYY-MM-DD HH:MM:SS' 或 'HH:MM' 等格式中提取 HH:MM。"""
    if not time_str:
        return ""
    s = time_str.strip()
    if " " in s:
        s = s.split(" ", 1)[1]
    return s[:5] if len(s) >= 5 else s


def list_flights_simple(result: dict, top_n: int | None = None) -> list[dict]:
    """提取航班列表，每航班一行含经济舱/公务舱最低价、机建燃油费，按经济舱价格排序。
    top_n=None 时返回全部航班（默认），不做条数截断。"""
    if result.get("code") != "0":
        return []
    data = result.get("data", {})
    flights = data.get("flightDetails", [])
    out = []
    for f in flights:
        carrier = f.get("carrier", {})
        from_airport = f.get("fromAirportName") or f.get("fromAirportCode", "")
        to_airport = f.get("toAirportName") or f.get("toAirportCode", "")
        from_terminal = f.get("fromTerminal") or ""
        to_terminal = f.get("toTerminal") or ""
        if from_terminal:
            from_airport = f"{from_airport}{from_terminal}"
        if to_terminal:
            to_airport = f"{to_airport}{to_terminal}"
        cabins = [c for c in f.get("cabins", []) if c.get("salePrice") is not None]
        economy = [c for c in cabins if c.get("grade") == "Y"]
        business = [c for c in cabins if c.get("grade") == "C"]
        cheapest_economy = min(economy, key=lambda c: c["salePrice"]) if economy else None
        cheapest_business = min(business, key=lambda c: c["salePrice"]) if business else None
        out.append({
            "flightNo": f.get("flightNo"),
            "carrierName": carrier.get("name"),
            "fromAirport": from_airport,
            "toAirport": to_airport,
            "fromTime": f.get("fromTime", ""),
            "toTime": f.get("toTime", ""),
            "economyPrice": cheapest_economy["salePrice"] if cheapest_economy else None,
            "economyCabin": (cheapest_economy.get("gradeDesc") or cheapest_economy.get("code")) if cheapest_economy else None,
            "businessPrice": cheapest_business["salePrice"] if cheapest_business else None,
            "businessCabin": (cheapest_business.get("gradeDesc") or cheapest_business.get("code")) if cheapest_business else None,
            "departureTax": f.get("departureTax", 0),
            "fuelTax": f.get("fuelTax", 0),
        })
    out.sort(key=lambda r: (r.get("economyPrice") is None, r.get("economyPrice") or 0))
    if top_n is None:
        return out
    return out[:top_n]


# ---------- 表格展示（与 flight_search.py 一致） ----------

def _display_width(s: str) -> int:
    """字符串在终端中的显示宽度：ASCII=1，中文等宽字符=2"""
    w = 0
    for c in s:
        w += 2 if "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or "\uff00" <= c <= "\uffef" else 1
    return w


def _pad_to_width(s: str, width: int, align: str = "left") -> str:
    """将字符串按显示宽度对齐到 width，不足用空格补齐"""
    s = s or ""
    current = _display_width(s)
    if current >= width:
        return s
    pad = " " * (width - current)
    return (s + pad) if align == "left" else (pad + s)


def _truncate_to_width(s: str, width: int) -> str:
    """按显示宽度截断字符串"""
    s = s or ""
    w = 0
    for i, c in enumerate(s):
        add = 2 if "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or "\uff00" <= c <= "\uffef" else 1
        if w + add > width:
            return s[:i]
        w += add
    return s


def _print_flights_table(rows: list[dict]) -> None:
    """打印航班表格：起飞/到达时间分两列，含机建费、燃油费"""
    W = [10, 22, 20, 20, 8, 8, 10, 10, 8, 8]
    sep = " "
    head = [
        _pad_to_width("航班号", W[0]),
        _pad_to_width("航空公司", W[1]),
        _pad_to_width("出发机场", W[2]),
        _pad_to_width("到达机场", W[3]),
        _pad_to_width("起飞", W[4]),
        _pad_to_width("到达", W[5]),
        _pad_to_width("经济舱", W[6], "right"),
        _pad_to_width("公务舱", W[7], "right"),
        _pad_to_width("机建", W[8], "right"),
        _pad_to_width("燃油", W[9], "right"),
    ]
    print(sep.join(head))
    print("-" * (sum(W) + len(sep) * (len(W) - 1)))
    for r in rows:
        from_time = _extract_hhmm(r.get("fromTime") or "")
        to_time = _extract_hhmm(r.get("toTime") or "")
        economy_str = f"¥{r['economyPrice']}" if r.get("economyPrice") is not None else "-"
        business_str = f"¥{r['businessPrice']}" if r.get("businessPrice") is not None else "-"
        dep_tax = r.get("departureTax", 0)
        fuel_tax = r.get("fuelTax", 0)
        dep_tax_str = f"¥{dep_tax}" if dep_tax else "-"
        fuel_tax_str = f"¥{fuel_tax}" if fuel_tax else "-"
        line = [
            _pad_to_width(_truncate_to_width(str(r.get("flightNo", "")), W[0]), W[0]),
            _pad_to_width(_truncate_to_width(r.get("carrierName") or "", W[1]), W[1]),
            _pad_to_width(_truncate_to_width(r.get("fromAirport") or "", W[2]), W[2]),
            _pad_to_width(_truncate_to_width(r.get("toAirport") or "", W[3]), W[3]),
            _pad_to_width(_truncate_to_width(from_time, W[4]), W[4]),
            _pad_to_width(_truncate_to_width(to_time, W[5]), W[5]),
            _pad_to_width(economy_str, W[6], "right"),
            _pad_to_width(business_str, W[7], "right"),
            _pad_to_width(dep_tax_str, W[8], "right"),
            _pad_to_width(fuel_tax_str, W[9], "right"),
        ]
        print(sep.join(line))


# ---------- 舱位报价查询（实时） ----------

def cabin_search(
    from_city: str,
    to_city: str,
    from_date: str,
    flight_no: str,
    air_range_type: str = "OW",
) -> dict:
    return _post("/flight/cabin-search", {
        "fromCityCode": from_city,
        "toCityCode": to_city,
        "fromDate": from_date,
        "flightNo": flight_no,
        "airRangeType": air_range_type,
    })


# ---------- 验舱验价 ----------

def check_price(flight_detail: dict, cabin: dict, from_city: str, to_city: str) -> dict:
    detail = {
        "cabinGrade": cabin.get("grade", "Y"),
        "cabinCode": cabin.get("code"),
        "flightNo": flight_detail.get("flightNo"),
        "fromAirportCode": flight_detail.get("fromAirportCode"),
        "toAirportCode": flight_detail.get("toAirportCode"),
    }
    if cabin.get("subCabinCode"):
        detail["subCabinCode"] = cabin["subCabinCode"]
    if flight_detail.get("fromTime"):
        detail["fromTime"] = flight_detail["fromTime"]
    if flight_detail.get("toTime"):
        detail["toTime"] = flight_detail["toTime"]
    if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
        detail["realFlightNo"] = flight_detail["realFlightNo"]
    return _post("/flight/check-price", {
        "airRangeType": flight_detail.get("airRangeType", "OW"),
        "departureTax": flight_detail.get("departureTax", 0),
        "fuelTax": flight_detail.get("fuelTax", 0),
        "facePrice": cabin.get("facePrice"),
        "salePrice": cabin.get("salePrice"),
        "originalPrice": cabin.get("originalPrice"),
        "productType": cabin.get("productType", "STANDARD_REFUND"),
        "fromCityCode": from_city,
        "toCityCode": to_city,
        "shoppingCode": cabin.get("shoppingCode", ""),
        "flightCheckPriceDetails": [detail],
    })


# ---------- 从查询结果中选取航班和舱位 ----------

def pick_flight_and_cabin(
    search_result: dict,
    flight_no: str,
    cabin_grade: str = "Y",
    cheapest: bool = True,
) -> tuple[dict, dict]:
    data = search_result.get("data", {})
    flights = data.get("flightDetails", [])
    if not flights:
        raise ValueError("查询结果中没有航班")
    target_flight = None
    for f in flights:
        if f.get("flightNo") == flight_no:
            target_flight = f
            break
    if not target_flight:
        raise ValueError(f"未找到航班 {flight_no}")
    cabins = target_flight.get("cabins", [])
    matched = [c for c in cabins if c.get("grade") == cabin_grade]
    if not matched:
        matched = cabins
    if not matched:
        raise ValueError(f"该航班无可用舱位（舱位等级 {cabin_grade}）")
    if cheapest:
        matched.sort(key=lambda c: c.get("salePrice", 999999))
    return target_flight, matched[0]


# ---------- 创建订单（常规） ----------

def create_order(
    flight_detail: dict,
    cabin: dict,
    from_city: str,
    to_city: str,
    from_date: str,
    shopping_code: str,
    price_info: str | None,
    passenger: dict,
    estimated_total: float,
) -> dict:
    to_encrypt: dict[str, str] = {}
    if passenger.get("credentialNo"):
        to_encrypt["credentialNo"] = passenger["credentialNo"]
    if passenger.get("mobile"):
        to_encrypt["mobile"] = passenger["mobile"]
    if passenger.get("email"):
        to_encrypt["email"] = passenger["email"]
    encrypted = encrypt_fields(to_encrypt) if to_encrypt else {}
    encrypted_passenger = dict(passenger)
    for k, v in encrypted.items():
        encrypted_passenger[k] = v
    order_data: dict = {
        "flightInfo": {
            "fromCityCode": from_city,
            "toCityCode": to_city,
            "fromDate": from_date,
            "flightNo": flight_detail.get("flightNo"),
            "cabinCode": cabin.get("code"),
        },
        "shoppingCode": shopping_code or "",
        "estimatedTotal": estimated_total,
        "travelBusiness": True,
        "passengerInfo": encrypted_passenger,
    }
    if price_info:
        order_data["priceInfo"] = price_info
    if cabin.get("subCabinCode"):
        order_data["flightInfo"]["subCabinCode"] = cabin["subCabinCode"]
    if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
        order_data["flightInfo"]["realFlightNo"] = flight_detail["realFlightNo"]
    return _post("/order/create", order_data)


# ---------- 支付前校验 ----------

def pay_validate(order_no: str) -> dict:
    return _post("/order/pay-validate", {"orderNo": order_no})


# ---------- 申请出票 ----------

def order_issue(
    order_no: str,
    payment_total: float = 0,
    payments: list[dict] | None = None,
) -> dict:
    issue_data: dict = {"orderNo": order_no}
    issue_data["paymentTotalAmount"] = payment_total
    if payments:
        issue_data["paymentInfos"] = payments
    return _post("/order/issue", issue_data)


# ---------- 订单详情 ----------

def order_detail(order_no: str) -> dict:
    return _get(f"/order/{order_no}")


def order_cancel(order_no: str) -> dict:
    return _post(f"/order/{order_no}/cancel")


def order_change(
    original_order_no: str,
    flight_detail: dict,
    cabin: dict,
    from_city: str,
    to_city: str,
    from_date: str,
    shopping_code: str,
    price_info: str | None,
    passenger: dict,
    estimated_total: float,
) -> dict:
    to_encrypt: dict[str, str] = {}
    if passenger.get("credentialNo"):
        to_encrypt["credentialNo"] = passenger["credentialNo"]
    if passenger.get("mobile"):
        to_encrypt["mobile"] = passenger["mobile"]
    if passenger.get("email"):
        to_encrypt["email"] = passenger["email"]
    encrypted = encrypt_fields(to_encrypt) if to_encrypt else {}
    encrypted_passenger = dict(passenger)
    for k, v in encrypted.items():
        encrypted_passenger[k] = v
    change_data: dict = {
        "orderNo": original_order_no,
        "changeType": 2,
        "passengerInfo": encrypted_passenger,
        "voluntary": True,
        "flightInfo": {
            "fromCityCode": from_city,
            "toCityCode": to_city,
            "fromDate": from_date,
            "flightNo": flight_detail.get("flightNo"),
            "cabinCode": cabin.get("code"),
        },
        "shoppingCode": shopping_code or "",
        "travelBusiness": True,
        "estimatedTotal": 1,
    }
    if price_info:
        change_data["priceInfo"] = price_info
    if cabin.get("subCabinCode"):
        change_data["flightInfo"]["subCabinCode"] = cabin["subCabinCode"]
    if flight_detail.get("isShareFlight") and flight_detail.get("realFlightNo"):
        change_data["flightInfo"]["realFlightNo"] = flight_detail["realFlightNo"]
    return _post("/order/change", change_data)


def order_refund(order_no: str, passengers: list[dict]) -> dict:
    refund_passengers = []
    for p in passengers:
        encrypted = encrypt_fields({"credentialNo": p["credentialNo"]})
        refund_passengers.append({
            "name": p["name"],
            "credentialNo": encrypted.get("credentialNo", p["credentialNo"]),
            "credentialType": p.get("credentialType", "IDENTITY"),
        })

    return _post(f"/order/{order_no}/refund", {
        "reason": "行程变更",
        "voluntary": True,
        "passengers": refund_passengers,
        "customerRefundOrderNo": f"REF{int(time.time() * 1000)}",
        "channelRefundOrderNo": f"REF{int(time.time() * 1000)}",
        "callbackUrl": "",
    })


# ---------- CLI ----------

def _birth_day_from_id_card(credential_no: str) -> str | None:
    """从 18 位身份证号中解析出生日期，返回 YYYY-MM-DD；非 18 位或非身份证则返回 None"""
    if not credential_no or len(credential_no) != 18:
        return None
    s = credential_no.strip()
    if not s[:17].isdigit():
        return None
    # 第 7–14 位为出生日期 YYYYMMDD
    ymd = s[6:14]
    if len(ymd) != 8 or not ymd.isdigit():
        return None
    return f"{ymd[:4]}-{ymd[4:6]}-{ymd[6:8]}"


# 订单状态中文展示
ORDER_SHOW_STATUS_ZH = {
    "WAIT_PAYMENT": "待支付",
    "WAIT_ISSUE": "待出票",
    "ISSUED": "已出票",
    "CANCELLED": "已取消",
    "REFUNDED": "已退票",
}
ORDER_STATUS_ZH = {"ORDERED": "已下单", "ISSUED": "已出票", "CANCELLED": "已取消", "REFUNDED": "已退票"}
PAYMENT_STATUS_ZH = {"WAIT_PAYMENT": "待支付", "PAID": "已支付", "REFUNDED": "已退款"}
PASSENGER_STATUS_ZH = {"NOT_ISSUE": "待出票", "ISSUED": "已出票", "REFUNDED": "已退票"}


def _print_order_summary_zh(data: dict) -> None:
    """打印订单关键信息的中文摘要"""
    if not data:
        return
    order_no = data.get("orderNo")
    total = data.get("totalAmount")
    show_status = data.get("orderShowStatus", "")
    order_status = data.get("orderStatus", "")
    pay_status = data.get("paymentStatus", "")
    cabin_zh = {"Y": "经济舱", "C": "公务舱"}
    print("---------- 订单关键信息 ----------")
    print(f"  订单号：{order_no}")
    print(f"  订单状态：{ORDER_SHOW_STATUS_ZH.get(show_status, show_status)}")
    print(f"  处理状态：{ORDER_STATUS_ZH.get(order_status, order_status)}")
    print(f"  支付状态：{PAYMENT_STATUS_ZH.get(pay_status, pay_status)}")
    if total is not None:
        print(f"  订单总额：¥{total}")
    flights = data.get("orderFlights") or []
    for i, f in enumerate(flights, 1):
        cabin = cabin_zh.get(f.get("cabinClass"), f.get("cabinClass") or "")
        from_t = f.get("fromTerminal") or ""
        to_t = f.get("toTerminal") or ""
        seg = f"{f.get('fromCityName','')}{f.get('fromAirportName','')}{from_t} {f.get('fromDate','')} {f.get('fromTime','')} → {f.get('toCityName','')}{f.get('toAirportName','')}{to_t} {f.get('toTime','')}"
        print(f"  航程{i}：{f.get('flightNo','')} {seg} {cabin}")
        rcd = f.get("refundChangeDetail") or {}
        baggage = (rcd.get("baggage") or "").strip()
        if baggage:
            print(f"    行李额：{baggage}")
        refund_headers = rcd.get("refundHeaders") or []
        refund_amounts = rcd.get("refundAmountList") or []
        if refund_headers and refund_amounts:
            pairs = [f"{h} ¥{a}" for h, a in zip(refund_headers, refund_amounts)]
            print(f"    退票手续费：{' / '.join(pairs)}")
        change_headers = rcd.get("changeHeaders") or []
        change_amounts = rcd.get("changeAmountList") or []
        if change_headers and change_amounts:
            pairs = [f"{h} ¥{a}" for h, a in zip(change_headers, change_amounts)]
            print(f"    改签手续费：{' / '.join(pairs)}")
    passengers = data.get("passengers") or []
    for p in passengers:
        st = p.get("status", "")
        ticket = p.get("ticketNo") or "—"
        print(f"  乘客：{p.get('name','')}  状态：{PASSENGER_STATUS_ZH.get(st, st)}  票号：{ticket}")
    print("----------------------------------")


def _format_refund_change_rules(order_data: dict) -> dict:
    """从订单详情中提取退改签规则，返回结构化 dict 供 Agent 展示给用户。"""
    flights = order_data.get("orderFlights") or []
    rules = []
    for f in flights:
        rcd = f.get("refundChangeDetail") or {}
        segment = (
            f"{f.get('fromCityName', '')} → {f.get('toCityName', '')} "
            f"{f.get('flightNo', '')} {f.get('fromDate', '')} {f.get('fromTime', '')}"
        )
        refund_table = list(zip(
            rcd.get("refundHeaders", []),
            rcd.get("refundAmountList", []),
        ))
        change_table = list(zip(
            rcd.get("changeHeaders", []),
            rcd.get("changeAmountList", []),
        ))
        rules.append({
            "segment": segment,
            "facePrice": f.get("facePrice"),
            "refund_rules": [{"period": h, "fee": a} for h, a in refund_table],
            "change_rules": [{"period": h, "fee": a} for h, a in change_table],
            "baggage": rcd.get("baggage", ""),
            "endorseRule": rcd.get("endorseRule", ""),
            "remark": rcd.get("remark", ""),
        })
    return {"refundChangeRules": rules}


# ---------- 订单缓存 ----------

def _load_orders_cache() -> dict:
    """读取本地订单缓存文件，返回 {orderNo: summary} 字典。"""
    if os.path.isfile(ORDERS_CACHE_FILE):
        try:
            with open(ORDERS_CACHE_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
        except (OSError, json.JSONDecodeError):
            pass
    return {}


def _save_order_to_cache(order_no: str, order_data: dict) -> None:
    """将订单详情的关键摘要追加/更新到缓存文件。"""
    cache = _load_orders_cache()
    cache[str(order_no)] = {
        "orderNo": str(order_no),
        "orderShowStatus": order_data.get("orderShowStatus"),
        "orderStatus": order_data.get("orderStatus"),
        "paymentStatus": order_data.get("paymentStatus"),
        "totalAmount": order_data.get("totalAmount"),
        "flights": [
            {
                "flightNo": fl.get("flightNo"),
                "fromCityName": fl.get("fromCityName"),
                "toCityName": fl.get("toCityName"),
                "fromDate": fl.get("fromDate"),
                "fromTime": fl.get("fromTime"),
            }
            for fl in (order_data.get("orderFlights") or [])
        ],
        "passengers": [
            {
                "name": p.get("name"),
                "status": p.get("status"),
                "ticketNo": p.get("ticketNo"),
            }
            for p in (order_data.get("passengers") or [])
        ],
        "refundOrders": [
            {
                "refundOrderNo": str(r.get("refundOrderNo", "")),
                "refundOrderShowStatus": r.get("refundOrderShowStatus"),
                "refundableTotalAmount": r.get("refundableTotalAmount"),
                "refundTotalAmount": r.get("refundTotalAmount"),
            }
            for r in (order_data.get("refundOrders") or [])
        ],
        "changeOrders": [
            {
                "changeOrderNo": str(c.get("orderNo", "")),
                "changeOrderShowStatus": c.get("orderShowStatus"),
            }
            for c in (order_data.get("changeOrders") or [])
        ],
        "updatedAt": time.strftime("%Y-%m-%d %H:%M:%S"),
    }
    try:
        with open(ORDERS_CACHE_FILE, "w", encoding="utf-8") as f:
            json.dump(cache, f, ensure_ascii=False, indent=2)
    except OSError:
        pass


# ---------- 乘客身份管理 ----------

def _load_passengers() -> list[dict]:
    """读取已保存的乘客身份列表。"""
    if os.path.isfile(PASSENGERS_FILE):
        try:
            with open(PASSENGERS_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
        except (OSError, json.JSONDecodeError):
            pass
    return []


def _save_passenger(name: str, mobile: str, credential_no: str, gender: str) -> dict:
    """保存乘客身份到文件，以证件号去重。返回保存后的乘客记录。"""
    passengers = _load_passengers()
    record = {
        "name": name,
        "mobile": mobile,
        "credentialNo": credential_no,
        "gender": gender,
        "savedAt": time.strftime("%Y-%m-%d %H:%M:%S"),
    }
    for i, p in enumerate(passengers):
        if p.get("credentialNo") == credential_no:
            passengers[i] = record
            break
    else:
        passengers.append(record)
    try:
        with open(PASSENGERS_FILE, "w", encoding="utf-8") as f:
            json.dump(passengers, f, ensure_ascii=False, indent=2)
    except OSError:
        pass
    return record


def cmd_search(args: argparse.Namespace) -> int:
    from_code = resolve_city(args.from_city) or args.from_city.strip()
    to_code = resolve_city(args.to_city) or args.to_city.strip()
    if not from_code:
        print(json.dumps({"ok": False, "error": f"无法识别出发城市: {args.from_city}，请填写三字码或支持的城市名"}, ensure_ascii=False))
        return 1
    if not to_code:
        print(json.dumps({"ok": False, "error": f"无法识别到达城市: {args.to_city}，请填写三字码或支持的城市名"}, ensure_ascii=False))
        return 1
    res = flight_search(from_code, to_code, args.from_date, flight_no=getattr(args, "flight_no", None))
    if res.get("code") != "0":
        print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
        return 1
    # 保存完整查询结果，供 create-order 仅传航班号时使用
    state = {
        "from_city": from_code,
        "to_city": to_code,
        "from_date": args.from_date,
        "result": res,
    }
    try:
        with open(LAST_SEARCH_FILE, "w", encoding="utf-8") as f:
            json.dump(state, f, ensure_ascii=False, indent=2)
    except OSError:
        pass  # 忽略写入失败，仅影响后续 create-order 需传全参
    rows = list_flights_simple(res, top_n=None)
    _print_flights_table(rows)
    print()
    print(f"共 {len(rows)} 条航班（已按经济舱价格升序展示全部，未截断）。")
    print("已保存最近一次查询结果。下单请使用: create-order --flight-no <航班号> [--cabin-grade Y|C]")
    return 0


def cmd_create_order(args: argparse.Namespace) -> int:
    passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
    passenger_mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
    passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
    passenger_gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()

    missing = []
    if not passenger_name:
        missing.append(ENV_PASSENGER_NAME)
    if not passenger_mobile:
        missing.append(ENV_PASSENGER_MOBILE)
    if not passenger_credential_no:
        missing.append(ENV_PASSENGER_CREDENTIAL_NO)
    if passenger_gender not in ("M", "F"):
        missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
    if missing:
        print(json.dumps({
            "ok": False,
            "error": "创建订单前请设置以下环境变量",
            "missing": missing,
        }, ensure_ascii=False))
        return 1

    passenger = {
        "name": passenger_name,
        "gender": passenger_gender,
        "credentialNo": passenger_credential_no,
        "credentialType": getattr(args, "credential_type", "IDENTITY"),
        "mobile": passenger_mobile,
        "passengerType": getattr(args, "passenger_type", "ADU"),
    }
    birth_day = getattr(args, "passenger_birth_day", None)
    if not birth_day and passenger_credential_no:
        birth_day = _birth_day_from_id_card(passenger_credential_no)
    if birth_day:
        passenger["birthDay"] = birth_day

    # 从最近一次查询结果读取行程与航班详情
    from_city = to_city = from_date = None
    search_res = None
    if os.path.isfile(LAST_SEARCH_FILE):
        try:
            with open(LAST_SEARCH_FILE, "r", encoding="utf-8") as f:
                state = json.load(f)
            from_city = state.get("from_city")
            to_city = state.get("to_city")
            from_date = state.get("from_date")
            search_res = state.get("result")
        except (OSError, json.JSONDecodeError):
            pass
    if not search_res or not from_city or not to_city or not from_date:
        print(json.dumps({
            "ok": False,
            "error": "未找到最近一次航班查询结果，请先执行查询航班后再下单",
        }, ensure_ascii=False))
        return 1
    try:
        flight, cabin = pick_flight_and_cabin(search_res, args.flight_no, args.cabin_grade)
    except ValueError as e:
        print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
        return 1

    verify_res = check_price(flight, cabin, from_city, to_city)
    shopping_code = ""
    price_info = None
    if verify_res.get("code") == "0":
        vdata = verify_res.get("data", {})
        shopping_code = vdata.get("shoppingCode", cabin.get("shoppingCode", ""))
        price_info = vdata.get("priceInfo")
    else:
        shopping_code = cabin.get("shoppingCode", "")

    estimated_total = float(cabin.get("salePrice", 0)) + float(flight.get("departureTax", 0)) + float(flight.get("fuelTax", 0))
    order_res = create_order(
        flight_detail=flight,
        cabin=cabin,
        from_city=from_city,
        to_city=to_city,
        from_date=from_date,
        shopping_code=shopping_code,
        price_info=price_info,
        passenger=passenger,
        estimated_total=estimated_total,
    )
    if order_res.get("code") != "0":
        print(json.dumps({
            "ok": False,
            "code": order_res.get("code"),
            "msg": order_res.get("msg"),
        }, ensure_ascii=False))
        return 1
    order_data = order_res.get("data", {})
    print(json.dumps({
        "ok": True,
        "orderNo": str(order_data.get("orderNo", "")),
        "pnrCode": order_data.get("pnrCode"),
        "expireTime": order_data.get("expireTime"),
        "paymentTotalAmount": estimated_total,
    }, ensure_ascii=False, indent=2))
    return 0


def cmd_pay_issue(args: argparse.Namespace) -> int:
    order_no = args.order_no
    # 从订单详情获取应付金额，无需用户传入
    detail_res = order_detail(order_no=order_no)
    if detail_res.get("code") != "0":
        print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
        return 1
    data = detail_res.get("data", {})
    _save_order_to_cache(order_no, data)
    order_status = data.get("orderStatus", "")
    if order_status == "WAIT_CHECK":
        print("---------- 订单状态异常 ----------")
        print(f"  订单号：{order_no}")
        print(f"  当前状态：{order_status}（等待审核）")
        print("  订单审核还未通过，暂时无法出票，请等待审核完成后再操作。")
        print("----------------------------------")
        print(json.dumps({"ok": False, "error": "订单审核未通过（WAIT_CHECK），无法出票", "orderStatus": order_status}, ensure_ascii=False))
        return 1
    total = data.get("totalAmount")
    if total is None:
        print(json.dumps({"ok": False, "error": "订单详情中无 totalAmount，无法申请出票"}, ensure_ascii=False))
        return 1
    total = float(total)
    pay_res = pay_validate(order_no)
    if pay_res.get("code") != "0":
        print(json.dumps({"ok": False, "step": "pay_validate", "code": pay_res.get("code"), "msg": pay_res.get("msg")}, ensure_ascii=False))
        return 1
    issue_res = order_issue(
        order_no=order_no,
        payment_total=total,
        payments=[{
            "paymentMethod": "BP_ACCOUNT",
            "payAmount": total,
            "paymentTradeId": f"PAY{int(time.time() * 1000)}",
        }],
    )
    if issue_res.get("code") != "0":
        print(json.dumps({
            "ok": False,
            "step": "order_issue",
            "code": issue_res.get("code"),
            "msg": issue_res.get("msg"),
        }, ensure_ascii=False))
        return 1
    print("---------- 出票申请 ----------")
    print(f"  订单号：{order_no}")
    print(f"  支付金额：¥{total}")
    print("  出票申请已提交（异步处理），请等待结果通知。")
    print("------------------------------")
    print(json.dumps({"ok": True, "message": "出票申请已提交（异步处理）"}, ensure_ascii=False, indent=2))
    return 0


def cmd_order_status(args: argparse.Namespace) -> int:
    order_no = getattr(args, "order_no", None) or ""
    if not order_no:
        print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
        return 1
    res = order_detail(order_no=order_no)
    if res.get("code") != "0":
        print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
        return 1
    data = res.get("data", {})
    _save_order_to_cache(order_no, data)
    _print_order_summary_zh(data)
    print(json.dumps({"ok": True, "data": data}, ensure_ascii=False, indent=2))
    return 0


def cmd_cancel_order(args: argparse.Namespace) -> int:
    order_no = (getattr(args, "order_no", None) or "").strip()
    if not order_no:
        print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
        return 1

    
    res = order_cancel(order_no)
    if res.get("code") != "0":
        print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
        return 1
    print(json.dumps({"ok": True, "message": f"订单 {order_no} 已取消"}, ensure_ascii=False, indent=2))
    return 0


def cmd_refund_order(args: argparse.Namespace) -> int:
    order_no = (getattr(args, "order_no", None) or "").strip()
    if not order_no:
        print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
        return 1

    dry_run = getattr(args, "dry_run", False)

    detail_res = order_detail(order_no=order_no)
    if detail_res.get("code") != "0":
        print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
        return 1
    data = detail_res.get("data", {})
    _save_order_to_cache(order_no, data)

    show_status = data.get("orderShowStatus", "")
    if show_status != "ISSUED":
        print(json.dumps({
            "ok": False,
            "error": f"订单当前状态为 {ORDER_SHOW_STATUS_ZH.get(show_status, show_status)}，仅已出票(ISSUED)订单可退票",
            "orderShowStatus": show_status,
        }, ensure_ascii=False))
        return 1

    rules = _format_refund_change_rules(data)

    if dry_run:
        _print_order_summary_zh(data)
        print(json.dumps({"ok": True, "dryRun": True, **rules}, ensure_ascii=False, indent=2))
        return 0

    passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
    passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
    if not passenger_name or not passenger_credential_no:
        print(json.dumps({
            "ok": False,
            "error": "退票需要乘客信息，请设置环境变量",
            "missing": [k for k, v in [
                (ENV_PASSENGER_NAME, passenger_name),
                (ENV_PASSENGER_CREDENTIAL_NO, passenger_credential_no),
            ] if not v],
        }, ensure_ascii=False))
        return 1

    passengers = [{
        "name": passenger_name,
        "credentialNo": passenger_credential_no,
        "credentialType": "IDENTITY",
    }]

    res = order_refund(order_no, passengers)
    if res.get("code") != "0":
        print(json.dumps({"ok": False, "code": res.get("code"), "msg": res.get("msg")}, ensure_ascii=False))
        return 1
    print(json.dumps({"ok": True, "message": f"订单 {order_no} 退票申请已提交", **rules}, ensure_ascii=False, indent=2))
    return 0


def cmd_change_order(args: argparse.Namespace) -> int:
    original_order_no = (getattr(args, "order_no", None) or "").strip()
    if not original_order_no:
        print(json.dumps({"ok": False, "error": "请提供 --order-no"}, ensure_ascii=False))
        return 1

    detail_res = order_detail(order_no=original_order_no)
    if detail_res.get("code") != "0":
        print(json.dumps({"ok": False, "step": "order_detail", "code": detail_res.get("code"), "msg": detail_res.get("msg")}, ensure_ascii=False))
        return 1
    detail_data = detail_res.get("data", {})
    _save_order_to_cache(original_order_no, detail_data)

    show_status = detail_data.get("orderShowStatus", "")
    if show_status != "ISSUED":
        print(json.dumps({
            "ok": False,
            "error": f"订单当前状态为 {ORDER_SHOW_STATUS_ZH.get(show_status, show_status)}，仅已出票(ISSUED)订单可改签",
            "orderShowStatus": show_status,
        }, ensure_ascii=False))
        return 1

    rules = _format_refund_change_rules(detail_data)

    passenger_name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
    passenger_mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
    passenger_credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
    passenger_gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()

    missing = []
    if not passenger_name:
        missing.append(ENV_PASSENGER_NAME)
    if not passenger_mobile:
        missing.append(ENV_PASSENGER_MOBILE)
    if not passenger_credential_no:
        missing.append(ENV_PASSENGER_CREDENTIAL_NO)
    if passenger_gender not in ("M", "F"):
        missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
    if missing:
        print(json.dumps({
            "ok": False,
            "error": "改签前请设置以下环境变量",
            "missing": missing,
        }, ensure_ascii=False))
        return 1

    passenger = {
        "name": passenger_name,
        "gender": passenger_gender,
        "credentialNo": passenger_credential_no,
        "credentialType": getattr(args, "credential_type", "IDENTITY"),
        "mobile": passenger_mobile,
        "passengerType": getattr(args, "passenger_type", "ADU"),
    }
    birth_day = getattr(args, "passenger_birth_day", None)
    if not birth_day and passenger_credential_no:
        birth_day = _birth_day_from_id_card(passenger_credential_no)
    if birth_day:
        passenger["birthDay"] = birth_day

    from_city = to_city = from_date = None
    search_res = None
    if os.path.isfile(LAST_SEARCH_FILE):
        try:
            with open(LAST_SEARCH_FILE, "r", encoding="utf-8") as f:
                state = json.load(f)
            from_city = state.get("from_city")
            to_city = state.get("to_city")
            from_date = state.get("from_date")
            search_res = state.get("result")
        except (OSError, json.JSONDecodeError):
            pass
    if not search_res or not from_city or not to_city or not from_date:
        print(json.dumps({
            "ok": False,
            "error": "未找到最近一次航班查询结果，请先执行查询航班后再改签",
        }, ensure_ascii=False))
        return 1

    try:
        flight, cabin = pick_flight_and_cabin(search_res, args.flight_no, args.cabin_grade)
    except ValueError as e:
        print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
        return 1

    verify_res = check_price(flight, cabin, from_city, to_city)
    shopping_code = ""
    price_info = None
    if verify_res.get("code") == "0":
        vdata = verify_res.get("data", {})
        shopping_code = vdata.get("shoppingCode", cabin.get("shoppingCode", ""))
        price_info = vdata.get("priceInfo")
    else:
        shopping_code = cabin.get("shoppingCode", "")

    estimated_total = float(cabin.get("salePrice", 0)) + float(flight.get("departureTax", 0)) + float(flight.get("fuelTax", 0))
    change_res = order_change(
        original_order_no=original_order_no,
        flight_detail=flight,
        cabin=cabin,
        from_city=from_city,
        to_city=to_city,
        from_date=from_date,
        shopping_code=shopping_code,
        price_info=price_info,
        passenger=passenger,
        estimated_total=estimated_total,
    )
    if change_res.get("code") != "0":
        print(json.dumps({
            "ok": False,
            "code": change_res.get("code"),
            "msg": change_res.get("msg"),
        }, ensure_ascii=False))
        return 1
    change_data = change_res.get("data", {})
    print(json.dumps({
        "ok": True,
        "changeOrderNo": str(change_data.get("orderNo", "")),
        "pnrCode": change_data.get("pnrCode"),
        "expireTime": change_data.get("expireTime"),
        "paymentTotalAmount": estimated_total,
        **rules,
    }, ensure_ascii=False, indent=2))
    return 0


def cmd_list_orders(args: argparse.Namespace) -> int:
    cache = _load_orders_cache()
    if not cache:
        print(json.dumps({"ok": True, "orders": [], "message": "暂无缓存的订单记录"}, ensure_ascii=False, indent=2))
        return 0
    orders = sorted(cache.values(), key=lambda o: o.get("updatedAt", ""), reverse=True)
    print(json.dumps({"ok": True, "orders": orders}, ensure_ascii=False, indent=2))
    return 0


def cmd_save_passenger(args: argparse.Namespace) -> int:
    name = os.environ.get(ENV_PASSENGER_NAME, "").strip()
    mobile = os.environ.get(ENV_PASSENGER_MOBILE, "").strip()
    credential_no = os.environ.get(ENV_PASSENGER_CREDENTIAL_NO, "").strip()
    gender = os.environ.get(ENV_PASSENGER_GENDER, "").strip().upper()
    missing = []
    if not name:
        missing.append(ENV_PASSENGER_NAME)
    if not mobile:
        missing.append(ENV_PASSENGER_MOBILE)
    if not credential_no:
        missing.append(ENV_PASSENGER_CREDENTIAL_NO)
    if gender not in ("M", "F"):
        missing.append(f"{ENV_PASSENGER_GENDER} (需为 M 或 F)")
    if missing:
        print(json.dumps({
            "ok": False,
            "error": "保存乘客身份前请设置以下环境变量",
            "missing": missing,
        }, ensure_ascii=False))
        return 1
    record = _save_passenger(name, mobile, credential_no, gender)
    print(json.dumps({"ok": True, "message": f"乘客 {name} 的身份信息已保存", "passenger": record}, ensure_ascii=False, indent=2))
    return 0


def cmd_list_passengers(args: argparse.Namespace) -> int:
    passengers = _load_passengers()
    if not passengers:
        print(json.dumps({"ok": True, "passengers": [], "message": "暂无已保存的乘客身份"}, ensure_ascii=False, indent=2))
        return 0
    display = []
    for i, p in enumerate(passengers):
        cred = p.get("credentialNo", "")
        masked = cred[:6] + "****" + cred[-4:] if len(cred) >= 10 else cred
        display.append({
            "index": i + 1,
            "name": p.get("name"),
            "gender": p.get("gender"),
            "credentialNo_masked": masked,
            "mobile": p.get("mobile", "")[:3] + "****" + p.get("mobile", "")[-4:] if len(p.get("mobile", "")) >= 7 else p.get("mobile", ""),
            "savedAt": p.get("savedAt"),
        })
    print(json.dumps({"ok": True, "passengers": display, "_raw": passengers}, ensure_ascii=False, indent=2))
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="机票查询与下单")
    sub = parser.add_subparsers(dest="command", required=True)

    # search：支持三字码或城市名（如 杭州、北京）
    p_search = sub.add_parser("search", help="航班查询，返回当日全部航班（按经济舱价格排序，不截断）")
    p_search.add_argument("--from-city", required=True, help="出发城市：三字码（如 HGH）或城市名（如 杭州）")
    p_search.add_argument("--to-city", required=True, help="到达城市：三字码（如 BJS）或城市名（如 北京）")
    p_search.add_argument("--from-date", required=True, help="出发日期 YYYY-MM-DD")
    p_search.add_argument("--flight-no", default=None, help="可选，指定航班号只查该航班（如 CA1723）")
    p_search.set_defaults(func=cmd_search)

    # create-order：仅需航班号，行程与舱位从最近一次 search 结果读取
    p_order = sub.add_parser("create-order", help="创建订单（需先执行 search；行程与舱位从上次查询结果读取）")
    p_order.add_argument("--flight-no", required=True, help="航班号（从 search 结果中选择）")
    p_order.add_argument("--cabin-grade", default="Y", choices=["Y", "C"], help="Y=经济舱 C=公务舱，默认 Y")
    p_order.add_argument("--passenger-birth-day", default=None, help="乘机人生日 YYYY-MM-DD（可选，默认从身份证号解析）")
    p_order.add_argument("--passenger-type", default="ADU", help="ADU/CHD/INF")
    p_order.add_argument("--credential-type", default="IDENTITY")
    p_order.set_defaults(func=cmd_create_order)

    # pay-issue：仅需订单号，支付金额从订单详情接口自动获取
    p_issue = sub.add_parser("pay-issue", help="支付前校验并申请出票（金额从订单详情自动获取）")
    p_issue.add_argument("--order-no", required=True, help="订单号")
    p_issue.set_defaults(func=cmd_pay_issue)

    # order-status：仅需订单号
    p_status = sub.add_parser("order-status", help="查询订单状态")
    p_status.add_argument("--order-no", required=True, help="订单号")
    p_status.set_defaults(func=cmd_order_status)

    # cancel-order：取消订单
    p_cancel = sub.add_parser("cancel-order", help="取消订单（待支付等可取消状态）")
    p_cancel.add_argument("--order-no", required=True, help="订单号")
    p_cancel.set_defaults(func=cmd_cancel_order)

    # refund-order：退票
    p_refund = sub.add_parser("refund-order", help="申请退票（已出票订单）")
    p_refund.add_argument("--order-no", required=True, help="订单号")
    p_refund.add_argument("--dry-run", action="store_true", default=False, help="仅查询退改签规则，不实际执行退票")
    p_refund.set_defaults(func=cmd_refund_order)

    # change-order：改签
    p_change = sub.add_parser("change-order", help="改签（需先执行 search；从上次查询结果中选取改签航班）")
    p_change.add_argument("--order-no", required=True, help="原订单号")
    p_change.add_argument("--flight-no", required=True, help="改签目标航班号（从 search 结果中选择）")
    p_change.add_argument("--cabin-grade", default="Y", choices=["Y", "C"], help="Y=经济舱 C=公务舱，默认 Y")
    p_change.add_argument("--passenger-birth-day", default=None, help="乘机人生日 YYYY-MM-DD（可选，默认从身份证号解析）")
    p_change.add_argument("--passenger-type", default="ADU", help="ADU/CHD/INF")
    p_change.add_argument("--credential-type", default="IDENTITY")
    p_change.set_defaults(func=cmd_change_order)

    # list-orders：列出缓存的订单
    p_list = sub.add_parser("list-orders", help="列出本地缓存的历史订单摘要")
    p_list.set_defaults(func=cmd_list_orders)

    # save-passenger：保存当前乘客身份到文件
    p_save_pax = sub.add_parser("save-passenger", help="将当前环境变量中的乘客身份保存到本地文件")
    p_save_pax.set_defaults(func=cmd_save_passenger)

    # list-passengers：列出已保存的乘客身份
    p_list_pax = sub.add_parser("list-passengers", help="列出已保存的所有乘客身份")
    p_list_pax.set_defaults(func=cmd_list_passengers)

    args = parser.parse_args()
    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())
