#!/usr/bin/env python3
"""Idempotently create and update the Binzhou Yunxiao child tasks."""

import datetime as dt
import json
import os
import pathlib
import urllib.error
import urllib.request


BASE = "https://openapi-rdc.aliyuncs.com"
ORG = os.environ["YUNXIAO_ORGANIZATION_ID"]
SPACE = os.environ["YUNXIAO_PROJECT_ID"]
TOKEN = os.environ["YUNXIAO_TOKEN"]
ASSIGNEE = "635755195dc8531eeb2e33d9"
TASK_TYPE = "ba102e46bc6a8483d9b7f25c"
PRIORITY_MEDIUM = "04f8224daf4e1e1050bca2cd4c"

PARENTS = {
    "VWCG-1065": "d018b6c204ed4ae9d35b238654",
    "VWCG-1070": "665789ae4cbb119f5c00ae633f",
    "VWCG-1071": "e4d0ac0f1223cb0b78d6381ac6",
    "VWCG-1072": "7bf53ba9a08e47f7d709f7c79c",
}

REPOS = {
    "store": ("4913137", "https://codeup.aliyun.com/60069db88deaa14d9e02b875/zhct/store"),
    "ai_api": ("5073360", "https://codeup.aliyun.com/60069db88deaa14d9e02b875/zhct/ai_api"),
    "ai_app": ("5055213", "https://codeup.aliyun.com/60069db88deaa14d9e02b875/zhct/ai_app"),
    "tray": ("4825265", "https://codeup.aliyun.com/60069db88deaa14d9e02b875/zhct/device/restaurant/billedBygram/ZhctTrayBindingMachine"),
    "consume": ("4937549", "https://codeup.aliyun.com/60069db88deaa14d9e02b875/zhct/device/restaurant/stallCheckout/CusumptionMachine"),
}


def commit(repo, sha, title):
    project_id, web_url = REPOS[repo]
    return {
        "repo": repo,
        "projectId": project_id,
        "sha": sha,
        "title": title,
        "url": f"{web_url}/commit/{sha}",
    }


