#!/usr/bin/env python3
"""Build reproducible Vaultfire x402 v2/Bazaar discovery artifacts.

The source registry records Vaultfire *contract* protocol versions separately
from the x402 payment rail.  Bazaar candidates always use resource/x402 v2;
a V3 contract route therefore remains ``protocolVersion: v3`` while its
payment and Bazaar resource versions remain v2.

No generated file proves a public endpoint is deployed, returns 402, or has
been indexed.  Those are intentionally checked by validate_bazaar_readiness.py
from owner-provided read-only validation and settlement evidence.
"""
from __future__ import annotations
import copy
import hashlib
import json
import os
import re
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
BASELINE = ROOT / "baseline" / "x402-route-registry.json"
V2_INVENTORY = ROOT / "baseline" / "extracted" / "v2_api_routes.txt"
V2_FIXTURES = ROOT / "baseline" / "schemas" / "v2" / "route-response-fixtures.json"
V2_SOURCE_METHODS = ROOT / "baseline" / "schemas" / "v2" / "route-source-method-evidence.json"
V2_INPUT_SCHEMAS = ROOT / "baseline" / "schemas" / "v2" / "x402-action-input-schema-evidence.json"
V3_SEED = ROOT / "pr7" / "registry" / "routes.ts"
V3_BOND_TYPES = ROOT / "pr7" / "sdk" / "v3" / "types.ts"
V3_BOND_ADAPTER = ROOT / "pr7" / "sdk" / "v3" / "bonds.ts"
V3_PATH_PARAMETER_SCHEMAS = ROOT / "repos" / "theloopbreaker-site" / "app" / "api" / "v3" / "_lib" / "path-parameter-schemas.json"
OUT = Path(os.environ.get("VAULTFIRE_REGISTRY_OUTPUT_DIR", str(ROOT / "pr7" / "generated"))).resolve()
OUT.mkdir(parents=True, exist_ok=True)

BASE_URL = "https://theloopbreaker.com"
BASE_NETWORK = "eip155:8453"
SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
V2_PAYEE = "0xfA15Ee28939B222B0448261A22156070f0A7813C"
X402_VERSION = 2
BAZAAR_RESOURCE_VERSION = "v2"
MAX_TIMEOUT_SECONDS = 60

# V3 source-defined discovery inputs.  These define request shape only and
# deliberately contain no deployment address, payee, or production example.
_HEX_ADDRESS = r"^0x[0-9a-fA-F]{40}$"
_HEX_BYTES32 = r"^0x[0-9a-fA-F]{64}$"
_HEX_PREFIX = r"^0x"
_UINT256_MAX = 2**256 - 1
_UINT96_MAX = 2**96 - 1
_UINT64_MAX = 2**64 - 1
_ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
_ZERO_BYTES32 = "0x" + "0" * 64

