#!/usr/bin/env python3
"""Build the additive V3 x402 route-disposition matrix from recovered evidence.

This script keeps two inventories deliberately separate:
* 97 recovered public identities (86 core x402, 8 Solana, bridge, 2 well-known)
* 132 preserved V2 source-route dispositions

It does not rewrite, infer methods for, or activate V2.  V3 is a separate,
explicitly gated namespace and every unsafe or unevidenced identity is either
retired for safety or blocked with a concrete activation prerequisite.
"""
from __future__ import annotations

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

ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "baseline" / "extracted" / "v2_api_routes.txt"
REGISTRY = ROOT / "pr7" / "generated" / "route-registry.json"
IDENTITIES = ROOT / "pr7" / "compat" / "recovered-97-public-x402-identities.json"
OUTPUT = ROOT / "pr7" / "compat" / "v2-v3-route-disposition-matrix.json"

STATUS_IMPLEMENTED = "implemented-additive-handler"
STATUS_WRAPPER = "safe-compatibility-wrapper"
STATUS_BLOCKED = "blocked-pending-v3-dependency-evidence"
STATUS_RETIRED = "retired-for-safety"

# These wrappers deliberately return V3 migration/discovery information only.
# They do not proxy V2, call a wallet, accept a payment, or reproduce a V2
# pricing, eligibility, reputation, or routing result.
SAFE_WRAPPERS = {
    "GET /.well-known/agent-card.json": "/api/v3/x402/discovery",
    "GET /.well-known/x402.json": "/api/v3/x402/discovery",
    "GET /api/x402/agent-guide": "/api/v3/x402/discovery",
    "GET /api/x402/agent-preflight": "/api/v3/x402/challenge-status",
    "GET /api/x402/discovery": "/api/v3/x402/discovery",
    "GET /api/x402/errors": "/api/v3/x402/discovery",
    "GET /api/x402/oracle/chainlink-status": "/api/v3/protocol/status",
    "GET /api/x402/trust/health": "/api/v3/protocol/status",
}

# Historic identity names that are unsafe to carry forward because their input
# can be used to decide custody, access, eligibility, pricing, slashing,
# settlement, disputes, or routing.  A static V3 safety explanation is allowed;
# no V3 value or action is materialized from the historical signal.
MISSION_RETIRED_TOKENS = (
    "belief",
    "flourishing",
    "accountability-metrics",
    "partnership-metrics",
    "reputation",
    "street-cred",
    "vouch",
    "trust/agent-status",
    "trust/agentproof",
    "trust/analytics",
    "trust/bonds",
    "trust/history",
    "trust/leaderboard",
    "trust/multi-chain-status",
    "trust/performance",
    "trust/relationships",
    "trust/risk-score",
    "trust/safety-status",
    "trust/sla-status",
    "trust/credentials",
    "trust/email-verify",
    "bridge-trust",
    "agent/bridge",
    "slash-reputation",
    "request-validation",
    "submit-validation",
    "stake-as-validator",
    "submit-trust",
    "submit-feedback",
    "issue-credential",
    "revoke-credential",
    "create-session-key",
    "revoke-session-key",
)


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


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


def mission_reason(identity: str) -> str:
    path = identity.split(" ", 1)[1]
    if "bridge" in path:
        return "retired: cross-chain trust or behavioral data must not affect V3 routing; no non-profiling redesign is evidenced"
    if any(token in path for token in ("belief", "flourishing", "metrics")):
        return "retired: behavioral, belief, or flourishing-derived input could affect V3 custody, eligibility, pricing, settlement, or disputes"
    if any(token in path for token in ("credential", "session-key", "email-verify")):
        return "retired: profiling-derived or reputation-linked credentials/session authority must not affect V3 access or eligibility"
    if any(token in path for token in ("validation", "reputation", "street-cred", "vouch", "trust/", "feedback")):
        return "retired: reputation, quality, validation, or profiling-derived input must not affect V3 access, eligibility, pricing, slashing, settlement, disputes, or routing"
    return "retired: historical behavioral/profiling signal has no evidenced V3 redesign that prevents a protected decision impact"


def blocked_reason(identity: str) -> str:
    method, path = identity.split(" ", 1)
    if path.startswith("/api/solana/"):
        if path.endswith("/webhook"):
            return "blocked: receive/write lane requires authenticated webhook configuration, durable replay storage, and Solana operational evidence"
        return "blocked: exact source-only Solana inventory has no retained handler, V3 program/address map, indexer/RPC evidence, or mainnet read evidence"
    if method != "GET":
        return "blocked: write/payment/settlement surface needs a reviewed V3 contract/address map, request binding, replay protection, durable receipt evidence, and finality evidence"
    return "blocked: read surface needs a route-specific V3 data source, address/deployment evidence, response schema, and read-only activation evidence"


def v3_disposition(item: dict[str, Any]) -> dict[str, Any]:
    identity = item["identity"]
    path = item["path"]
    if identity == "GET /api/solana/health":
        return {
            "status": STATUS_IMPLEMENTED,
            "route": "/api/v3/solana/health",
            "reason": "implemented additive V3 status handler returns source-only readiness without RPC, payment, settlement, or deployment claims",
            "activationEvidence": "exact VAULTFIRE_X402_V3_RELEASE=read-only-fail-closed-v1; handler is static and does not contact Solana",
        }
    if identity in SAFE_WRAPPERS:
        return {
            "status": STATUS_WRAPPER,
            "route": SAFE_WRAPPERS[identity],
            "reason": "safe V3 wrapper returns only V3 migration/discovery or payment-challenge availability labels; it does not proxy V2 or accept payment",
            "activationEvidence": "exact VAULTFIRE_X402_V3_RELEASE=read-only-fail-closed-v1; wrapper response has no V3 payment address, Bazaar listing ID, or settlement receipt",
        }
    if any(token in path for token in MISSION_RETIRED_TOKENS):
        return {
            "status": STATUS_RETIRED,
            "route": None,
            "reason": mission_reason(identity),
            "activationEvidence": "none; requires a separately reviewed non-profiling V3 redesign before any new route could be proposed",
        }
    return {
        "status": STATUS_BLOCKED,
        "route": None,
        "reason": blocked_reason(identity),
        "activationEvidence": "none; remain fail-closed until the stated V3 dependency/evidence is independently available and reviewed",
    }