TASKS = [
    {
        "parent": "VWCG-1065",
        "subject": "开发：普通二维码唤起小程序并绑定当前用户",
        "start": "2026-07-02", "end": "2026-07-17", "estimate": 28, "actual": 8,
        "status": "100011",
        "scope": "完成普通 URL 二维码 q 参数解析、登录后续绑、当前用户身份解析，并复用 store 绑盘与余额校验链路。该阶段不代表 scene/token 正式 V2 已完成。",
        "validation": "已完成代码回溯、PHP 7.3 语法检查及既有滨州绑盘全流程证据核对。",
        "commits": [
            commit("ai_app", "de5f08f96019444dd57fa5825cca73bf01694ec5", "滨州获取二维码参数"),
            commit("ai_app", "6b9fbbd8529118f1d6ecf355b4dc17aa509d1614", "滨州小程序更改"),
            commit("ai_api", "4851206bfb82e59119dca4d36117671b2734089a", "微信扫一扫扫码绑盘"),
            commit("ai_api", "ba452b8723356fb65a90daac863980cd68a4ba7c", "绑盘"),
            commit("ai_api", "997ee05ba0a834c41e31ff55b28f50e8754423fc", "绑盘接口"),
            commit("ai_api", "dc4ee7231bedf185ff49b194c73cde9a6456738b", "微信扫一扫绑盘"),
            commit("store", "17133ad5c9914a91e190ddbb8d9d8e71bb78c9ba", "滨州微信普通二维码域名校验文件"),
            commit("store", "4f875af5fb88ada668f2aa2ad2baf55be378eaf7", "绑盘接口查询余额"),
        ],
    },
    {
        "parent": "VWCG-1065",
        "subject": "开发：餐盘二维码令牌、解析接口与后台管理",
        "start": "2026-08-05", "end": "2026-08-07", "estimate": 32, "actual": None,
        "status": "100005",
        "scope": "新增餐盘二维码 token 映射、resolveQr/mpBind/bindDetail、企业与餐厅归属校验，以及二维码生成、下载、停用和重新生成后台能力。",
        "validation": "待开发。",
        "commits": [],
    },
    {
        "parent": "VWCG-1065",
        "subject": "开发：小程序专用绑盘页、确认交互与真机回归",
        "start": "2026-08-10", "end": "2026-08-12", "estimate": 26, "actual": None,
        "status": "100005",
        "scope": "实现 scene=t:plate,k:{token},v:1 解析、专用绑盘确认页、登录回跳持久化、本人重复扫码幂等，并完成微信真机及原绑盘回归。",
        "validation": "待开发与真机验收。",
        "commits": [],
    },
    {
        "parent": "VWCG-1070",
        "subject": "开发：绑盘机扫描 C 前缀支付码绑定餐盘",
        "start": "2026-07-02", "end": "2026-07-15", "estimate": 8, "actual": 2,
        "status": "100011",
        "scope": "绑盘机识别小程序支付码 C 前缀，剥离前缀后以 qr_code 调用 /p/api/bindPlate，复用支付码有效期、人员、余额和餐盘占用校验。该阶段不代表后端已兼容两种输入。",
        "validation": "已核对设备扫码分流、请求字段和 store 绑盘实现；裸码兼容与并发幂等另行处理。",
        "commits": [
            commit("store", "bf799b3f3e7f152500c194e5f2d611e68be3ba17", "生成定时二维码用于绑盘及验证"),
            commit("store", "6b14cae77860ac80f2d4df2f3c0f4718b9984944", "二维码绑定真实支付码用户"),
            commit("store", "244ab363b99fd9455b08c5407d051b32bc118a07", "二维码绑盘空人员兼容"),
            commit("ai_api", "c7d3806f5dd1959d0b69039adc4b6d7e6feefef6", "支付二维码接口"),
            commit("ai_app", "beb9771be676e4ce25458003d1c909f8ff8eba2d", "我的页面支付二维码弹窗"),
            commit("ai_api", "06fa0885056db83f47dedd861e8203f8ed061f0c", "限时支付码支持绑盘与消费"),
            commit("ai_app", "02733463f19021fc1116e1d1bcbbc629c4d44e36", "接入支付二维码接口"),
            commit("store", "8723d11141a68cdaeabf9f0199e006d95fcc7303", "二维码绑盘"),
            commit("ai_api", "a2f76e82649a7113da6da8e858d251f00efef138", "个人支付二维码"),
            commit("ai_app", "1b2067b2354a573fbe033b2d2e063ab7af305053", "小程序 Canvas 生成支付二维码"),
            commit("ai_api", "ed2ca8fd13327df0803aa2de686e1b03d0ed873d", "支付二维码有效期"),
            commit("ai_app", "82fe04a65f0c95c18490fdacbc02510acb86cae4", "支付二维码失效与刷新修复"),
            commit("tray", "2d98878a81a4f0648fb9d00ca8f2829f9b7bc23e", "绑盘机改版：支持卡码脸的绑定"),
            commit("tray", "22c7d580c4111c6353b47a8aa7ec0e57702eff16", "优卡特绑盘机二维码设备适配"),
            commit("store", "4f875af5fb88ada668f2aa2ad2baf55be378eaf7", "绑盘接口查询余额"),
        ],
    },
    {
        "parent": "VWCG-1070",
        "subject": "开发：支付码双格式兼容、绑盘幂等与专项回归",
        "start": "2026-08-05", "end": "2026-08-06", "estimate": 12, "actual": None,
        "status": "100005",
        "scope": "由 store 防御性统一带 C/不带 C 支付码，补齐支付码类型限制、异常提示顺序和重复绑盘并发幂等，并完成过期、无效、重复、余额及支付回归。",
        "validation": "待开发与绑盘机真机验收。",
        "commits": [],
    },
    {
        "parent": "VWCG-1071",
        "subject": "开发：小程序扫一扫餐盘码绑定当前用户",
        "start": "2026-07-15", "end": "2026-07-17", "estimate": 10, "actual": 3,
        "status": "100011",
        "scope": "在小程序我的页面识别 type=100 与 equipment_code，按当前登录用户解析 staff_uuid，将餐盘码映射为 plate_code，并保留非绑盘二维码原扫码支付流程。",
        "validation": "关键 PHP 文件通过 PHP 7.3 语法检查，代码链路和错误返回已核对；保留小程序真机验收。",
        "commits": [
            commit("ai_app", "6b9fbbd8529118f1d6ecf355b4dc17aa509d1614", "滨州小程序更改"),
            commit("ai_api", "4851206bfb82e59119dca4d36117671b2734089a", "微信扫一扫扫码绑盘"),
            commit("ai_api", "ba452b8723356fb65a90daac863980cd68a4ba7c", "绑盘"),
            commit("ai_api", "997ee05ba0a834c41e31ff55b28f50e8754423fc", "绑盘接口"),
            commit("ai_api", "dc4ee7231bedf185ff49b194c73cde9a6456738b", "微信扫一扫绑盘"),
            commit("store", "4f875af5fb88ada668f2aa2ad2baf55be378eaf7", "绑盘接口查询余额"),
        ],
    },
    {
        "parent": "VWCG-1072",
        "subject": "开发：一卡通组织、账户、卡及余额同步",
        "start": "2026-07-06", "end": "2026-07-13", "estimate": 32, "actual": 8,
        "status": "100011",
        "scope": "完成一卡通鉴权、身份类别、部门、人员/账户、卡与余额增量同步及同步状态维护。",
        "validation": "已核对同步命令、映射表及一致性校验；线上回读 consistent=true。",
        "commits": [
            commit("store", "14a44d92965216a26209996948ff04e1f0bd91c4", "一卡通 token"),
            commit("store", "08d89b9269d5e306e8cf82e15a3f003091b915db", "一卡通 access_token 鉴权"),
            commit("store", "8ff2ae780205edc894e986e0c15600524d7b2200", "一卡通鉴权 SQL 修正"),
            commit("store", "8ef2ef1551621e93bb381cc9a1098ee72082333a", "一卡通对接与同步框架"),
            commit("store", "53e06dcba2b65cd73fa7204f9e3b664ad0273abb", "一卡通身份类别、部门、人员/账户、卡、余额同步"),
            commit("store", "b2bc75c980ca9bc16aed26224a89b98a7371b03c", "一卡通余额同步游标优化"),
            commit("store", "9a0aec3c69b1efad545b0c731804d1ac231f911b", "一卡通同步 SQL 修正"),
            commit("store", "9758742bce3aa00769107cc81f4e46965c7b70a2", "一卡通手机号同步"),
            commit("store", "5b8f7dab4aa21eb502a62bc632fea684ec989fa1", "一卡通人员体测默认值"),
            commit("store", "59bf4be1cc4595d298e0861d9cc4bcdb9149fb55", "一卡通卡号兼容"),
            commit("store", "4d13c6fa127379a69f2b3203422514f9b49ceb64", "一卡通手机号与非法账号修复"),
            commit("store", "57b6ed3bf53fe5446d48d983c78be24255073a8c", "一卡通同步一致性与小额测试修复"),
            commit("store", "77f529f1e96406ad516929bc61eccade747cb0ab", "一卡通同步账号手机号填充"),
        ],
    },
    {
        "parent": "VWCG-1072",
        "subject": "开发：一卡通在线消费、退款与绑盘余额校验",
        "start": "2026-07-14", "end": "2026-07-15", "estimate": 24, "actual": 6,
        "status": "100011",
        "scope": "接入一卡通预下单、订单处理、退款冲正和交易补偿，并在绑盘入口按一卡通远端余额校验。",
        "validation": "store 一卡通测试、数据库回滚烟测和 ai_api 路由检查已有通过证据；保留现场小额交易验收。",
        "commits": [
            commit("store", "d438040d81ba482f9616d1739b140717f7d2340b", "一卡通交易 SQL 汇总修正"),
            commit("store", "567026adc16974451132001d13fa3918de86f069", "一卡通支付 SQL 版本修正"),
            commit("store", "a0390a262520f8db1fa41580f6ac732572300fc6", "一卡通支付表 SQL 修正"),
            commit("store", "0f585455bb036242fec4d958c3a9d9910ef9f332", "一卡通支付和退款冲正"),
            commit("store", "41eda95a0e9e9492fbc130f5bd6c8baf7438b616", "一卡通消费链路修正"),
            commit("ai_api", "cd539aa695163826db05737d35a56a6ef037dcf5", "一卡通支付和退款/冲正"),
            commit("store", "4f875af5fb88ada668f2aa2ad2baf55be378eaf7", "绑盘接口查询一卡通余额"),
            commit("store", "a7f4d7e264067213cb314dd98b92fc07d03f228e", "一卡通补贴优先消费"),
            commit("ai_api", "1517cd9b2328e0a5ff3b6d25e2bfa2ce88227d65", "一卡通双钱包路由修正"),
        ],
    },
    {
        "parent": "VWCG-1072",
        "subject": "开发：一卡通与访客混合模式及冲突治理",
        "start": "2026-07-17", "end": "2026-07-31", "estimate": 24, "actual": 8,
        "status": "100011",
        "scope": "按 one_card 配置和 account map 区分一卡通用户与访客，补齐充值、提现、本地补贴门禁，并记录手机号与本地访客冲突。",
        "validation": "访客资金测试、一卡通用户门禁和同步一致性证据已核对。",
        "commits": [
            commit("store", "55826d483416c52a59803816272cbf2d1f1664f4", "滨州访客"),
            commit("ai_api", "2901dfd7072d0db0807f1717dcc2a94257adb95e", "滨州访客"),
            commit("store", "7e13e01a42a780267d8ffe35093321740f907715", "访客与一卡通混合模式测试"),
            commit("store", "bd3903cfa576f397004bedc5b38e0639d544d584", "访客财务逐笔流水接口"),
            commit("store", "34659b2573adba0f8b8878d2760ef15fcf6c2c18", "复原既有提现处理逻辑"),
            commit("store", "d398ca2e220c7341dbcaa3f83e2a8baa6ff886e9", "复用商户认证并隔离访客流水"),
            commit("store", "00631e51e55a9d42b419ff6eb4527399f9672ec0", "精简并加固访客财务流水接口"),
            commit("ai_api", "bd452cca144ca25bf10fd8dc871a42d8d5c9b9d1", "提现失败成功流水保护尝试"),
            commit("ai_api", "e98a79e6a02f288603fe741f87db902ac4fda3aa", "复原既有提现处理逻辑"),
            commit("store", "128fcde68256f9b9210d151677487c67001823b4", "恢复访客流水类型和手机号筛选"),
            commit("store", "f1414939b11d2767b9edc3c7187c57919fa4a3fa", "访客消费与绑盘分流"),
            commit("ai_api", "961c089648dbbf7d3d8b1614cbc670b199373889", "访客支付与退款分流"),
            commit("store", "7ece37ee0f0e92a53260261851b1029644256c11", "一卡通用户本地补贴门禁"),
            commit("store", "4e1b4488139322ff50c175d3091dd9cd90b0e3ab", "一卡通手机号与访客冲突保护"),
        ],
    },
    {
        "parent": "VWCG-1072",
        "subject": "开发：消费机离线补传与一卡通三态回执",
        "start": "2026-07-28", "end": "2026-07-30", "estimate": 16, "actual": 6,
        "status": "100011",
        "scope": "离线订单补传由后端识别一卡通用户并走预下单交易链路，消费机按成功、可重试、终态失败三态处理回执。",
        "validation": "后端回归已完成；Android Gradle、APK 和现场离线小额交易仍由测试阶段验收。",
        "commits": [
            commit("store", "1c5550563c12a0dde1c2f245f6950d0056334afa", "补全一卡通离线消费链路"),
            commit("consume", "8d5b6933cbb58afe8cc3d8c4eaaaaffa34dcdfb0", "支持离线订单三态回执"),
        ],
    },
    {
        "parent": "VWCG-1072",
        "subject": "开发：一卡通双钱包、补贴流水与一致性校验",
        "start": "2026-08-03", "end": "2026-08-04", "estimate": 24, "actual": 8,
        "status": "100011",
        "scope": "接入现金 1 号钱包与补贴 2 号钱包，补贴优先消费，镜像现金/补贴余额并记录本地补贴发放与消费流水，补齐同步一致性检查。",
        "validation": "store 131 tests/680 assertions、双钱包数据库烟测、线上账户映射和余额一致性回读已有通过证据。",
        "commits": [
            commit("store", "1b146022ee6d87af89f481582d893cf1205fad95", "一卡通接口直观检查脚本"),
            commit("store", "b9db0bfb26feba56d6034e5d156283f3423612d7", "一卡通双钱包基础"),
            commit("ai_api", "707e94e110f36dea1ac901d72c3bb8913110caa6", "一卡通双钱包消费退款"),
            commit("store", "a7f4d7e264067213cb314dd98b92fc07d03f228e", "一卡通补贴优先消费"),
            commit("ai_api", "1517cd9b2328e0a5ff3b6d25e2bfa2ce88227d65", "一卡通双钱包路由修正"),
            commit("store", "1365e1de18c58528d1aa2dcb16f478a5bd7e2c1d", "一卡通补贴同步"),
            commit("store", "b853ff655fbf45c988b79570aa6f21939b72d340", "一卡通补贴异常包终态处理"),
            commit("ai_api", "4ce14c510dccdcf59c7d156675e1e0e6d414e328", "一卡通补贴余额展示"),
            commit("store", "61e9e95b70ac3de5b786f70424471ed3c3763953", "一卡通双钱包本地流水收口"),
            commit("ai_api", "dab284c6fd4c7f039bcac9d77dc4e35bcddf6122", "一卡通双钱包收口"),
        ],
    },
]


