#!/usr/bin/env python3
"""Create the 安徽武警 parent requirement and six child tasks idempotently."""

from __future__ import annotations

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"]

REQ_TYPE = "9uy29901re573f561d69jn40"
TASK_TYPE = "ba102e46bc6a8483d9b7f25c"
PENDING = "100005"
LI_XIAO = "63575519287a2b849fa9a642"
LAI_QINGTAO = "643cc26b2ca4e1cd30923389"

SUBJECT = "【安徽武警】智慧食堂大屏、生日妈妈菜、带量食谱与意见闭环"
PUBLIC_PROTO = (
    "https://img.kxunpt.cn/public/ai-marketing-platform/modules/product/products/"
    "smart-canteen/requirements/2026-08-22-anhui-armed-police-demand-gap/"
    "anhui-armed-police-smart-canteen-confirmation-prototype.html"
)
DOC_BASE = (
    "https://zhctpmt.yyangpt.cn/modules/product/products/smart-canteen/requirements/"
    "2026-08-22-anhui-armed-police-demand-gap"
)

TASKS = [
    {
        "subject": "开发：安徽武警需求契约数据库与接口基础",
        "start": "2026-08-24",
        "end": "2026-08-25",
        "hours": 16,
        "repos": "store",
        "scope": "冻结生日妈妈菜、独立加餐、带量快照、评价处理数据对象及状态机，完成权限、审计和大屏空快照接口基础。",
        "acceptance": "SQL/状态机/权限/大屏空快照专项测试通过；接口不返回手机号、身份证、余额、订单明细或未授权照片。",
    },
    {
        "subject": "开发：生日妈妈菜加餐与宣传内容后台管理",
        "start": "2026-08-26",
        "end": "2026-08-28",
        "hours": 24,
        "repos": "store",
        "scope": "沿用现有 PC 演示系统，实现生日妈妈菜、提醒与隐藏、独立加餐计划和吃动平衡内容有效期。",
        "acceptance": "PC 构建通过；生日/加餐/宣传主流程、空态、长文本、无图、权限和刷新恢复可验证。",
    },
    {
        "subject": "开发：98/100寸智慧食堂大屏与生日点触交互",
        "start": "2026-08-29",
        "end": "2026-09-02",
        "hours": 24,
        "repos": "store",
        "scope": "实现 4+4 当餐菜品轮播、周菜谱夜餐过滤与加餐高亮、生日妈妈菜详情、健康宣传和最近成功快照降级。",
        "acceptance": "3840×2160 与 1920×1080 页面无关键裁切；触摸、10 秒轮播、15 秒返回、断网降级和敏感字段门禁通过。",
    },
    {
        "subject": "开发：带量食谱计算与周菜谱Excel",
        "start": "2026-09-03",
        "end": "2026-09-07",
        "hours": 32,
        "repos": "store",
        "scope": "实现计划人数、配方、单位、损耗/出成率计算框架，数据完整度阻断、审核快照和周菜谱 Excel。",
        "acceptance": "黄金样例、数据门禁、下钻和 Excel 同源回读通过；缺配方时禁止标记为可执行。",
    },
    {
        "subject": "开发：iPad就餐意见与后台周反馈闭环",
        "start": "2026-09-08",
        "end": "2026-09-10",
        "hours": 24,
        "repos": "ai_web、ai_api、store",
        "scope": "复用现有评价字段，实现 iPad 固定终端布局、提交幂等、后台周汇总、处理状态、值班厨师配置和菜谱备注写回。",
        "acceptance": "身份策略按客户确认配置；重复/失败提交、周汇总、处理审计、写回与撤回测试通过。",
    },
    {
        "subject": "联调验收：安徽武警多端业务闭环与客户确认",
        "start": "2026-09-11",
        "end": "2026-09-15",
        "hours": 24,
        "repos": "store、ai_web、ai_api",
        "scope": "按 8 页面流程执行构建、接口、视觉、权限、导出、断网恢复和 intended-vs-implemented 验收。",
        "acceptance": "P0 测试矩阵 100% 通过；8 页面截图与控制台证据齐全；无高风险隐私或错误带量结论；形成客户确认报告。",
    },
]


