#!/usr/bin/env python3
"""校验标签输出格式是否合法。

这个脚本只做“格式、存在性、引用关系”的确定性校验，不判断 query 语义。
适合在 Agent 给出 target 后、写入训练/评测数据前调用。
"""

from __future__ import annotations

import argparse
import ast
import json
import re
import sys
from pathlib import Path
from typing import Any

try:
    from build_label_manifest import DEFAULT_OUTPUT, SKILL_ROOT, build_manifest
except ImportError:  # pragma: no cover - 兼容从其他 cwd 直接执行
    sys.path.insert(0, str(Path(__file__).resolve().parent))
    from build_label_manifest import DEFAULT_OUTPUT, SKILL_ROOT, build_manifest


AGENT_RE = re.compile(r'^Agent\(\s*tag\s*=\s*["\'](.+?)["\']\s*\)$')
ASSIGNMENT_RE = re.compile(r"^(x\d+)\s*=\s*(.+)$")


def normalize_quotes(text: str) -> str:
    return (
        text.replace("“", '"')
        .replace("”", '"')
        .replace("‘", "'")
        .replace("’", "'")
        .strip()
    )


def load_manifest(path: Path) -> dict[str, Any]:
    if path.exists():
        return json.loads(path.read_text(encoding="utf-8"))
    return build_manifest()