# V3 route bodies are bound to the actual SDK adapter signature and its terms
# type. This is deliberately not a V2 shape with a V3 label.
V3_DISCOVERY_INPUT_SCHEMAS = {
    "v3-mandate-lookup": {
        "type": "object",
        "properties": {"mandateHash": {"type": "string", "description": "MandateRegistryV3 bytes32 mandate hash"}},
        "required": ["mandateHash"],
        "additionalProperties": False,
    },
    "v3-create-partnership-bond": {
        "type": "object",
        "properties": {
            "terms": {
                "type": "object",
                "properties": {
                    "bondId": {"type": "integer", "minimum": 0, "maximum": _UINT256_MAX, "example": 1},
                    "partyA": {"type": "string", "pattern": _HEX_ADDRESS, "not": {"const": _ZERO_ADDRESS}},
                    "partyB": {"type": "string", "pattern": _HEX_ADDRESS, "not": {"const": _ZERO_ADDRESS}, "example": "0x0000000000000000000000000000000000000002"},
                    "partyAStake": {"type": "integer", "minimum": 0, "maximum": _UINT256_MAX, "example": 1},
                    "partyBStake": {"type": "integer", "minimum": 0, "maximum": _UINT256_MAX, "example": 2},
                    "stakeAsset": {"type": "string", "pattern": _HEX_ADDRESS},
                    "partnershipType": {"type": "string", "pattern": _HEX_BYTES32, "not": {"const": _ZERO_BYTES32}},
                    "breachConditionsHash": {"type": "string", "pattern": _HEX_BYTES32, "not": {"const": _ZERO_BYTES32}},
                    "flourishRewardConditionsHash": {"type": "string", "pattern": _HEX_BYTES32},
                    "rewardProgramId": {"type": "integer", "minimum": 0, "maximum": _UINT256_MAX},
                    "proposalDeadline": {"type": "integer", "minimum": 0, "maximum": _UINT64_MAX, "example": 4_000_000_000},
                    "activePeriodStart": {"type": "integer", "minimum": 0, "maximum": _UINT64_MAX, "example": 4_000_000_001},
                    "activePeriodEnd": {"type": "integer", "minimum": 0, "maximum": _UINT64_MAX, "example": 4_000_000_002},
                    "challengeWindow": {"type": "integer", "minimum": 0, "maximum": _UINT64_MAX},
                    "nonce": {"type": "integer", "minimum": 0, "maximum": _UINT96_MAX},
                    "chainId": {"type": "integer", "minimum": 0, "maximum": _UINT256_MAX, "example": 8453},
                    "protocolVersion": {"type": "string", "example": "3.0.0"},
                },
                "required": [
                    "bondId", "partyA", "partyB", "partyAStake", "partyBStake", "stakeAsset",
                    "partnershipType", "breachConditionsHash", "flourishRewardConditionsHash",
                    "rewardProgramId", "proposalDeadline", "activePeriodStart", "activePeriodEnd",
                    "challengeWindow", "nonce", "chainId", "protocolVersion",
                ],
                # PartnershipBondV3Safe rejects two zero stakes; it does not
                # impose a JSON-only ordering rule for time fields.
                "anyOf": [
                    {"properties": {"partyAStake": {"not": {"const": 0}}}},
                    {"properties": {"partyBStake": {"not": {"const": 0}}}},
                ],
            },
            "sigA": {"type": "string", "pattern": _HEX_PREFIX},
            "sigB": {"type": "string", "pattern": _HEX_PREFIX},
        },
        "required": ["terms", "sigA", "sigB"],
    },
}


def _atomic_usdc(value):
    """Convert a snapshotted USD price to six-decimal USDC atomic units."""
    if value in (None, "", "free", "0"):
        return None
    text = str(value).removeprefix("$")
    whole, dot, fraction = text.partition(".")
    if not whole.isdigit() or (dot and not fraction.isdigit()) or len(fraction) > 6:
        raise ValueError(f"unsupported V2 price: {value!r}")
    return str(int(whole) * 1_000_000 + int((fraction + "000000")[:6]))


def _generated_at():
    return datetime.fromtimestamp(int(os.environ.get("SOURCE_DATE_EPOCH", "0")), timezone.utc).isoformat()


