#!/usr/bin/env python3
"""Freeze exact V2 x402 action body schemas from the pinned route handlers.

The historical x402 manifest is intentionally not treated as body-schema
evidence.  A schema is admitted only when the exact POST handler includes its
own discoverable ``outputSchema.input.bodySchema`` declaration.

``--v2-root`` regenerates or verifies source hashes against the pinned V2
checkout.  Offline ``--check`` validates the checked-in binding and schema
shape, so normal CI remains independent of the V2 checkout.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import re
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[2]
REGISTRY = ROOT / "baseline" / "x402-route-registry.json"
V2_HEAD = ROOT / "baseline" / "V2_HEAD_COMMIT.txt"
OUTPUT = ROOT / "baseline" / "schemas" / "v2" / "x402-action-input-schema-evidence.json"
SOURCE_ROOT = Path("repos/theloopbreaker-site")


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def canonical(payload: dict[str, Any]) -> str:
    return json.dumps(payload, indent=2) + "\n"


def action_paths() -> list[str]:
    source = json.loads(REGISTRY.read_text())
    result: list[str] = []
    for route in source["routes"]:
        raw = route.get("raw") or {}
        path = route.get("path")
        if (
            route.get("method") == "POST"
            and isinstance(path, str)
            and path.startswith("/api/x402/actions/")
            and raw.get("inputSchema") is None
        ):
            result.append(path)
    return result


def _extract_braced(text: str, start: int) -> str:
    """Return an object literal beginning at ``start`` (small string-aware scan)."""
    opening = text.find("{", start)
    if opening < 0:
        raise ValueError("expected object literal")
    depth = 0
    quote: str | None = None
    escape = False
    for index in range(opening, len(text)):
        ch = text[index]
        if quote:
            if escape:
                escape = False
            elif ch == "\\":
                escape = True
            elif ch == quote:
                quote = None
            continue
        if ch in {"'", '"', "`"}:
            quote = ch
        elif ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                return text[opening : index + 1]
    raise ValueError("unterminated object literal")


def _quoted_values(text: str) -> list[str]:
    return re.findall(r"['\"]([^'\"]+)['\"]", text)


def extract_body_schema(handler: Path) -> dict[str, Any]:
    """Parse the restricted bodySchema literal used by the canonical handlers.

    This intentionally accepts only the source form used here: a flat object
    of flat property declarations containing ``type`` and optional string
    ``enum``.  Descriptions are not contract material and are omitted so that
    prose quoting cannot change an otherwise exact field contract. It fails
    rather than inferring if the source grows a different shape.
    """
    text = handler.read_text()
    marker = "bodySchema:"
    start = text.find(marker)
    if start < 0:
        raise ValueError(f"no bodySchema in {handler}")
    literal = _extract_braced(text, start + len(marker))

    required_match = re.search(r"required\s*:\s*\[([^\]]*)\]", literal, re.S)
    if not required_match:
        raise ValueError(f"bodySchema has no required array in {handler}")
    required = _quoted_values(required_match.group(1))

    properties_start = literal.find("properties:")
    if properties_start < 0:
        raise ValueError(f"bodySchema has no properties in {handler}")
    properties_literal = _extract_braced(literal, properties_start + len("properties:"))
    property_pattern = re.compile(
        r"(?P<name>[A-Za-z_$][A-Za-z0-9_$]*)\s*:\s*\{\s*"
        r"type\s*:\s*['\"](?P<type>[^'\"]+)['\"]"
        r"(?P<rest>.*?)\s*\}",
        re.S,
    )
    properties: dict[str, dict[str, Any]] = {}
    for match in property_pattern.finditer(properties_literal):
        field: dict[str, Any] = {"type": match.group("type")}
        rest = match.group("rest")
        enum = re.search(r"enum\s*:\s*\[([^\]]*)\]", rest, re.S)
        if enum:
            field["enum"] = _quoted_values(enum.group(1))
        properties[match.group("name")] = field
    if not properties or any(name not in properties for name in required):
        raise ValueError(f"could not exactly recover bodySchema properties from {handler}")
    return {"type": "object", "properties": properties, "required": required}


def handler_path(v2_root: Path, path: str) -> Path:
    return v2_root / SOURCE_ROOT / ("app" + path) / "route.ts"


def build(v2_root: Path) -> dict[str, Any]:
    entries = []
    for path in action_paths():
        handler = handler_path(v2_root, path)
        if not handler.is_file():
            raise ValueError(f"missing canonical V2 action handler for {path}: {handler}")
        entries.append(
            {
                "path": path,
                "method": "POST",
                "source": str(handler.relative_to(v2_root)),
                "sha256": sha256(handler),
                "schema": extract_body_schema(handler),
            }
        )
    return {
        "schemaVersion": "1.0.0",
        "evidencePolicy": (
            "Each schema is the exact discoverable bodySchema declared by the matching "
            "POST handler in the pinned V2 checkout. No fields, requiredness, formats, "
            "or constraints are inferred from route names, adjacent handlers, SDKs, or "
            "live services."
        ),
        "v2Snapshot": {
            "commit": V2_HEAD.read_text().strip(),
            "sourceRoot": str(SOURCE_ROOT),
        },
        "selection": {
            "registry": str(REGISTRY.relative_to(ROOT)),
            "registrySha256": sha256(REGISTRY),
            "pathCount": len(entries),
        },
        "entries": entries,
    }


def validate_offline(payload: dict[str, Any]) -> None:
    if payload.get("schemaVersion") != "1.0.0":
        raise ValueError("unexpected input-schema evidence schemaVersion")
    if payload.get("v2Snapshot", {}).get("commit") != V2_HEAD.read_text().strip():
        raise ValueError("input-schema evidence is bound to the wrong V2 snapshot")
    selection = payload.get("selection", {})
    if selection.get("registrySha256") != sha256(REGISTRY):
        raise ValueError("input-schema evidence registry binding is stale")
    expected_paths = action_paths()
    entries = payload.get("entries")
    if not isinstance(entries, list) or [entry.get("path") for entry in entries] != expected_paths:
        raise ValueError("input-schema evidence does not exactly cover the unresolved V2 action paths")
    if selection.get("pathCount") != len(entries):
        raise ValueError("input-schema evidence selection count is stale")
    for entry in entries:
        if entry.get("method") != "POST":
            raise ValueError(f"unexpected method for {entry.get('path')!r}")
        source, source_hash, schema = entry.get("source"), entry.get("sha256"), entry.get("schema")
        if not isinstance(source, str) or not source.startswith(f"{SOURCE_ROOT}/app/api/x402/actions/"):
            raise ValueError(f"invalid source path for {entry.get('path')!r}")
        if not isinstance(source_hash, str) or not re.fullmatch(r"[0-9a-f]{64}", source_hash):
            raise ValueError(f"invalid source hash for {entry.get('path')!r}")
        if (
            not isinstance(schema, dict)
            or schema.get("type") != "object"
            or not isinstance(schema.get("properties"), dict)
            or not isinstance(schema.get("required"), list)
            or not schema["properties"]
            or any(required not in schema["properties"] for required in schema["required"])
        ):
            raise ValueError(f"invalid body schema for {entry.get('path')!r}")
        for property_schema in schema["properties"].values():
            if not isinstance(property_schema, dict) or property_schema.get("type") not in {"string", "boolean"}:
                raise ValueError(f"unsupported body property schema for {entry.get('path')!r}")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--v2-root", type=Path, help="pinned V2 checkout root containing repos/theloopbreaker-site")
    parser.add_argument("--check", action="store_true", help="verify checked-in evidence")
    args = parser.parse_args()
    if args.v2_root is not None:
        expected = canonical(build(args.v2_root))
        if args.check:
            if not OUTPUT.exists() or OUTPUT.read_text() != expected:
                print(f"V2 input-schema evidence is stale: run {Path(__file__).relative_to(ROOT)} --v2-root <pinned-v2-root>")
                return 1
            print(f"V2 input-schema evidence matches pinned source: {OUTPUT.relative_to(ROOT)}")
            return 0
        OUTPUT.parent.mkdir(parents=True, exist_ok=True)
        OUTPUT.write_text(expected)
        print(f"wrote {OUTPUT.relative_to(ROOT)}")
        return 0
    if not args.check:
        parser.error("--v2-root is required to generate evidence")
    try:
        validate_offline(json.loads(OUTPUT.read_text()))
    except (OSError, ValueError, json.JSONDecodeError) as exc:
        print(f"invalid V2 input-schema evidence: {exc}")
        return 1
    print(f"V2 input-schema evidence is structurally current: {OUTPUT.relative_to(ROOT)}")
    return 0


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