def names_by_key(items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
    return {item["name"]: item for item in items if item.get("name")}


class LabelOutputValidator:
    def __init__(self, manifest: dict[str, Any]):
        self.manifest = manifest
        self.agent_tags = set(manifest.get("agent_tags", []))
        self.functions = names_by_key(manifest.get("functions", []))
        self.objects = names_by_key(manifest.get("objects", []))
        self.intents = names_by_key(manifest.get("intents", []))

    def validate(self, raw_target: str) -> dict[str, Any]:
        target = normalize_quotes(raw_target)
        result: dict[str, Any] = {
            "valid": True,
            "detected_type": "unknown",
            "normalized_output": target,
            "errors": [],
            "warnings": [],
            "references": [],
        }

        if not target:
            self.error(result, "输出为空")
            return result

        dimensions, target_body = self.split_dimension_prefix(target)
        if dimensions:
            result["dimensions"] = dimensions
        target_body = target_body.strip()

        if target_body.startswith("["):
            self.validate_group_json(target_body, result)
        elif AGENT_RE.match(target_body):
            self.validate_agent_target(target_body, result)
        elif self.looks_like_call_program(target_body):
            self.validate_call_program(target_body, result)
        elif target_body in self.intents:
            result["detected_type"] = "intent"
            result["references"].append({"type": "intent", "name": target_body})
        elif target_body in self.agent_tags:
            result["detected_type"] = "bare_label"
            result["warnings"].append("只输出了标签名；如果当前任务需要可消费 target，建议明确 intent、function 或 Agent 包装形式")
            result["references"].append({"type": "label", "name": target_body})
        else:
            self.error(result, f"无法识别的输出形式：{target_body}")

        if dimensions:
            result["normalized_output"] = "\n".join([*dimensions, result["normalized_output"]])
        result["valid"] = not result["errors"]
        return result

    def split_dimension_prefix(self, target: str) -> tuple[list[str], str]:
        dimensions: list[str] = []
        lines = [line.strip() for line in target.splitlines() if line.strip()]
        while lines and re.fullmatch(r"(complex|multi_instruction|auto_task)\s*=\s*(true|false)", lines[0], re.I):
            key, value = [part.strip().lower() for part in lines.pop(0).split("=", 1)]
            dimensions.append(f"{key}={value}")
        return dimensions, "\n".join(lines)

    def validate_agent_target(self, target: str, result: dict[str, Any]) -> None:
        result["detected_type"] = "agent"
        match = AGENT_RE.match(target)
        assert match is not None
        tag = match.group(1)
        result["normalized_output"] = f'Agent(tag="{tag}")'
        if tag not in self.agent_tags:
            self.error(result, f'Agent tag 不在标签知识库中：{tag}')
            return
        result["references"].append({"type": "agent_tag", "name": tag})

    def looks_like_call_program(self, target: str) -> bool:
        lines = [line.strip() for line in target.splitlines() if line.strip()]
        return bool(lines) and all("(" in line and line.endswith(")") for line in lines)

    def validate_call_program(self, target: str, result: dict[str, Any]) -> None:
        result["detected_type"] = "function_program"
        variables: dict[str, str] = {}
        lines = [line.strip() for line in target.splitlines() if line.strip()]

        for index, line in enumerate(lines):
            assignment = ASSIGNMENT_RE.match(line)
            if assignment:
                var_name, expr = assignment.group(1), assignment.group(2).strip()
                call_info = self.parse_call(expr, result, line)
                if not call_info:
                    continue
                object_name = call_info["name"]
                if object_name not in self.objects:
                    self.error(result, f"变量 {var_name} 只能绑定 object，但 {object_name} 不在对象目录中")
                    continue
                self.validate_call_kwargs(call_info, self.objects[object_name], result)
                variables[var_name] = object_name
                result["references"].append({"type": "object", "name": object_name, "variable": var_name})
                continue

            call_info = self.parse_call(line, result, line)
            if not call_info:
                continue
            function_name = call_info["name"]
            if function_name not in self.functions:
                self.error(result, f"最终调用必须是已定义 function，但 {function_name} 不在函数目录中")
                continue
            self.validate_call_kwargs(call_info, self.functions[function_name], result)
            self.validate_variable_refs(call_info, variables, result)
            result["references"].append({"type": "function", "name": function_name})

            if index != len(lines) - 1:
                self.error(result, f"非最后一行不能直接调用 function：{line}")

    def parse_call(self, expr: str, result: dict[str, Any], raw_line: str) -> dict[str, Any] | None:
        try:
            node = ast.parse(expr, mode="eval").body
        except SyntaxError as exc:
            self.error(result, f"调用语法无法解析：{raw_line} ({exc.msg})")
            return None
        if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name):
            self.error(result, f"不是合法的函数调用：{raw_line}")
            return None
        return {
            "name": node.func.id,
            "keywords": node.keywords,
            "positional_count": len(node.args),
            "raw": raw_line,
        }

    def validate_call_kwargs(self, call_info: dict[str, Any], spec: dict[str, Any], result: dict[str, Any]) -> None:
        if call_info["positional_count"]:
            self.error(result, f"暂不支持位置参数，请使用命名参数：{call_info['raw']}")

        params = spec.get("params", {})
        allow_unknown = bool(spec.get("allow_unknown_params"))
        for keyword in call_info["keywords"]:
            if keyword.arg is None:
                self.error(result, f"暂不支持 **kwargs：{call_info['raw']}")
                continue
            if not allow_unknown and keyword.arg not in params:
                self.error(result, f"{call_info['name']} 未定义参数：{keyword.arg}")
                continue
            enum_values = params.get(keyword.arg, {}).get("enum_values", [])
            if enum_values:
                self.validate_enum(keyword, enum_values, call_info, result)

    def validate_enum(self, keyword: ast.keyword, enum_values: list[str], call_info: dict[str, Any], result: dict[str, Any]) -> None:
        values = self.literal_string_values(keyword.value)
        for value in values:
            if value not in enum_values:
                self.error(result, f"{call_info['name']}.{keyword.arg} 枚举值不合法：{value}，可选：{', '.join(enum_values)}")

    def literal_string_values(self, node: ast.AST) -> list[str]:
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            return [node.value]
        if isinstance(node, ast.List):
            values: list[str] = []
            for item in node.elts:
                values.extend(self.literal_string_values(item))
            return values
        return []

    def validate_variable_refs(self, call_info: dict[str, Any], variables: dict[str, str], result: dict[str, Any]) -> None:
        referenced = sorted({node.id for keyword in call_info["keywords"] for node in ast.walk(keyword.value) if isinstance(node, ast.Name)})
        for name in referenced:
            if name not in variables:
                self.error(result, f"引用了未定义 object 变量：{name}")

    def validate_group_json(self, target: str, result: dict[str, Any]) -> None:
        result["detected_type"] = "group_json"
        try:
            groups = json.loads(target)
        except json.JSONDecodeError as exc:
            self.error(result, f"多指令/自动任务 JSON 解析失败：{exc.msg}")
            return
        if not isinstance(groups, list):
            self.error(result, "多指令/自动任务输出必须是 JSON list")
            return

        for group_index, group in enumerate(groups):
            if not isinstance(group, dict):
                self.error(result, f"第 {group_index} 个 group 不是 object")
                continue
            if "condition" not in group:
                self.error(result, f"第 {group_index} 个 group 缺少 condition")
            querys = group.get("querys")
            if not isinstance(querys, list):
                self.error(result, f"第 {group_index} 个 group 的 querys 必须是 list")
                continue
            for query_index, item in enumerate(querys):
                self.validate_group_item(item, group_index, query_index, result)

    def validate_group_item(self, item: Any, group_index: int, query_index: int, result: dict[str, Any]) -> None:
        prefix = f"第 {group_index} 个 group 的第 {query_index} 条 query"
        if not isinstance(item, dict):
            self.error(result, f"{prefix} 不是 object")
            return
        if not item.get("subquery"):
            self.error(result, f"{prefix} 缺少 subquery")

        value_fields = [field for field in ("function", "intent", "target", "label") if item.get(field)]
        if not value_fields:
            self.error(result, f"{prefix} 缺少 function、intent、target 或 label")
            return
        if len(value_fields) > 1:
            self.error(result, f"{prefix} 同时包含多个输出字段：{', '.join(value_fields)}")
            return

        field = value_fields[0]
        value = str(item[field])
        if field == "function":
            nested = self.validate(value)
            self.merge_nested(result, nested, prefix)
        elif field == "intent":
            if value not in self.intents and value not in self.agent_tags:
                self.error(result, f"{prefix} 的 intent 不在知识库中：{value}")
            else:
                result["references"].append({"type": "intent", "name": value})
        else:
            nested = self.validate(value)
            self.merge_nested(result, nested, prefix)

    def merge_nested(self, result: dict[str, Any], nested: dict[str, Any], prefix: str) -> None:
        for error in nested.get("errors", []):
            self.error(result, f"{prefix}: {error}")
        for warning in nested.get("warnings", []):
            result["warnings"].append(f"{prefix}: {warning}")
        result["references"].extend(nested.get("references", []))

    def error(self, result: dict[str, Any], message: str) -> None:
        result["errors"].append(message)


