#!/usr/bin/env python3
"""Build a two-endpoint Apifox OpenAPI patch from the current remote contract."""

import copy
import importlib.util
import json
import os
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
SKILL = Path('/Users/lqt/.codex/skills/apifox-api-sync')
module_spec = importlib.util.spec_from_file_location('apifox_sync', SKILL / 'scripts/sync_apifox_openapi.py')
sync = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(sync)
sync.load_env_file(SKILL / '.local.env')
token = os.environ['APIFOX_ACCESS_TOKEN']
status, exported = sync.export_openapi('5737553', token)
if status != 200:
    raise RuntimeError(f'Apifox export failed: HTTP {status}')

paths = ['/p/api/facedata', '/p/api/userdata']
for path in paths:
    if 'post' not in exported.get('paths', {}).get(path, {}):
        raise RuntimeError(f'Missing existing POST {path}')

before_path = Path('/Users/lqt/.codex/apifox-backups/2026-10-10-face-sync/apifox_face_sync_export_before.json')
before_path.parent.mkdir(parents=True, exist_ok=True)
before_path.write_text(json.dumps(exported, ensure_ascii=False, indent=2), encoding='utf-8')
before_path.chmod(0o600)

spec = {
    'openapi': '3.0.0',
    'info': {'title': '智慧营养健康餐厅人脸同步接口契约修订', 'version': '2026-10-10'},
    'paths': {},
}
for path in paths:
    operation = copy.deepcopy(exported['paths'][path]['post'])
    operation['tags'] = []
    operation['x-apifox-folder'] = ''
    spec['paths'][path] = {'post': operation}

face = spec['paths']['/p/api/facedata']['post']
face['description'] = '''## 接口概述

设备读取既有人脸数据；此接口保持原有契约，不承载张家港设备端学习模式。全量查询仅下发已有非空 `feature` 的有效人脸；无特征的人脸不会出现在全量结果中。

## 接口信息

| 项目 | 内容 |
| --- | --- |
| 地址 | `POST /p/api/facedata` |
| Content-Type | `application/x-www-form-urlencoded` |
| 调用方 | 人脸设备 |

## 请求头（Header）

`Content-Type: application/x-www-form-urlencoded`。历史可选 `Access-Token` 头仍保留；当前业务方法以设备编码核对设备是否存在，不校验该头。

## 请求参数（Body）

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `code` | 是 | 已登记且未删除的设备编码。 |
| `type` | 否 | `1` 全量，`0` 增量；默认 `0`，其他值按 `0` 处理。 |
| `staff_uuid` | 否 | 全量时可只返回指定人员；增量时限制待处理消息。 |

## 响应格式

成功：`{"code":0,"message":"查询成功","data":{"face_data":[{"id":123,"staff_uuid":"11111111-2222-3333-4444-555555555555","head_img_url":"http://example.invalid/static/upload/face/example.jpg","feature":"<已学习特征>","face_token":"<已学习token>","name":"示例人员","hardware_uuid":"66666666-7777-8888-9999-000000000000","status":1}]},"trace_id":"<本次请求追踪ID>"}`。

失败：`{"code":1,"message":"设备不存在","data":null,"trace_id":"<本次请求追踪ID>"}`。无匹配人员时 `face_data` 可以为空数组。

## 状态码与设备处理

HTTP 通常为 200，必须以顶层 `code` 判定业务结果：`0` 处理 `data.face_data`，`1` 不更新本地数据并核查参数或设备登记。全量拉取会更新设备同步时间，并将该设备待处理的人脸消息标记已处理。

## 响应字段说明

`data.face_data[].id` 为人员 ID；`head_img_url` 为完整照片地址；`feature` 和 `face_token` 是已有学习结果；`status=1` 为全量有效人脸。`trace_id` 在成功和业务失败响应中均由项目统一补充。

## trace_id 排查说明

排查时提供 `trace_id`、设备编码和请求时间，以便关联服务端日志；不要在工单中传播真实人脸特征值。
'''
face_body = face['requestBody']['content']['application/x-www-form-urlencoded']
face_body['schema']['properties']['type']['description'] = '0=增量；1=全量；默认0。'
face_response = face['responses']['200']['content']['application/json']
face_schema = face_response['schema']
face_schema['properties'].pop('face_learning_mode', None)
face_schema['x-apifox-orders'] = [key for key in face_schema.get('x-apifox-orders', []) if key != 'face_learning_mode']
face_schema['properties']['data']['properties']['face_data']['items']['properties']['feature']['description'] = '已有的人脸特征；全量不返回无特征记录。'
face_schema['properties']['data']['properties']['face_data']['items']['properties']['face_token']['description'] = '已学习的人脸 token。'
face_response['examples'] = {
    'fullSuccess': {'summary': '全量：已有特征人脸', 'value': {
        'code': 0, 'message': '查询成功', 'data': {'face_data': [{
            'id': 123, 'staff_uuid': '11111111-2222-3333-4444-555555555555',
            'head_img_url': 'http://example.invalid/static/upload/face/example.jpg',
            'feature': '<已学习特征>', 'face_token': '<已学习token>', 'name': '示例人员',
            'hardware_uuid': '66666666-7777-8888-9999-000000000000', 'status': 1,
        }]}, 'trace_id': '<本次请求追踪ID>',
    }},
    'businessError': {'summary': '设备不存在', 'value': {
        'code': 1, 'message': '设备不存在', 'data': None, 'trace_id': '<本次请求追踪ID>',
    }},
}