def api(method: str, path: str, body=None, allow=(200, 201, 204)):
    data = None if body is None else json.dumps(body, ensure_ascii=False).encode()
    req = urllib.request.Request(
        BASE + path,
        data=data,
        method=method,
        headers={"x-yunxiao-token": TOKEN, "content-type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req) as response:
            raw = response.read().decode()
            return response.status, json.loads(raw) if raw else None
    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_items(category: str):
    result = []
    for page in range(1, 30):
        _, rows = api(
            "POST",
            f"/oapi/v1/projex/organizations/{ORG}/workitems:search",
            {"spaceId": SPACE, "category": category, "page": page, "perPage": 200},
        )
        result.extend(rows)
        if len(rows) < 200:
            return result
    raise RuntimeError(f"{category} pagination exceeded safe limit")


def parent_description():
    links = [
        ("客户确认原型（外网）", PUBLIC_PROTO),
        ("需求与标品差异分析", f"{DOC_BASE}/anhui-armed-police-demand-gap-analysis.md"),
        ("Codex Prompt 包入口", f"{DOC_BASE}/codex-development-package/README.md"),
        ("正式需求规格", f"{DOC_BASE}/codex-development-package/01-requirement-spec.md"),
        ("系统与数据合同", f"{DOC_BASE}/codex-development-package/02-system-and-data-contract.md"),
        ("页面交互合同", f"{DOC_BASE}/codex-development-package/03-page-interaction-contract.md"),
        ("实施计划", f"{DOC_BASE}/codex-development-package/04-implementation-plan.md"),
        ("测试与验收矩阵", f"{DOC_BASE}/codex-development-package/05-test-and-acceptance.md"),
        ("开发前待确认项", f"{DOC_BASE}/codex-development-package/07-open-decisions.md"),
        ("Codex 总执行 Prompt", f"{DOC_BASE}/codex-development-package/prompts/00-codex-master-prompt.md"),
        ("Build Verifier Prompt", f"{DOC_BASE}/codex-development-package/prompts/01-build-verifier-prompt.md"),
        ("Visual Auditor Prompt", f"{DOC_BASE}/codex-development-package/prompts/02-visual-auditor-prompt.md"),
        ("意图与实现复核 Prompt", f"{DOC_BASE}/codex-development-package/prompts/03-intended-vs-implemented-review-prompt.md"),
    ]
    link_text = "\n".join(f"- [{title}]({url})" for title, url in links)
    return f"""## 需求背景
安徽武警智慧食堂需要在现有康比特智慧营养健康餐厅系统基础上，形成“餐前准备—大屏展示—餐后反馈—下周改进”的业务闭环。现有系统可复用菜谱、人员、营养资讯和就餐评价底座；生日妈妈菜、独立加餐、带量食谱、周反馈处理和 98/100 寸大屏需要增量实现。

## 目标用户
- 食堂管理员、营养/厨务负责人、宣传管理员、下周值班厨师。
- 就餐人员和大屏浏览/点触用户。

## 首期范围
1. 生日妈妈菜登记、提醒、授权、上屏和点触详情。
2. 菜谱与独立加餐管理；夜餐后台保留但不进入主屏。
3. 当餐 8 道菜 4+4 轮播、周菜谱、健康宣传和断网降级。
4. 带量食谱数据门禁、计算快照和周菜谱 Excel。
5. iPad 文字意见、后台周汇总、处理状态和值班厨师菜谱备注写回。

## 开发边界
- 当前阶段为 `CONDITIONAL_READY`：页面和业务流程可进入任务拆解；生日公开授权、加餐定义、带量公式、iPad 身份策略、排班来源和硬件/内网仍需负责人关闭。
- 未确认项只能使用配置开关、合成数据或失败关闭，不得替客户拍板。
- 不授权生产发布、生产 SQL、真实人员导入或演示系统数据复制。

## 验收要点
- 23 个需求 ID 和 P0 测试矩阵可追溯。
- 8 个页面在 PC、4K/1080p 大屏和 iPad 视口完成真实点击与截图验收。
- 大屏匿名快照不包含手机号、身份证、余额、订单明细或未授权照片。
- 带量食谱在数据不完整时失败关闭。
- iPad 重复提交、周汇总、处理审计和菜谱备注写回可回归。

## 原型与资料
{link_text}

## 负责人
- 父需求：李潇。
- 开发与联调子任务：赖清涛。

## 证据边界
原型中的人员、订单、评价、数量、营养值和原料量均为合成数据；真实演示系统只做了只读取证。
"""


def task_description(spec):
    return f"""## 父需求
{SUBJECT}

## 实现范围
{spec['scope']}

## 目标仓库
{spec['repos']}

## 验收标准
{spec['acceptance']}

## 计划
- 计划开始：{spec['start']}
- 计划完成：{spec['end']}
- 无 AI 预计工时：{spec['hours']}h

## 开发 Prompt 与资料
- [Codex Prompt 包入口]({DOC_BASE}/codex-development-package/README.md)
- [需求规格]({DOC_BASE}/codex-development-package/01-requirement-spec.md)
- [实施计划]({DOC_BASE}/codex-development-package/04-implementation-plan.md)
- [测试矩阵]({DOC_BASE}/codex-development-package/05-test-and-acceptance.md)
- [客户确认原型]({PUBLIC_PROTO})

## 停止规则
未关闭的业务决策只允许配置/合成数据/失败关闭；不得发布生产、执行生产 SQL 或导入真实客户数据。
"""


def create_parent():
    reqs = list_items("Req")
    exact = [row for row in reqs if row.get("subject") == SUBJECT]
    if len(exact) > 1:
        raise RuntimeError("multiple exact parent requirements found")
    if exact:
        return exact[0]["id"], False
    _, result = api(
        "POST",
        f"/oapi/v1/projex/organizations/{ORG}/workitems",
        {
            "subject": SUBJECT,
            "description": parent_description(),
            "documentFormat": "MARKDOWN",
            "assignedTo": LI_XIAO,
            "participants": [LI_XIAO, LAI_QINGTAO],
            "spaceId": SPACE,
            "workitemTypeId": REQ_TYPE,
        },
    )
    parent_id = result.get("id") if isinstance(result, dict) else result
    if not parent_id:
        raise RuntimeError(f"parent create returned no id: {result}")
    return parent_id, True


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


def update_pending(item_id):
    current = read_item(item_id)
    if current.get("status", {}).get("id") != PENDING:
        api("PUT", f"/oapi/v1/projex/organizations/{ORG}/workitems/{item_id}", {"status": PENDING})


def create_tasks(parent_id):
    existing = {
        (row.get("parentId"), row.get("subject")): row for row in list_items("Task")
    }
    results = []
    for spec in TASKS:
        key = (parent_id, spec["subject"])
        item = existing.get(key)
        created = False
        if item:
            task_id = item["id"]
        else:
            _, created_item = api(
                "POST",
                f"/oapi/v1/projex/organizations/{ORG}/workitems",
                {
                    "subject": spec["subject"],
                    "description": task_description(spec),
                    "documentFormat": "MARKDOWN",
                    "assignedTo": LAI_QINGTAO,
                    "participants": [LAI_QINGTAO, LI_XIAO],
                    "spaceId": SPACE,
                    "workitemTypeId": TASK_TYPE,
                    "parentId": parent_id,
                    "customFieldValues": {"79": spec["start"], "80": spec["end"]},
                },
            )
            task_id = created_item.get("id") if isinstance(created_item, dict) else created_item
            if not task_id:
                raise RuntimeError(f"task create returned no id: {created_item}")
            created = True
        update_pending(task_id)
        _, 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["hours"],
                    "owner": LAI_QINGTAO,
                    "description": "按无 AI 的需求理解、设计、开发、测试、评审和提交准备估算。",
                },
            )
        item = read_item(task_id)
        _, estimates = api(
            "GET",
            f"/oapi/v1/projex/organizations/{ORG}/workitems/{task_id}/estimatedEfforts",
        )
        results.append({"created": created, "spec": spec, "item": item, "estimatedEfforts": estimates})
    return results