def _sha256(path: Path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def load_v2_fixture_statuses():
    return {item["path"]: item["fixtureStatus"] for item in json.loads(V2_FIXTURES.read_text())["fixtures"]}


def load_v2_fixtures():
    return {item["path"]: item for item in json.loads(V2_FIXTURES.read_text())["fixtures"]}


def load_v2_source_method_evidence():
    """Load pinned source-handler method evidence without inferring operations."""
    return json.loads(V2_SOURCE_METHODS.read_text())["entries"]


def load_v2_input_schema_evidence():
    """Load only schemas frozen from exact pinned V2 action handlers."""
    payload = json.loads(V2_INPUT_SCHEMAS.read_text())
    return {entry["path"]: entry for entry in payload["entries"]}


def load_v2_inventory_paths():
    return [line.strip() for line in V2_INVENTORY.read_text().splitlines() if line.strip()]


def v2_inventory_alignment(reg):
    """Preserve operational, method-only, and unresolved V2 evidence tiers."""
    inventory = load_v2_inventory_paths()
    fixture_statuses = load_v2_fixture_statuses()
    by_path, operational_by_path, source_method_by_path = {}, {}, {}
    for route in (route for route in reg if route["version"] == "v2"):
        by_path.setdefault(route["path"], []).append(route["id"])
        if route.get("evidenceKind") == "x402-manifest":
            operational_by_path.setdefault(route["path"], []).append(route["id"])
        elif route.get("evidenceKind") == "source-method":
            source_method_by_path.setdefault(route["path"], []).append(route["id"])
    entries = []
    for path in inventory:
        registry_status = "operational" if path in operational_by_path else "method-only" if path in source_method_by_path else "unresolved"
        entries.append({"path": path, "registryStatus": registry_status, "registryRouteIds": by_path.get(path, []), "responseFixtureStatus": fixture_statuses[path]})
    unresolved = [entry["path"] for entry in entries if entry["registryStatus"] == "unresolved"]
    operational = [entry["path"] for entry in entries if entry["registryStatus"] == "operational"]
    method_only = [entry["path"] for entry in entries if entry["registryStatus"] == "method-only"]
    registry_only = sorted(set(by_path) - set(inventory))
    return {
        "schemaVersion": "1.0.0",
        "inventory": {"file": str(V2_INVENTORY.relative_to(ROOT)), "routeCount": len(inventory), "sha256": _sha256(V2_INVENTORY)},
        "registeredRouteCount": len(inventory) - len(unresolved),
        "operationalRouteCount": len(operational), "methodOnlyRouteCount": len(method_only),
        "unresolvedRouteCount": len(unresolved), "unresolvedRoutes": unresolved,
        "registryOnlyRouteCount": len(registry_only), "registryOnlyRoutes": registry_only, "entries": entries,
    }


def _default_accepts(amount):
    return [{
        "scheme": "exact", "network": BASE_NETWORK, "maxAmountRequired": amount,
        "asset": BASE_USDC, "payTo": V2_PAYEE, "maxTimeoutSeconds": MAX_TIMEOUT_SECONDS,
    }]


def _normalize_accepts(options, amount):
    """Preserve source-grounded payment options and normalize only the old Solana alias."""
    if not options:
        return _default_accepts(amount)
    normalized = []
    for option in options:
        network = option.get("network")
        if network == "solana":
            network = SOLANA_MAINNET
        if network not in {BASE_NETWORK, SOLANA_MAINNET}:
            raise ValueError(f"unsupported payment network in baseline: {network!r}")
        result = {
            "scheme": option.get("scheme"), "network": network,
            "maxAmountRequired": option.get("maxAmountRequired") or amount,
            "asset": option.get("asset"), "payTo": option.get("payTo"),
            "maxTimeoutSeconds": option.get("maxTimeoutSeconds") or MAX_TIMEOUT_SECONDS,
        }
        if not all(result.values()):
            raise ValueError(f"incomplete payment option for amount {amount}: {option!r}")
        normalized.append(result)
    return normalized


def load_v2_routes():
    data = json.loads(BASELINE.read_text())
    fixture_statuses = load_v2_fixture_statuses()
    input_schema_evidence = load_v2_input_schema_evidence()
    result = []
    for source in data["routes"]:
        method = (source.get("method") or "GET").upper()
        path = source.get("path") or ""
        amount = _atomic_usdc(source.get("price_atomic"))
        is_free = (not source.get("priced")) or not amount
        raw = source.get("raw") or {}
        source_schema = copy.deepcopy(raw.get("inputSchema"))
        schema_evidence = input_schema_evidence.get(path)
        if source_schema is None and schema_evidence is not None:
            source_schema = copy.deepcopy(schema_evidence["schema"])
            source_schema["x-vaultfire-provenance"] = {
                "path": schema_evidence["source"],
                "sha256": schema_evidence["sha256"],
            }
        route = {
            "id": source.get("id") or f"v2-{method.lower()}-{re.sub(r'[^a-z0-9]+', '-', path.lower()).strip('-')}",
            "method": method, "path": path, "free": is_free, "version": "v2", "contractStatus": "active",
            "finality": "safe", "dataSource": _classify_source(path), "privacy": "public" if is_free else "priced-read",
            "requestSchema": f"v2/{method.lower()}-{re.sub(r'[^a-z0-9]+', '-', path.lower()).strip('-')}.req#",
            "responseSchema": None, "responseFixtureStatus": fixture_statuses.get(path, "unresolved"),
            "evidenceKind": "x402-manifest",
            "idempotencyRule": "key-optional" if method in {"POST", "PUT"} else "none",
            "description": source.get("description") or path,
            # Retain source evidence rather than manufacturing schemas.
            "inputSchema": source_schema,
            "outputSchema": copy.deepcopy(raw.get("outputSchema")),
        }
        if schema_evidence is not None:
            route["inputSchemaEvidence"] = {
                "path": schema_evidence["source"],
                "sha256": schema_evidence["sha256"],
            }
        if not is_free:
            route["price"] = {"amount": amount, "asset": BASE_USDC, "network": BASE_NETWORK, "payee": V2_PAYEE, "scheme": "exact", "x402Version": X402_VERSION}
            route["accepts"] = _normalize_accepts(raw.get("accepts"), amount)
        result.append(route)
    return result


def load_v2_source_method_routes():
    """Build narrow rows from frozen handler exports; never advertise them as x402."""
    fixtures = load_v2_fixtures()
    routes = []
    for evidence in load_v2_source_method_evidence():
        path = evidence["path"]
        fixture = fixtures[path]
        for method in evidence["methods"]:
            slug = re.sub(r"[^a-z0-9]+", "-", path.lower()).strip("-")
            routes.append({
                "id": f"v2-source-{method.lower()}-{slug}", "method": method, "path": path, "free": None,
                "version": "v2", "contractStatus": "unresolved", "finality": "unresolved", "dataSource": "unresolved",
                "privacy": "unresolved", "requestSchema": None, "responseSchema": None,
                "responseFixtureStatus": fixture["fixtureStatus"], "idempotencyRule": "unresolved",
                "description": "Method exported by the pinned V2 route handler; request, response, payment, and operational metadata remain unresolved.",
                "inputSchema": None, "outputSchema": None, "evidenceKind": "source-method",
                "sourceEvidence": {"file": evidence["source"], "sha256": evidence["sha256"]},
            })
    return routes


def _classify_source(path):
    if path.startswith("/api/x402") or path.startswith("/api/agents"):
        return "indexer"
    if path.startswith("/api/contracts"):
        return "onchain"
    return "computed" if path.startswith("/.well-known") or path == "/SKILL.md" else "computed"


def load_v3_routes_from_ts():
    """Parse V3 seed entries; V2 continues to come only from the baseline registry."""
    text = V3_SEED.read_text()
    positions = [m.start() for m in re.finditer(r'(?m)^\s*id:\s*"', text)] + [len(text)]
    entries = []
    for index in range(len(positions) - 1):
        block = text[positions[index]:positions[index + 1]]
        def get(field):
            match = re.search(rf'{field}:\s*"([^\"]+)"', block)
            return match.group(1) if match else None
        def get_bool(field):
            match = re.search(rf'{field}:\s*(true|false)', block)
            return match and match.group(1) == "true"
        route_id, method, path, version, status, description = (get(name) for name in ("id", "method", "path", "version", "contractStatus", "description"))
        if not all((route_id, method, path, version, status)) or version != "v3":
            continue
        free = bool(get_bool("free"))
        route = {
            "id": route_id, "method": method, "path": path, "free": free, "version": version,
            "contractStatus": status, "finality": get("finality"), "dataSource": get("dataSource"),
            "privacy": get("privacy"),
            "requestSchema": get("requestSchema") or f"{version}/{route_id}.req#",
            "responseSchema": get("responseSchema") or f"{version}/{route_id}.res#",
            "responseStatus": get("responseStatus") or "200",
            "responseContracts": {
                status_code: schema_url for status_code, schema_url in
                (pair.split("=", 1) for pair in (get("responseContracts") or "").split(";") if pair)
            },
            "idempotencyRule": get("idempotencyRule"),
            "description": description or path,
            "inputSchema": copy.deepcopy(V3_DISCOVERY_INPUT_SCHEMAS.get(route_id)), "outputSchema": None,
        }
        if route_id == "v3-create-partnership-bond":
            route["inputSchema"]["x-vaultfire-provenance"] = {
                "sources": [
                    {
                        "path": str(V3_BOND_TYPES.relative_to(ROOT)),
                        "sha256": _sha256(V3_BOND_TYPES),
                        "symbol": "V3BondTerms",
                    },
                    {
                        "path": str(V3_BOND_ADAPTER.relative_to(ROOT)),
                        "sha256": _sha256(V3_BOND_ADAPTER),
                        "symbol": "BondsModuleV3.createPartnership",
                    },
                ],
            }
            route["inputSchemaEvidence"] = copy.deepcopy(route["inputSchema"]["x-vaultfire-provenance"])
        required_fields = ("finality", "dataSource", "privacy", "requestSchema", "responseSchema", "idempotencyRule")
        if any(not route[field] for field in required_fields):
            raise ValueError(f"incomplete V3 source registry entry: {route_id}")
        entries.append(route)
    return entries


def _example_for_schema(schema, depth=0):
    """Create a deterministic non-sensitive example solely from a declared schema."""
    if depth > 5 or not isinstance(schema, dict):
        return "example"
    for field in ("const", "default", "example"):
        if field in schema:
            return copy.deepcopy(schema[field])
    if schema.get("examples"):
        return copy.deepcopy(schema["examples"][0])
    if schema.get("enum"):
        return copy.deepcopy(schema["enum"][0])
    schema_type = schema.get("type")
    if schema_type in {"number", "integer"}:
        minimum = schema.get("minimum")
        value = int(minimum) if isinstance(minimum, (int, float)) else 1
        forbidden = schema.get("not", {}).get("const")
        return value + 1 if value == forbidden else value
    if schema_type == "string":
        pattern = schema.get("pattern")
        if pattern == _HEX_ADDRESS:
            value = "0x0000000000000000000000000000000000000001"
        elif pattern == _HEX_BYTES32:
            value = "0x" + "1" * 64
        elif pattern == _HEX_PREFIX:
            value = "0x00"
        else:
            value = "example"
        forbidden = schema.get("not", {}).get("const")
        return value if value != forbidden else "example"
    if schema_type == "array":
        return [_example_for_schema(schema.get("items", {}), depth + 1)]
    if schema_type == "object" or "properties" in schema:
        properties = schema.get("properties", {})
        required = schema.get("required", list(properties))
        return {name: _example_for_schema(properties.get(name, {}), depth + 1) for name in required}
    if schema_type == "boolean":
        return True
    return "example"


def _input_schema_for(route):
    """A GET with no documented parameters is explicitly input-free; writes are not guessed."""
    schema = route.get("inputSchema")
    if schema:
        return schema
    if route["method"] == "GET" and ":" not in route["path"]:
        return {"type": "object", "properties": {}, "additionalProperties": False}
    return None


def _solana_payment_status(route):
    if route.get("free") is None:
        return "method-only-no-payment-claim"
    if route.get("free") is True:
        return "not-priced"
    solana_options = [option for option in route.get("accepts", []) if option.get("network") == SOLANA_MAINNET]
    if not solana_options:
        return "read-only-api-no-solana-settlement"
    for option in solana_options:
        # Solana USDC mint and payTo must be concrete source-backed base58 values;
        # an empty/default value must never turn a Base option into a Solana claim.
        if not isinstance(option.get("asset"), str) or not re.fullmatch(r"[1-9A-HJ-NP-Za-km-z]{32,44}", option["asset"]):
            return "blocked-solana-payment-config-invalid"
        if not isinstance(option.get("payTo"), str) or not re.fullmatch(r"[1-9A-HJ-NP-Za-km-z]{32,44}", option["payTo"]):
            return "blocked-solana-payment-config-invalid"
    return "metadata-configured-unverified"


def _bazaar_eligibility(route):
    if route["free"] is None:
        return "method-only"
    if route["free"] is True:
        return "not-priced"
    if route["method"] not in {"GET", "POST"}:
        return "blocked-unsupported-method"
    if not _input_schema_for(route):
        return "blocked-input-schema-unresolved"
    return "candidate"


def _bazaar_extension(route):
    schema = _input_schema_for(route)
    if schema is None:
        raise ValueError(f"cannot build Bazaar extension without input schema: {route['id']}")
    example = _example_for_schema(schema)
    input_info = {"type": "http", "method": route["method"]}
    input_schema = {
        "type": "object",
        "properties": {"type": {"const": "http"}, "method": {"const": route["method"]}},
        "required": ["type", "method"],
        "additionalProperties": False,
    }
    if route["method"] == "GET":
        input_key = "pathParams" if ":" in route["path"] else "queryParams"
        input_info[input_key] = example
        input_schema["properties"][input_key] = schema
        input_schema["required"].append(input_key)
    else:
        input_info["bodyType"] = "json"
        input_info["body"] = example
        input_schema["properties"].update({"bodyType": {"const": "json"}, "body": schema})
        input_schema["required"].extend(["bodyType", "body"])
    output_example = _example_for_schema(route.get("outputSchema") or {"type": "object", "properties": {"ok": {"type": "boolean"}}, "required": ["ok"]})
    return {
        "info": {"method": route["method"], "path": route["path"], "input": input_info, "output": {"example": output_example}},
        "schema": {
            "type": "object",
            "properties": {
                "method": {"const": route["method"]}, "path": {"const": route["path"]},
                "input": input_schema,
                "output": {"type": "object", "properties": {"example": {}}, "required": ["example"], "additionalProperties": False},
            },
            "required": ["method", "path", "input", "output"], "additionalProperties": False,
        },
    }


def _resource(route):
    return {
        "resource": f"{BASE_URL}{route['path']}", "method": route["method"], "routeTemplate": route["path"],
        "description": route["description"][:500], "mimeType": "application/json",
        "resourceVersion": BAZAAR_RESOURCE_VERSION,
        "protocolVersion": route["version"], "contractStatus": route["contractStatus"],
        "x402Version": X402_VERSION, "solanaPaymentStatus": route["solanaPaymentStatus"], "accepts": route["accepts"],
        "extensions": {"bazaar": _bazaar_extension(route)},
    }


def emit(reg):
    now = _generated_at()
    alignment = v2_inventory_alignment(reg)
    for route in reg:
        route["solanaPaymentStatus"] = _solana_payment_status(route)
        route["bazaarEligibility"] = _bazaar_eligibility(route)
    candidates = [route for route in reg if route["bazaarEligibility"] == "candidate"]
    blocked = [{"id": route["id"], "method": route["method"], "path": route["path"], "reason": route["bazaarEligibility"]} for route in reg if route["bazaarEligibility"].startswith("blocked-")]

    (OUT / "route-registry.json").write_text(json.dumps({"generatedAt": now, "count": len(reg), "routes": reg, "v2InventoryAlignment": alignment}, indent=2))
    (OUT / "v2-inventory-alignment.json").write_text(json.dumps(alignment, indent=2))
    resources = [_resource(route) for route in candidates]
    common = {
        "generatedAt": now, "x402Version": X402_VERSION, "bazaarResourceVersion": BAZAAR_RESOURCE_VERSION,
        "resourceServer": BASE_URL, "facilitator": "https://api.cdp.coinbase.com/platform/v2/x402",
        "protocol": "vaultfire", "protocolVersions": ["v2", "v3"],
        "publicationState": "metadata-candidates-not-live-validated",
    }
    (OUT / "well-known-x402.json").write_text(json.dumps({**common, "endpoints": resources}, indent=2))
    (OUT / "coinbase-bazaar-v2.json").write_text(json.dumps({**common, "resources": resources}, indent=2))
    v3_publication_gate = {
        "schema": "vaultfire.v3.x402.publication-gate.v1",
        "schemaVersion": "1.0.0",
        "candidateCount": sum(1 for route in candidates if route["version"] == "v3"),
        "publicationEligible": False,
        "listingReceiptIds": [],
        "deploymentEvidenceIds": [],
        "requiredEvidence": ["objective-listing-receipt-id", "verified-v3-deployment-evidence"],
        "claim": "no-v3-bazaar-listing-or-settlement",
    }
    (OUT / "v3-bazaar-publication-gate.json").write_text(json.dumps({
        "generatedAt": now, **v3_publication_gate,
    }, indent=2))
    (OUT / "bazaar-readiness.json").write_text(json.dumps({
        **common, "candidateCount": len(candidates), "blockedCount": len(blocked), "blockedRoutes": blocked,
        "requiredLiveEvidence": ["read-only-validate", "successful-settled-payment-with-resource", "extension-response-not-rejected"],
        "indexingClaim": "none; validation does not index and no settlement evidence is committed",
        "v3PublicationGate": v3_publication_gate,
    }, indent=2))

    path_parameter_schemas = json.loads(V3_PATH_PARAMETER_SCHEMAS.read_text())
    paths_obj = {}
    for route in reg:
        if route.get("evidenceKind") == "source-method":
            # Method evidence is not enough to publish an OpenAPI/x402 operation.
            continue
        response = {"description": "Response" if route["responseSchema"] else "Response schema unresolved; a route-specific recorded response is required before this may be typed."}
        if route["responseSchema"]:
            response["content"] = {"application/json": {"schema": {"$ref": route["responseSchema"]}}}
        if route["version"] == "v3":
            response_contracts = route.get("responseContracts") or {route.get("responseStatus", "200"): route["responseSchema"]}
            responses = {}
            for status_code, schema_urls in sorted(response_contracts.items()):
                refs = schema_urls.split("|")
                schema_contract = {"$ref": refs[0]} if len(refs) == 1 else {"oneOf": [{"$ref": ref} for ref in refs]}
                responses[status_code] = {"description": "V3 typed response", "content": {"application/json": {"schema": schema_contract}}}
        else:
            responses = {"200": response, "402": {"description": "Payment Required (x402 v2)"}, "503": {"description": "Data source unavailable"}}
        operation = {
            "operationId": route["id"], "summary": route["description"], "x-protocol-version": route["version"],
            "x-contract-status": route["contractStatus"], "x-finality": route["finality"], "x-data-source": route["dataSource"],
            "x-privacy": route["privacy"], "x-idempotency": route["idempotencyRule"],
            "x-response-fixture-status": route.get("responseFixtureStatus", "not-applicable"),
            "x-bazaar-eligibility": route["bazaarEligibility"], "x-solana-payment-status": route["solanaPaymentStatus"],
            "responses": responses,
        }
        if route["version"] == "v3":
            parameters = []
            for parameter_name in re.findall(r":([A-Za-z][A-Za-z0-9_]*)", route["path"]):
                parameter_schema = path_parameter_schemas.get(parameter_name)
                if parameter_schema is None:
                    raise ValueError(f"missing canonical V3 path parameter schema: {parameter_name}")
                parameters.append({"name": parameter_name, "in": "path", "required": True, "schema": copy.deepcopy(parameter_schema)})
            if route["id"] == "bond-party":
                parameters.append({"name": "party", "in": "query", "required": True, "schema": {"type": "string", "pattern": "^0x[0-9a-f]{40}$"}})
            if route["id"] == "pull-credit":
                parameters.extend({"name": name, "in": "query", "required": True, "schema": {"type": "string", "pattern": "^0x[0-9a-f]{40}$"}} for name in ("contract", "asset", "holder"))
            if parameters:
                operation["parameters"] = parameters
        if route.get("free") is False:
            operation["x-price"] = route["price"]
            operation["x-accepts"] = route["accepts"]
        if route["method"] != "GET":
            operation["requestBody"] = {"content": {"application/json": {"schema": route["inputSchema"] or {}}}}
        openapi_path = re.sub(r":([A-Za-z][A-Za-z0-9_]*)", r"{\1}", route["path"])
        paths_obj.setdefault(openapi_path, {})[route["method"].lower()] = operation
    (OUT / "openapi.json").write_text(json.dumps({
        "openapi": "3.1.0",
        "info": {"title": "Vaultfire API (V2 + V3)", "version": "3.0.0", "description": "Generated from pr7/registry. V2 preserved; V3 added under /api/v3.",
                 "x-v2-inventory-alignment": {"inventoryRouteCount": alignment["inventory"]["routeCount"], "registeredRouteCount": alignment["registeredRouteCount"], "unresolvedRouteCount": alignment["unresolvedRouteCount"]}},
        "paths": paths_obj,
    }, indent=2))
    counts = {"total": len(reg), "byVersion": {}, "byMethod": {}, "pricedByVersion": {}, "freeByVersion": {}, "bazaar": {"candidates": len(candidates), "blocked": len(blocked)}, "v2InventoryAlignment": {"inventoryRouteCount": alignment["inventory"]["routeCount"], "registeredRouteCount": alignment["registeredRouteCount"], "unresolvedRouteCount": alignment["unresolvedRouteCount"]}}
    for route in reg:
        counts["byVersion"][route["version"]] = counts["byVersion"].get(route["version"], 0) + 1
        counts["byMethod"][route["method"]] = counts["byMethod"].get(route["method"], 0) + 1
        if route["free"] is True:
            counts["freeByVersion"][route["version"]] = counts["freeByVersion"].get(route["version"], 0) + 1
        elif route["free"] is False:
            counts["pricedByVersion"][route["version"]] = counts["pricedByVersion"].get(route["version"], 0) + 1
        else:
            counts.setdefault("methodOnlyByVersion", {})
            counts["methodOnlyByVersion"][route["version"]] = counts["methodOnlyByVersion"].get(route["version"], 0) + 1
    (OUT / "endpoint-counts.json").write_text(json.dumps(counts, indent=2))
    (OUT / "cross-chain-matrix.md").write_text(_cross_chain_matrix(reg, candidates, blocked))
    return counts


def _cross_chain_matrix(reg, candidates, blocked):
    solana_resources = sum(1 for route in candidates if any(option["network"] == SOLANA_MAINNET for option in route["accepts"]))
    return "\n".join([
        "# Vaultfire — Cross-chain Capability Matrix", "",
        "This is a metadata/readiness matrix, not a deployment or indexing claim.", "",
        "| Chain | EVM contracts | x402 v2 payment rail | Bazaar candidate resources | V3 contract protocol |",
        "|---|---|---|---|---|",
        "| Base (8453) | V2 addresses are historical baseline evidence; no new address is asserted here. | Yes — `eip155:8453` | Yes — metadata candidates only; live 402/preflight/settlement still required. | Base V3 routes retain `protocolVersion: v3`; their x402 rail remains v2. |",
        f"| Solana mainnet | No EVM contracts. | Exact settlement only for {solana_resources} candidates with canonical source-backed Solana asset + payTo; all other routes are read-only/API-on-Solana with no Solana settlement claim. | Yes — {solana_resources} candidates; no Solana V3 contract claim. | Not applicable. |",
        "", f"Registry: {len(reg)} routes; {sum(route.get('free') is False for route in reg)} priced; {sum(route.get('free') is True for route in reg)} free; {sum(route.get('free') is None for route in reg)} method-only; {len(candidates)} Bazaar metadata candidates; {len(blocked)} blocked for missing input metadata.",
        "", "**Non-goal:** generated metadata does not prove an endpoint is public, returns 402, has passed CDP preflight, has settled a payment, or is indexed.", "",
    ])


def main():
    counts = emit(load_v2_routes() + load_v2_source_method_routes() + load_v3_routes_from_ts())
    print(json.dumps({"generated": str(OUT), "counts": counts}, indent=2))


if __name__ == "__main__":
    main()