def build() -> dict[str, Any]:
    source_paths = [line.strip() for line in SOURCE.read_text().splitlines() if line.strip()]
    if len(source_paths) != 132 or len(set(source_paths)) != 132:
        raise ValueError("expected exactly 132 unique preserved V2 source routes")

    recovered = json.loads(IDENTITIES.read_text())
    identities = recovered.get("identities")
    if not isinstance(identities, list) or len(identities) != 97:
        raise ValueError("expected exactly 97 recovered public identities")
    identity_values = [entry.get("identity") for entry in identities]
    if len(set(identity_values)) != 97 or any(not isinstance(value, str) or " " not in value for value in identity_values):
        raise ValueError("recovered public identities must be unique method/path identities")
    expected_counts = {"core-x402": 86, "solana": 8, "cross-chain": 1, "well-known": 2}
    actual_counts = {group: sum(item.get("group") == group for item in identities) for group in expected_counts}
    if actual_counts != expected_counts:
        raise ValueError(f"recovered public identity groups differ: {actual_counts}")

    registry = json.loads(REGISTRY.read_text())
    methods_by_path: dict[str, list[str]] = {}
    for route in registry.get("routes", []):
        if route.get("version") == "v2":
            methods_by_path.setdefault(route["path"], []).append(route["method"])

    public_rows = []
    for index, item in enumerate(identities, 1):
        disposition = v3_disposition(item)
        public_rows.append({
            "identityIndex": index,
            "identity": item["identity"],
            "method": item["method"],
            "path": item["path"],
            "group": item["group"],
            "recoveredDescription": item.get("description"),
            "evidence": item["evidence"],
            "v3": {
                **disposition,
                "v2Fallback": "forbidden",
                "paymentSettlement": "not-enabled-without-real-v3-address-and-durable-route-bound-settlement-evidence",
                "bazaar": "no-listing-id-or-indexing-claim; generated metadata is not a publication receipt",
            },
        })

    public_by_path: dict[str, list[dict[str, Any]]] = {}
    for row in public_rows:
        public_by_path.setdefault(row["path"], []).append(row)
    source_rows = []
    for index, path in enumerate(source_paths, 1):
        linked = public_by_path.get(path, [])
        source_rows.append({
            "sourceIndex": index,
            "v2SourcePath": path,
            "v2Methods": sorted(set(methods_by_path.get(path, []))),
            "preserveV2Unchanged": True,
            "v2Disposition": "preserve-unchanged",
            "publicIdentityRefs": [row["identity"] for row in linked],
            "v3Relationship": "separate additive V3 namespace; no V2 handler, ABI, manifest, or deployed-address record is changed",
        })

    counts = {
        "publicIdentities": len(public_rows),
        "coreX402": actual_counts["core-x402"],
        "solana": actual_counts["solana"],
        "crossChain": actual_counts["cross-chain"],
        "wellKnown": actual_counts["well-known"],
        "v2SourceRoutes": len(source_rows),
    }
    for status in (STATUS_IMPLEMENTED, STATUS_WRAPPER, STATUS_BLOCKED, STATUS_RETIRED):
        counts[status] = sum(row["v3"]["status"] == status for row in public_rows)

    return {
        "schema": "vaultfire.v2-v3-x402-route-disposition.v2",
        "generatedAt": "1970-01-01T00:00:00Z",
        "policy": {
            "v2": "preserve-unchanged",
            "v3": "additive-explicit-opt-in-fail-closed",
            "v2Fallback": "forbidden",
            "missionBoundary": "behavioral, flourishing, reputation, loyalty, quality, belief, biometric, profiling, and surveillance-derived input must not affect custody, access, eligibility, pricing, slashing, settlement, disputes, or routing",
            "paymentSettlement": "disabled until real V3 address evidence and durable request/resource-bound settlement evidence are independently verified",
            "bazaar": "metadata candidates are not Bazaar listing IDs, indexing proof, or settlement receipts",
        },
        "sources": {
            "recoveredPublicIdentityInventory": {
                "path": IDENTITIES.relative_to(ROOT).as_posix(),
                "sha256": sha256(IDENTITIES),
                "counts": recovered["counts"],
                "qualification": recovered["qualification"],
            },
            "v2SourceInventory": {
                "path": SOURCE.relative_to(ROOT).as_posix(),
                "sha256": sha256(SOURCE),
                "routeCount": len(source_paths),
            },
            "generatedRegistry": {"path": REGISTRY.relative_to(ROOT).as_posix(), "sha256": sha256(REGISTRY)},
        },
        "counts": counts,
        "publicIdentityDispositions": public_rows,
        "v2SourceRouteDispositions": source_rows,
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--check", action="store_true")
    args = parser.parse_args()
    rendered = canonical(build())
    if args.check:
        if not OUTPUT.exists() or OUTPUT.read_text() != rendered:
            print(f"stale: {OUTPUT.relative_to(ROOT)}")
            return 1
        print(f"ok: {OUTPUT.relative_to(ROOT)}")
        return 0
    OUTPUT.write_text(rendered)
    print(f"wrote {OUTPUT.relative_to(ROOT)}")
    return 0


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