user = spec['paths']['/p/api/userdata']['post']
user['description'] = '''## 接口概述

设备同步人员、人脸照片及卡片数据。张家港设备端学习使用 `type=1` 全量：读取 `data.face_learning_mode`，模式 `2` 下有照片但尚无特征的人员仍以 `status=1` 下发，由设备下载照片并学习；学习成功后调用 `/p/api/facedataLearnResult` 回传。`type=0` 增量沿用原有消息队列逻辑，本期不改造增量下发。

## 接口信息

| 项目 | 内容 |
| --- | --- |
| 地址 | `POST /p/api/userdata` |
| Content-Type | `application/x-www-form-urlencoded` |
| 调用方 | 消费机等安卓设备 |

## 请求头（Header）

`Content-Type: application/x-www-form-urlencoded`。历史可选 `Access-Token` 头仍保留；当前业务方法以设备编码核对设备是否存在，不校验该头。

## 请求参数（Body）

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `code` | 是 | 已登记且未删除的设备编码。 |
| `type` | 否 | `1` 全量，`0` 增量；默认 `0`，其他值按 `0` 处理。 |

## 响应格式

成功示例（模式 2、待设备学习）：

```json
{
  "code": 0,
  "message": "查询成功",
  "data": {
    "face_data": [
      {
        "id": 123,
        "staff_uuid": "11111111-2222-3333-4444-555555555555",
        "name": "示例人员",
        "head_img_url": "http://example.invalid/static/upload/face/example.jpg",
        "feature": "",
        "face_token": "",
        "hardware_uuid": "66666666-7777-8888-9999-000000000000",
        "status": 1,
        "card_id": ""
      }
    ],
    "face_learning_mode": 2
  },
  "trace_id": "<本次请求追踪ID>"
}
```

失败示例：

```json
{
  "code": 1,
  "message": "设备不存在",
  "data": null,
  "trace_id": "<本次请求追踪ID>"
}
```

增量成功时 `data.face_data` 为消息队列中的待同步记录，具体记录字段按消息类型变化。

## 状态码与设备处理

HTTP 通常为 200，必须以顶层 `code` 判定业务结果。`data.face_learning_mode=1` 表示服务端学习，`2` 表示设备端学习；该字段在成功响应中返回，**不在响应最外层**。模式 2 下，有照片且 `face_token` 为空时，设备下载照片并学习；有 token 时按既有设备逻辑保存。无照片也无有效卡号时 `status=3`；有有效卡号时仍可作为有效人员。全量查询更新设备同步时间并处理积累消息。

## 响应字段说明

`data.face_data` 是人员、人脸和卡片列表；`id` 为 `ydy_staff.id`，学习结果回传时传作 `staff_id`；`head_img_url` 是完整照片地址，回传时须原样传入；`feature`、`face_token` 在未学习时可以为空；`card_id` 可为空。全量记录的 `status=1` 为有效，`status=3` 为删除/无有效人脸或卡号；增量状态沿用原消息。`data.face_learning_mode` 为 `1` 或 `2`。`trace_id` 在成功和业务失败响应中均由项目统一补充。

## trace_id 排查说明

排查时提供 `trace_id`、设备编码和请求时间，以便关联服务端日志；不要在工单中传播真实人脸特征值。
'''
user_response = user['responses']['200']['content']['application/json']
user_response.pop('example', None)
user_response['schema'] = {
    'type': 'object', 'required': ['code', 'message', 'data', 'trace_id'],
    'properties': {
        'code': {'type': 'integer', 'enum': [0, 1], 'description': '0成功，1业务失败。'},
        'message': {'type': 'string'},
        'data': {'type': 'object', 'nullable': True, 'properties': {
            'face_data': {'type': 'array', 'items': {'type': 'object', 'properties': {
                'id': {'type': 'integer'}, 'staff_uuid': {'type': 'string'}, 'name': {'type': 'string'},
                'head_img_url': {'type': 'string'}, 'feature': {'type': 'string'},
                'face_token': {'type': 'string'}, 'hardware_uuid': {'type': 'string'},
                'status': {'type': 'integer'}, 'card_id': {'type': 'string'},
            }}},
            'face_learning_mode': {'type': 'integer', 'enum': [1, 2],
                                   'description': '成功响应：1服务端学习，2设备端学习。'},
        }},
        'trace_id': {'type': 'string', 'description': '固定返回的请求追踪ID。'},
    },
}
user_response['examples'] = {
    'deviceLearning': {'summary': '模式2：待设备学习的人员', 'value': {
        'code': 0, 'message': '查询成功', 'data': {
            'face_data': [{'id': 123, 'staff_uuid': '11111111-2222-3333-4444-555555555555',
                           'name': '示例人员', 'head_img_url': 'http://example.invalid/static/upload/face/example.jpg',
                           'feature': '', 'face_token': '',
                           'hardware_uuid': '66666666-7777-8888-9999-000000000000',
                           'status': 1, 'card_id': ''}],
            'face_learning_mode': 2,
        }, 'trace_id': '<本次请求追踪ID>',
    }},
    'businessError': {'summary': '设备不存在', 'value': {
        'code': 1, 'message': '设备不存在', 'data': None, 'trace_id': '<本次请求追踪ID>',
    }},
}

out_path = ROOT / 'raw' / 'apifox_face_userdata_incremental_openapi.json'
out_path.write_text(json.dumps(spec, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'backup': str(before_path), 'incremental': str(out_path),
                  'paths': paths, 'source_path_count': len(exported['paths'])}, ensure_ascii=False))