def api(method, path, body=None, allow=(200, 201, 204)):
    data = None if body is None else json.dumps(body, ensure_ascii=False).encode()
    request = urllib.request.Request(
        BASE + path,
        data=data,
        method=method,
        headers={"x-yunxiao-token": TOKEN, "content-type": "application/json"},
    )
    try:
        with urllib.request.urlopen(request) as response:
            raw = response.read().decode()
            result = json.loads(raw) if raw else None
            return response.status, result
    except urllib.error.HTTPError as exc:
        raw = exc.read().decode()
        try:
            detail = json.loads(raw)
        except json.JSONDecodeError:
            detail = raw
        if exc.code in allow:
            return exc.code, detail
        raise RuntimeError(f"{method} {path} failed: HTTP {exc.code} {detail}") from exc


def list_tasks():
    result = []
    page = 1
    while True:
        _, rows = api("POST", f"/oapi/v1/projex/organizations/{ORG}/workitems:search", {
            "spaceId": SPACE, "category": "Task", "page": page, "perPage": 200,
        })
        result.extend(rows)
        if len(rows) < 200:
            return result
        page += 1


def description(spec):
    return (
        f"## 父需求\n{spec['parent']}\n\n"
        f"## 实现范围\n{spec['scope']}\n\n"
        f"## 验证\n{spec['validation']}\n\n"
        "## 计划\n"
        f"- 计划开始：{spec['start']}\n"
        f"- 计划完成：{spec['end']}\n"
        f"- 预计工时：{spec['estimate']}h\n"
        "- 创建来源：Codex 依据滨州历史代码与实施记录回填\n"
    )