def load_targets_from_file(path: Path, field: str) -> list[str]:
    text = path.read_text(encoding="utf-8").strip()
    if not text:
        return []
    if path.suffix == ".jsonl":
        targets: list[str] = []
        for line_no, line in enumerate(text.splitlines(), start=1):
            if not line.strip():
                continue
            item = json.loads(line)
            if field not in item:
                raise ValueError(f"{path}:{line_no} 缺少字段 {field}")
            targets.append(str(item[field]))
        return targets

    data = json.loads(text)
    if isinstance(data, dict):
        if field in data:
            return [str(data[field])]
        if "records" in data and isinstance(data["records"], list):
            return [str(item[field]) for item in data["records"] if isinstance(item, dict) and field in item]
    if isinstance(data, list):
        return [str(item[field]) for item in data if isinstance(item, dict) and field in item]
    raise ValueError(f"无法从 {path} 读取字段 {field}")


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="校验标签输出格式")
    parser.add_argument("--target", help="直接校验一个 target 字符串")
    parser.add_argument("--file", help="从 JSON/JSONL 文件中批量读取 target")
    parser.add_argument("--field", default="target", help="批量文件中的字段名，默认 target")
    parser.add_argument("--manifest", default=str(DEFAULT_OUTPUT), help="manifest JSON 路径")
    args = parser.parse_args(argv)

    if not args.target and not args.file:
        parser.error("必须提供 --target 或 --file")

    manifest_path = Path(args.manifest)
    if not manifest_path.is_absolute():
        manifest_path = SKILL_ROOT / manifest_path
    validator = LabelOutputValidator(load_manifest(manifest_path))

    targets = [args.target] if args.target else load_targets_from_file(Path(args.file), args.field)
    results = [validator.validate(target or "") for target in targets]
    payload: dict[str, Any]
    if len(results) == 1:
        payload = results[0]
    else:
        payload = {
            "valid": all(item["valid"] for item in results),
            "total": len(results),
            "invalid": sum(1 for item in results if not item["valid"]),
            "results": results,
        }

    print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
    return 0 if payload["valid"] else 1


if __name__ == "__main__":
    raise SystemExit(main())
