#!/usr/bin/env python3
"""校验 + 修复式重试回路（无 key 可跑，纯标准库模拟）。

即使不能使用结构化输出（旧模型 / 第三方平台），生产管线也需要：
  生成 -> 本地 schema 校验 -> 失败则把错误信息拼进 prompt 修复式重问 -> 再校验
本脚本用「假 LLM」模拟模型输出从坏到好的过程，验证回路逻辑正确。

架构要点（模块 05 README 第 2.5 节）：
  - 格式类错误 -> 重试/修复有效
  - 语义类错误 -> 重试无效，应走评估/人审
运行: python3 validate_retry_harness.py
"""

from __future__ import annotations

import json

SCHEMA = {
    "type": "object",
    "properties": {
        "topic": {"type": "string"},
        "severity": {"enum": ["low", "medium", "high", "urgent"]},
    },
    "required": ["topic", "severity"],
    "additionalProperties": False,
}


def validate(obj, schema=SCHEMA, path="$") -> list[str]:
    """极简 JSON-Schema 子集校验器（演示用；生产请用 jsonschema/Pydantic）。"""
    errors = []
    if schema.get("type") == "object":
        if not isinstance(obj, dict):
            return [f"{path}: 应为 object，实为 {type(obj).__name__}"]
        props = schema.get("properties", {})
        for key in schema.get("required", []):
            if key not in obj:
                errors.append(f"{path}.{key}: 缺必填字段")
        if schema.get("additionalProperties") is False:
            for key in obj:
                if key not in props:
                    errors.append(f"{path}.{key}: 多余字段（additionalProperties=false）")
        for key, sub in props.items():
            if key in obj:
                errors += validate(obj[key], sub, f"{path}.{key}")
    elif "enum" in schema:
        # 官方已知行为：enum 匹配应大小写不敏感
        if not any(str(obj).lower() == str(v).lower() for v in schema["enum"]):
            errors.append(f"{path}: '{obj}' 不在枚举 {schema['enum']} 内")
    elif schema.get("type") == "string" and not isinstance(obj, str):
        errors.append(f"{path}: 应为 string")
    return errors


# ---- 假 LLM：模拟三次调用，输出逐次变好（真实场景由你的 LLM 客户端替代） ----
FAKE_LLM_OUTPUTS = [
    '好的，这是您要的工单信息：```json\n{"topic": "prod down", "severity": "!!urgent!!"}\n```',  # 围栏+脏值
    '{"topic": "prod down", "severity": "URGENT", "extra": true}',  # 大小写+多余字段
    '{"topic": "prod down", "severity": "urgent"}',  # 干净
]


def fake_llm(prompt: str, attempt: int) -> str:
    return FAKE_LLM_OUTPUTS[min(attempt, len(FAKE_LLM_OUTPUTS) - 1)]


def strip_code_fence(text: str) -> str:
    """常见修复：剥掉 ```json 围栏。"""
    t = text.strip()
    if t.startswith("```"):
        t = t.split("\n", 1)[1] if "\n" in t else t
        t = t.rsplit("```", 1)[0]
    return t.strip()


def normalize(obj: dict) -> dict:
    """枚举大小写归一（官方文档建议的大小写不敏感处理）。"""
    if "severity" in obj and isinstance(obj["severity"], str):
        obj["severity"] = obj["severity"].strip("! ").lower()
    return obj


def generate_with_repair(max_retries: int = 3) -> tuple[dict, int]:
    prompt = "抽取工单字段，返回 JSON: " + json.dumps(SCHEMA)
    for attempt in range(max_retries + 1):
        raw = fake_llm(prompt, attempt)
        try:
            obj = json.loads(strip_code_fence(raw))
        except json.JSONDecodeError as e:
            prompt += f"\n上一次输出不是合法 JSON（错误: {e}）。只返回 JSON，不要围栏和解释。"
            continue
        obj = normalize(obj)
        errors = validate(obj)
        if not errors:
            return obj, attempt
        # 修复式重问：把校验错误显式告诉模型
        prompt += "\n上一次输出违反 schema: " + "; ".join(errors) + "。请修正后只返回 JSON。"
    raise RuntimeError(f"重试 {max_retries} 次后仍不合法 -> 应升级人审/降级路径")


if __name__ == "__main__":
    ticket, attempts = generate_with_repair()
    print(f"[OK] 第 {attempts + 1} 次尝试通过校验:")
    print(json.dumps(ticket, ensure_ascii=False, indent=2))
    assert ticket == {"topic": "prod down", "severity": "urgent"}
    print("\n回路要点: 格式类错误可修复重试；语义类错误(值本身错)重试无效，走评估/人审。")