def epoch(date_text, hour):
    zone = dt.timezone(dt.timedelta(hours=8))
    value = dt.datetime.fromisoformat(date_text).replace(hour=hour, tzinfo=zone)
    return int(value.timestamp() * 1000)


def create_task(spec):
    body = {
        "subject": spec["subject"],
        "description": description(spec),
        "documentFormat": "MARKDOWN",
        "assignedTo": ASSIGNEE,
        "spaceId": SPACE,
        "workitemTypeId": TASK_TYPE,
        "parentId": PARENTS[spec["parent"]],
        "customFieldValues": {
            "79": spec["start"], "80": spec["end"], "priority": PRIORITY_MEDIUM,
        },
        "participants": [ASSIGNEE],
    }
    _, result = api("POST", f"/oapi/v1/projex/organizations/{ORG}/workitems", body)
    task_id = result.get("id") if isinstance(result, dict) else result
    if not task_id:
        raise RuntimeError(f"create returned no task id: {result}")
    return task_id


def add_relation(task_id, relation):
    body = {
        "category": "codeupCommit",
        "projectId": relation["projectId"],
        "commitSha": relation["sha"],
        "title": f"{relation['repo']} {relation['sha'][:9]} {relation['title']}",
        "url": relation["url"],
    }
    paths = [
        f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/extRelationRecords",
        f"/oapi/v1/projex/workitems/{task_id}/extRelationRecords",
    ]
    errors = []
    for path in paths:
        try:
            _, result = api("POST", path, body)
            return {"path": path, "result": result}
        except RuntimeError as exc:
            errors.append(str(exc))
    raise RuntimeError("; ".join(errors))


