#!/usr/bin/env python3
"""Build or verify the evidence-bound V2 response compatibility fixture set.

This builder deliberately does not infer a route response schema from a route
name, a related endpoint, an OpenAPI document, or an x402 manifest.  A route is
``exact`` only when a recorded response is tied to that exact inventoried path.
All other inventory entries are emitted as ``unresolved`` so CI makes missing
capture work visible without freezing invented consumer contracts.
"""
from __future__ import annotations

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

ROOT = Path(__file__).resolve().parents[2]
INVENTORY = ROOT / "baseline" / "extracted" / "v2_api_routes.txt"
OUTPUT = ROOT / "baseline" / "schemas" / "v2" / "route-response-fixtures.json"

# Evidence is intentionally a small, explicit allow-list.  The well-known
# captures in baseline are retained as unmatched evidence below because their
# captured paths are not present in the V2 API inventory.
EXACT_RESPONSE_SNAPSHOTS = {
    "/api/contracts": {
        "method": "GET",
        "file": ROOT / "baseline" / "raw_api_contracts.txt",
        "capturedPath": "/api/contracts",
    },
}
UNMATCHED_SNAPSHOTS = [
    {
        "file": "baseline/raw__well-known_agent-card_json.txt",
        "capturedPath": "/.well-known/agent-card.json",
        "reason": "captured path is absent from baseline/extracted/v2_api_routes.txt; not used to infer /api/.well-known/agent-card.json",
    },
    {
        "file": "baseline/raw__well-known_x402_json.txt",
        "capturedPath": "/.well-known/x402.json",
        "reason": "captured path is absent from baseline/extracted/v2_api_routes.txt; route manifest is not evidence of an individual endpoint response schema",
    },
]


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


def schema_for(value: Any) -> dict[str, Any]:
    """Infer a minimal JSON Schema from one exact recorded response.

    Every observed object key is required to preserve existing consumers.
    ``additionalProperties: true`` permits additive fields, which must remain
    optional for old readers.  No enums, formats, or guessed patterns are
    introduced because a single capture cannot prove them.
    """
    if value is None:
        return {"type": "null"}
    if isinstance(value, bool):
        return {"type": "boolean"}
    if isinstance(value, int):
        return {"type": "integer"}
    if isinstance(value, float):
        return {"type": "number"}
    if isinstance(value, str):
        return {"type": "string"}
    if isinstance(value, list):
        if not value:
            return {"type": "array", "items": {}}
        item_schemas = [schema_for(item) for item in value]
        unique: list[dict[str, Any]] = []
        for item_schema in item_schemas:
            if item_schema not in unique:
                unique.append(item_schema)
        return {
            "type": "array",
            "items": unique[0] if len(unique) == 1 else {"anyOf": unique},
        }
    if isinstance(value, dict):
        return {
            "type": "object",
            "properties": {key: schema_for(item) for key, item in value.items()},
            "required": list(value.keys()),
            "additionalProperties": True,
        }
    raise TypeError(f"unsupported JSON value: {type(value)!r}")


def build() -> dict[str, Any]:
    routes = [line.strip() for line in INVENTORY.read_text().splitlines() if line.strip()]
    fixtures: list[dict[str, Any]] = []
    for route in routes:
        source = EXACT_RESPONSE_SNAPSHOTS.get(route)
        if source is None:
            fixtures.append({
                "path": route,
                "method": None,
                "fixtureStatus": "unresolved",
                "reason": "no route-specific recorded response snapshot",
                "responseSchema": None,
            })
            continue
        snapshot_file: Path = source["file"]
        snapshot = json.loads(snapshot_file.read_text())
        fixtures.append({
            "path": route,
            "method": source["method"],
            "fixtureStatus": "exact",
            "evidence": {
                "file": str(snapshot_file.relative_to(ROOT)),
                "capturedPath": source["capturedPath"],
                "sha256": sha256(snapshot_file),
            },
            "responseSchema": {
                "$schema": "https://json-schema.org/draft/2020-12/schema",
                "$id": f"https://vaultfire.example/compat/v2{route}.response.schema.json",
                "title": f"Recorded V2 response: {source['method']} {route}",
                **schema_for(snapshot),
            },
        })
    return {
        "schemaVersion": "1.0.0",
        "inventory": {
            "file": str(INVENTORY.relative_to(ROOT)),
            "routeCount": len(routes),
            "sha256": sha256(INVENTORY),
        },
        "fixturePolicy": {
            "exact": "A route-specific recorded response exists; observed fields are required and additive fields are permitted.",
            "unresolved": "No route-specific response capture exists. No response schema is asserted until a live capture is recorded.",
        },
        "fixtures": fixtures,
        "unmatchedEvidence": UNMATCHED_SNAPSHOTS,
    }


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


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--check", action="store_true", help="fail when checked-in fixture output is stale")
    args = parser.parse_args()
    expected = canonical(build())
    if args.check:
        if not OUTPUT.exists() or OUTPUT.read_text() != expected:
            print(f"V2 fixture output is stale: run {Path(__file__).relative_to(ROOT)}")
            return 1
        print(f"V2 fixture output is current: {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 __name__ == "__main__":
    raise SystemExit(main())