def compact(item):
    fields = {}
    for field in item.get("customFieldValues") or []:
        fields[field.get("fieldId")] = [value.get("displayValue") for value 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"),
        "workitemType": item.get("workitemType"),
        "fields": fields,
    }


def main():
    parent_id, parent_created = create_parent()
    update_pending(parent_id)
    parent = read_item(parent_id)
    tasks = create_tasks(parent_id)
    lines = "\n".join(
        f"- {entry['item'].get('serialNumber')} {entry['item'].get('subject')}："
        f"{entry['item'].get('assignedTo', {}).get('name')} / {entry['item'].get('status', {}).get('name')}"
        for entry in tasks
    )
    marker = "2026-08-23 Codex Prompt 包与任务拆解"
    _, comments = api("GET", f"/oapi/v1/projex/organizations/{ORG}/workitems/{parent_id}/comments")
    if not any(marker in (comment.get("content") or "") for comment in comments):
        api(
            "POST",
            f"/oapi/v1/projex/organizations/{ORG}/workitems/{parent_id}/comments",
            {"content": f"## {marker}\n{lines}\n\nPrompt 包提交：`zhctprompt@735968aa2`\n\n客户原型：{PUBLIC_PROTO}"},
        )
    evidence = {
        "parentCreated": parent_created,
        "parent": compact(read_item(parent_id)),
        "tasks": [
            {
                "created": entry["created"],
                "workitem": compact(entry["item"]),
                "estimatedEfforts": entry["estimatedEfforts"],
                "plan": {
                    "start": entry["spec"]["start"],
                    "finish": entry["spec"]["end"],
                    "estimatedHours": entry["spec"]["hours"],
                    "repos": entry["spec"]["repos"],
                },
            }
            for entry in tasks
        ],
        "prototype": PUBLIC_PROTO,
        "promptCommit": "735968aa2fc8f9a11e3af4404d6d3495acd10f8d",
    }
    out = pathlib.Path(__file__).with_name("yunxiao-readback.sanitized.json")
    out.write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding="utf-8")
    print(
        json.dumps(
            {
                "parent": {
                    "serial": evidence["parent"]["serialNumber"],
                    "status": evidence["parent"]["status"].get("name"),
                    "assignee": evidence["parent"]["assignedTo"].get("name"),
                },
                "tasks": [
                    {
                        "serial": row["workitem"]["serialNumber"],
                        "status": row["workitem"]["status"].get("name"),
                        "assignee": row["workitem"]["assignedTo"].get("name"),
                        "start": row["plan"]["start"],
                        "finish": row["plan"]["finish"],
                        "hours": row["plan"]["estimatedHours"],
                    }
                    for row in evidence["tasks"]
                ],
                "evidence": str(out),
            },
            ensure_ascii=False,
            indent=2,
        )
    )


if __name__ == "__main__":
    main()