def list_relations(task_id):
    for path in (
        f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/extRelationRecords?category=codeupCommit",
        f"/oapi/v1/projex/workitems/{task_id}/extRelationRecords?category=codeupCommit",
    ):
        try:
            _, result = api("GET", path)
            return result
        except RuntimeError:
            continue
    return []


def add_comment(workitem_id, content):
    _, result = api("POST", f"/oapi/v1/projex/organizations/{ORG}/workitems/{workitem_id}/comments", {"content": content})
    return result


def ensure_comment(workitem_id, marker, content):
    _, comments = api("GET", f"/oapi/v1/projex/organizations/{ORG}/workitems/{workitem_id}/comments")
    if any(marker in (comment.get("content") or "") for comment in comments):
        return None
    return add_comment(workitem_id, content)


def update_status(workitem_id, status):
    api("PUT", f"/oapi/v1/projex/organizations/{ORG}/workitems/{workitem_id}", {"status": status})


def read_workitem(workitem_id):
    _, result = api("GET", f"/oapi/v1/projex/organizations/{ORG}/workitems/{workitem_id}")
    return result


def compact(item):
    fields = {}
    for field in item.get("customFieldValues") or []:
        fields[field.get("fieldId")] = [v.get("displayValue") for v in field.get("values") or []]
    return {
        "id": item.get("id"), "serialNumber": item.get("serialNumber"),
        "subject": item.get("subject"), "parentId": item.get("parentId"),
        "status": item.get("status"), "assignedTo": item.get("assignedTo"),
        "fields": fields,
    }


