#!/usr/bin/env python3
"""Build the evidence-bound V2-to-V3 compatibility/discovery inventory.

The output deliberately records unknowns as null or explicit unresolved states.
It is not a claim that checked-in metadata is live, settled, or Bazaar-indexed.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[2]
OUTPUT = ROOT / "pr7" / "compat" / "compatibility-discovery-v2-v3.json"
SUMMARY = ROOT / "pr7" / "compat" / "V2_V3_COMPATIBILITY_MATRIX.md"
DEPLOYMENTS = ROOT / "baseline" / "deployments.json"
ROUTE_REGISTRY = ROOT / "pr7" / "generated" / "route-registry.json"
V2_ROUTES = ROOT / "baseline" / "extracted" / "v2_api_routes.txt"
FIXTURES = ROOT / "baseline" / "schemas" / "v2" / "route-response-fixtures.json"
ABI_MANIFEST = ROOT / "legacy" / "v2" / "manifest.json"
BAZAAR = ROOT / "pr7" / "generated" / "coinbase-bazaar-v2.json"
WELL_KNOWN = ROOT / "pr7" / "generated" / "well-known-x402.json"
V2_SDK = ROOT / "repos" / "vaultfire-sdk"
V2_X402_SDK = ROOT / "repos" / "vaultfire-x402"
V2_PAYMENT_GATE = ROOT / "repos" / "theloopbreaker-site" / "app" / "api" / "x402" / "trust" / "_lib" / "verify-payment.ts"


def canonical(value: Any) -> str:
    return json.dumps(value, indent=2, sort_keys=True) + "\n"


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


def source_ref(path: Path) -> dict[str, str]:
    return {"path": path.relative_to(ROOT).as_posix(), "sha256": sha256(path)}


def load(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def extract_sdk_surface(root: Path) -> list[dict[str, Any]]:
    """Inventory public TypeScript declarations without inventing signatures."""
    declarations: list[dict[str, Any]] = []
    class_pattern = re.compile(r"^export class ([A-Za-z_$][\w$]*)", re.MULTILINE)
    function_pattern = re.compile(r"^export (?:async )?function ([A-Za-z_$][\w$]*)\(", re.MULTILINE)
    method_pattern = re.compile(r"^  (?:async )?([A-Za-z_$][\w$]*)\(", re.MULTILINE)
    for path in sorted((root / "src").rglob("*.ts")):
        text = path.read_text(encoding="utf-8")
        classes = []
        for match in class_pattern.finditer(text):
            body = text[match.end():]
            methods = []
            for method in method_pattern.finditer(body):
                if method.group(1) == "constructor":
                    continue
                methods.append(method.group(1))
            classes.append({"name": match.group(1), "methods": sorted(set(methods))})
        functions = sorted(set(match.group(1) for match in function_pattern.finditer(text)))
        if classes or functions:
            declarations.append({
                "source": source_ref(path),
                "classes": classes,
                "functions": functions,
            })
    return declarations


def build() -> dict[str, Any]:
    deployments = load(DEPLOYMENTS)
    registry = load(ROUTE_REGISTRY)
    fixtures = load(FIXTURES)
    abi_manifest = load(ABI_MANIFEST)
    bazaar = load(BAZAAR)
    well_known = load(WELL_KNOWN)
    v2_paths = [line.strip() for line in V2_ROUTES.read_text(encoding="utf-8").splitlines() if line.strip()]
    fixture_by_path = {fixture["path"]: fixture for fixture in fixtures["fixtures"]}
    abi_by_contract = {record["contract"]: record for record in abi_manifest["records"]}
    blocked_abis = {record["contract"]: record for record in abi_manifest["blockedDeployedContracts"]}
    registry_routes = registry["routes"]

    contract_rows = []
    for contract in deployments["contracts"]:
        contract_name = contract["name"]
        abi = abi_by_contract.get(contract_name)
        blocked = blocked_abis.get(contract_name)
        contract_rows.append({
            "name": contract_name,
            "chain": contract["chain"],
            "chainId": contract["chainId"],
            "address": contract["address"],
            "status": contract["status"],
            "protocolVersion": contract["version"],
            "category": contract["category"],
            "custodiesFunds": contract["custodiesFunds"],
            "compatibility": {
                "v2Disposition": "frozen-deployed-baseline",
                "v3Replacement": None,
                "v3WriteRouting": "none",
                "reason": "V3 is parallel; no V2 contract write is redirected to V3.",
            },
            "abi": (
                {
                    "status": "provenance-backed",
                    "file": f"legacy/v2/{abi['file']}",
                    "sha256": abi["sha256"],
                    "provenance": abi["provenance"],
                }
                if abi else {
                    "status": "blocked-unresolved",
                    "file": None,
                    "reason": blocked["reason"],
                    "evidence": blocked["evidence"],
                    "searchedPaths": blocked["searchedPaths"],
                }
            ),
        })

    api_rows = []
    for path in v2_paths:
        fixture = fixture_by_path[path]
        api_rows.append({
            "path": path,
            "protocolVersion": "v2",
            "compatibility": {
                "v2Disposition": "preserved",
                "v3Replacement": None,
                "v3WriteRouting": "forbidden",
            },
            "responseFixture": fixture,
        })

    route_rows = []
    for route in registry_routes:
        item = dict(route)
        item["compatibility"] = (
            {
                "v2Disposition": "preserved",
                "v3Replacement": None,
                "v3WriteRouting": "forbidden",
            }
            if route["version"] == "v2" else {
                "v2Disposition": "not-applicable",
                "v3Access": "explicit-opt-in-only",
                "v2Fallback": "forbidden",
                "activation": "fail-closed-until-live-readiness-evidence",
            }
        )
        route_rows.append(item)

    return {
        "schema": "vaultfire.v2-v3-compatibility-discovery.v1",
        "baseline": {
            "releaseCommit": "eb7c7836e432f591745f807bd5199f5fa344b36f",
            "v2PinnedCommit": (ROOT / "baseline" / "V2_HEAD_COMMIT.txt").read_text(encoding="utf-8").strip(),
            "sources": [source_ref(path) for path in (DEPLOYMENTS, ROUTE_REGISTRY, V2_ROUTES, FIXTURES, ABI_MANIFEST, BAZAAR, WELL_KNOWN)],
        },
        "operatingPolicy": {
            "defaultProtocolVersion": "v2",
            "v3Access": "explicit-opt-in-allowlist-only",
            "v2ToV3WriteRedirect": "forbidden",
            "v3ToV2MoneyFallback": "forbidden",
            "v3DefaultState": "fail-closed",
            "v3LiveReadiness": "not-established-by-this-inventory",
        },
        "versionConcepts": {
            "contractProtocolVersion": {
                "values": ["v2", "v3"],
                "meaning": "Vaultfire contract/API/SDK namespace and compatibility version.",
                "evidence": ["baseline/deployments.json", "pr7/generated/route-registry.json"],
            },
            "x402PaymentRailVersion": {
                "value": 2,
                "meaning": "Payment challenge/payment/receipt message format; independent of the contract protocol version.",
                "evidence": ["pr7/sdk/v3/x402.ts", "pr7/generated/route-registry.json"],
            },
            "bazaarResourceVersion": {
                "value": "v2",
                "meaning": "Coinbase Bazaar resource metadata format; it can describe either Vaultfire protocol v2 or protocol v3 routes.",
                "evidence": ["pr7/generated/coinbase-bazaar-v2.json", "pr7/generated/well-known-x402.json"],
            },
            "invariants": [
                "A Vaultfire protocol v3 route can have x402Version 2.",
                "Bazaar resourceVersion v2 does not assert Vaultfire protocolVersion v2.",
                "No V2 route is changed to protocolVersion v3 by this metadata.",
            ],
        },
        "currentV2PaymentProcessing": {
            "state": "fail-closed-in-source",
            "evidence": source_ref(V2_PAYMENT_GATE),
            "behavior": "verifyX402Payment returns x402ReleaseGateClosed before parsing or accepting a payment header.",
            "liveSettlementClaim": "none",
        },
        "contracts": contract_rows,
        "v2ApiInventory": api_rows,
        "x402AndDiscoveryRoutes": route_rows,
        "sdk": {
            "v2DefaultPackages": [
                {
                    "package": "@vaultfire/agent-sdk",
                    "packageJson": source_ref(V2_SDK / "package.json"),
                    "surface": extract_sdk_surface(V2_SDK),
                    "compatibility": "retained-v2-default; no implicit V3 dispatch",
                },
                {
                    "package": "@vaultfire/x402",
                    "packageJson": source_ref(V2_X402_SDK / "package.json"),
                    "surface": extract_sdk_surface(V2_X402_SDK),
                    "compatibility": "retained-v2-package; payment rail version is independent of Vaultfire protocol version",
                },
            ],
            "v3PackageSource": {
                "path": "pr7/sdk/v3/index.ts",
                "compatibility": "separate namespace; callers must instantiate/select V3 explicitly",
            },
        },
        "discoveryPublication": {
            "localCandidateState": bazaar["publicationState"],
            "resourceServer": bazaar["resourceServer"],
            "facilitator": bazaar["facilitator"],
            "resourceCount": len(bazaar["resources"]),
            "protocolCounts": dict(Counter(resource["protocolVersion"] for resource in bazaar["resources"])),
            "resourceMetadataEqualsWellKnown": bazaar["resources"] == well_known["endpoints"],
            "liveIndexingOrSettlementProven": False,
        },
        "responseEvidenceRules": {
            "exactFixtures": sum(1 for fixture in fixtures["fixtures"] if fixture["fixtureStatus"] == "exact"),
            "unresolvedFixtures": sum(1 for fixture in fixtures["fixtures"] if fixture["fixtureStatus"] == "unresolved"),
            "rule": "Only a route-specific recorded response may create an exact schema. Unresolved routes retain method/schema null evidence and are never inferred.",
        },
        "canaryGate": {
            "paidRetry": "prohibited",
            "requires": [
                "owner approval of a public wallet",
                "owner-approved maximum budget",
                "route-specific live 402 challenge evidence",
                "owner-approved controlled payment plan",
            ],
            "prohibitedActions": [
                "deployment", "broadcast", "production routing change", "canary-fund spend", "receipt fabrication",
            ],
        },
    }


def summary(metadata: dict[str, Any]) -> str:
    contracts = metadata["contracts"]
    routes = metadata["x402AndDiscoveryRoutes"]
    v2_api = metadata["v2ApiInventory"]
    v2_routes = [row for row in routes if row["version"] == "v2"]
    v3_routes = [row for row in routes if row["version"] == "v3"]
    lines = [
        "# V2-to-V3 Compatibility Matrix",
        "",
        "Generated by `pr7/compat/build-v2-v3-compatibility-matrix.py`; do not hand-edit.",
        "",
        "## Default and activation policy",
        "",
        "| Control | Value |",
        "|---|---|",
        f"| Default protocol | `{metadata['operatingPolicy']['defaultProtocolVersion']}` |",
        f"| V3 access | `{metadata['operatingPolicy']['v3Access']}` |",
        f"| V2-to-V3 write redirect | `{metadata['operatingPolicy']['v2ToV3WriteRedirect']}` |",
        f"| V3 default state | `{metadata['operatingPolicy']['v3DefaultState']}` |",
        "",
        "## Version concepts remain distinct",
        "",
        "| Concept | Value | Meaning |",
        "|---|---|---|",
        "| Vaultfire contract/API protocol | `v2` / `v3` | Namespace and compatibility version |",
        "| x402 payment rail | `2` | Challenge, payment, and receipt message version |",
        "| Coinbase Bazaar resource metadata | `v2` | Resource-description format; may describe protocol v2 or v3 |",
        "",
        "## Exhaustive machine-readable inventory",
        "",
        f"- V2 deployed contract instances: **{len(contracts)}**.",
        f"- V2 API inventory paths: **{len(v2_api)}**.",
        f"- Registry entries: **{len(routes)}** (**{len(v2_routes)} V2**, **{len(v3_routes)} V3**).",
        f"- Exact V2 response fixtures: **{metadata['responseEvidenceRules']['exactFixtures']}**; unresolved: **{metadata['responseEvidenceRules']['unresolvedFixtures']}**.",
        "",
        "Every contract instance, V2 API path, x402 route, request/price/payment metadata field, fixture state, ABI state, and extracted SDK declaration is in [`compatibility-discovery-v2-v3.json`](./compatibility-discovery-v2-v3.json). `null`, `unresolved`, and `blocked-unresolved` are intentional evidence states, not defaults.",
        "",
        "## V3 allowlist",
        "",
        "| Method | Path | Price state | Activation |",
        "|---|---|---|---|",
    ]
    for route in v3_routes:
        price = "free" if route["free"] else f"{route['price']['amount']} {route['price']['asset']} on {route['price']['network']}"
        lines.append(f"| `{route['method']}` | `{route['path']}` | `{price}` | explicit opt-in, fail-closed until readiness evidence |")
    lines += [
        "",
        "## Payment and release state",
        "",
        "The checked-in V2 verifier currently returns its release-closed 503 response before it parses or accepts a payment. The generated Bazaar artifacts are metadata candidates only (`metadata-candidates-not-live-validated`); they are not proof of a public challenge, settlement, receipt, or indexing.",
        "",
        "## Canary gate",
        "",
        "No paid retry is authorized. A paid retry requires owner approval of a public wallet and maximum budget, plus route-specific live 402 evidence and an approved controlled payment plan.",
        "",
        "## Source URLs and evidence limits",
        "",
        "Public read-only evidence and exact command results are retained in `audit-evidence/phase3-v2-compatibility-20260822/`. Public endpoints: https://theloopbreaker.com/.well-known/x402.json, https://theloopbreaker.com/.well-known/agent-card.json, https://theloopbreaker.com/api/x402/discovery, https://theloopbreaker.com/api/x402/agent-preflight, and https://theloopbreaker.com/api/contracts. The collected read tool did not expose HTTP status/headers or permit an OPTIONS method, so no live OPTIONS or 402-header claim is made.",
        "",
    ]
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--check", action="store_true", help="fail if generated compatibility outputs are stale")
    args = parser.parse_args()
    metadata = build()
    rendered_json = canonical(metadata)
    rendered_summary = summary(metadata)
    if args.check:
        stale = []
        if not OUTPUT.is_file() or OUTPUT.read_text(encoding="utf-8") != rendered_json:
            stale.append(OUTPUT.relative_to(ROOT).as_posix())
        if not SUMMARY.is_file() or SUMMARY.read_text(encoding="utf-8") != rendered_summary:
            stale.append(SUMMARY.relative_to(ROOT).as_posix())
        if stale:
            print("V2/V3 compatibility outputs are stale: " + ", ".join(stale), file=sys.stderr)
            return 1
        print(f"V2/V3 compatibility inventory is current: {OUTPUT.relative_to(ROOT)}")
        return 0
    OUTPUT.write_text(rendered_json, encoding="utf-8")
    SUMMARY.write_text(rendered_summary, encoding="utf-8")
    print(f"wrote {OUTPUT.relative_to(ROOT)} and {SUMMARY.relative_to(ROOT)}")
    return 0


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