def main():
    existing = {(x.get("parentId"), x.get("subject")): x for x in list_tasks()}
    evidence = {"tasks": [], "parents": {}}
    by_parent = {}
    for spec in TASKS:
        key = (PARENTS[spec["parent"]], spec["subject"])
        task = existing.get(key)
        task_id = task["id"] if task else create_task(spec)

        _, estimates = api("GET", f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/estimatedEfforts")
        if not estimates:
            api("POST", f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/estimatedEfforts", {
                "spentTime": spec["estimate"], "owner": ASSIGNEE,
                "description": "按无 AI 的人工开发、联调与验证投入估算。",
            })

        _, actuals = api("GET", f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/effortRecords")
        if spec["actual"] is not None and not actuals:
            api("POST", f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/effortRecords", {
                "actualTime": spec["actual"], "owner": ASSIGNEE,
                "gmtStart": epoch(spec["start"], 9), "gmtEnd": epoch(spec["end"], 18),
                "description": "依据历史提交、实现批次和验证记录折算的 AI 辅助实际投入。",
            })

        relations_before = list_relations(task_id)
        linked_shas = {x.get("businessId") for x in relations_before or [] if isinstance(x, dict)}
        relation_results = []
        for relation in spec["commits"]:
            if relation["sha"] not in linked_shas:
                relation_results.append(add_relation(task_id, relation))

        if relation_results:
            relation_marker = "2026-08-04 代码关联补全 v2"
            relation_lines = "\n".join(
                f"- [{x['repo']}@{x['sha'][:9]}]({x['url']}) {x['title']}" for x in spec["commits"]
            )
            ensure_comment(
                task_id,
                relation_marker,
                f"## {relation_marker}\n已按代码仓库提交历史补全本任务的真实实现提交：\n{relation_lines}\n\n"
                "待处理任务没有实际实现代码，不做错误关联；本次也未创建空提交或无差异 MR。",
            )

        current = read_workitem(task_id)
        if current.get("status", {}).get("id") != spec["status"]:
            update_status(task_id, spec["status"])

        code_lines = "\n".join(f"- [{x['repo']}@{x['sha'][:9]}]({x['url']}) {x['title']}" for x in spec["commits"]) or "- 暂无代码：该任务待开发。"
        marker = "2026-08-04 批量回填"
        comment = (
            f"## {marker}\n"
            f"- 父需求：{spec['parent']}\n"
            f"- 计划：{spec['start']} 至 {spec['end']}\n"
            f"- 预计工时：{spec['estimate']}h\n"
            f"- 实际工时：{str(spec['actual']) + 'h' if spec['actual'] is not None else '待开发后填写'}\n"
            f"- 验证：{spec['validation']}\n\n"
            f"### 真实代码提交\n{code_lines}\n\n"
            "历史提交标题未统一包含本子任务号，本次通过云效代码外部关系和评论补建可追溯关联；未创建空提交或无差异 MR。"
        )
        ensure_comment(task_id, marker, comment)

        final_item = read_workitem(task_id)
        _, final_estimates = api("GET", f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/estimatedEfforts")
        _, final_actuals = api("GET", f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/effortRecords")
        final_relations = list_relations(task_id)
        evidence["tasks"].append({
            "workitem": compact(final_item),
            "estimatedEfforts": final_estimates,
            "effortRecords": final_actuals,
            "relations": final_relations,
            "relationWrites": relation_results,
        })
        by_parent.setdefault(spec["parent"], []).append(final_item)

    parent_statuses = {
        "VWCG-1065": "142838",
        "VWCG-1070": "142838",
        "VWCG-1071": "1dec9023be7be6cf79e6bde901",
        "VWCG-1072": "1dec9023be7be6cf79e6bde901",
    }
    for serial, status in parent_statuses.items():
        parent_id = PARENTS[serial]
        children = by_parent[serial]
        lines = "\n".join(
            f"- {x['serialNumber']} {x['subject']}：{x['status']['name']}" for x in children
        )
        if serial == "VWCG-1065":
            boundary = "正式 V2 的 token 映射、后台管理、专用页面和真机回归仍有待处理任务，因此父需求保持开发中。"
        elif serial == "VWCG-1070":
            boundary = "C 前缀设备链路已实现；后端双格式兼容、并发幂等和专项回归仍有待处理任务，因此父需求保持开发中。"
        else:
            boundary = "历史代码已关联，开发子任务进入开发完成；父需求进入待测试，保留现场或真机最终验收。"
        parent_marker = "2026-08-04 任务拆分与代码回填"
        ensure_comment(parent_id, parent_marker, f"## {parent_marker}\n{lines}\n\n{boundary}")
        current = read_workitem(parent_id)
        if current.get("status", {}).get("id") != status:
            update_status(parent_id, status)
        evidence["parents"][serial] = compact(read_workitem(parent_id))

    output = pathlib.Path(__file__).with_name("yunxiao-readback.sanitized.json")
    output.write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps({
        "tasks": [{"serial": x["workitem"]["serialNumber"], "status": x["workitem"]["status"]["name"]} for x in evidence["tasks"]],
        "parents": {k: v["status"]["name"] for k, v in evidence["parents"].items()},
        "evidence": str(output),
    }, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
