#!/usr/bin/env python3
"""Fail-closed static checks for generated Coinbase Bazaar x402 v2 candidates.

This validator intentionally proves only the checked-in metadata.  It cannot
prove a public route returns 402 or is indexed; validate_bazaar_readiness.py
requires owner-supplied read-only CDP validation and settlement evidence for
those claims.
"""
import json
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
REGISTRY = ROOT / "pr7" / "generated" / "route-registry.json"
BAZAAR = ROOT / "pr7" / "generated" / "coinbase-bazaar-v2.json"
WELL_KNOWN = ROOT / "pr7" / "generated" / "well-known-x402.json"
BASE_URL = "https://theloopbreaker.com"
BASE_NETWORK = "eip155:8453"
SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
SUPPORTED_NETWORKS = {BASE_NETWORK, SOLANA_MAINNET}


def fail(message):
    raise SystemExit(f"BLOCKED: {message}")


def matches_schema(value, schema, label):
    """Small strict JSON Schema subset sufficient for generated extension metadata."""
    if not isinstance(schema, dict):
        fail(f"{label}: schema must be an object")
    if "const" in schema and value != schema["const"]:
        fail(f"{label}: expected const {schema['const']!r}")
    if "enum" in schema and value not in schema["enum"]:
        fail(f"{label}: not in enum")
    if "not" in schema:
        try:
            matches_schema(value, schema["not"], label)
        except SystemExit:
            pass
        else:
            fail(f"{label}: matches prohibited schema")
    if "anyOf" in schema:
        for branch in schema["anyOf"]:
            try:
                matches_schema(value, branch, label)
            except SystemExit:
                continue
            break
        else:
            fail(f"{label}: does not match any anyOf branch")
    expected_type = schema.get("type")
    type_ok = {
        "object": isinstance(value, dict), "array": isinstance(value, list),
        "string": isinstance(value, str), "number": isinstance(value, (int, float)) and not isinstance(value, bool),
        "integer": isinstance(value, int) and not isinstance(value, bool), "boolean": isinstance(value, bool),
    }
    if expected_type and not type_ok.get(expected_type, True):
        fail(f"{label}: expected {expected_type}")
    if isinstance(value, str) and "pattern" in schema:
        if not re.search(schema["pattern"], value):
            fail(f"{label}: does not match pattern")
    if isinstance(value, (int, float)) and not isinstance(value, bool):
        if "minimum" in schema and value < schema["minimum"]:
            fail(f"{label}: below minimum")
        if "maximum" in schema and value > schema["maximum"]:
            fail(f"{label}: above maximum")
    if isinstance(value, dict):
        properties = schema.get("properties", {})
        for name in schema.get("required", []):
            if name not in value:
                fail(f"{label}: missing required property {name}")
        if schema.get("additionalProperties") is False:
            unexpected = set(value) - set(properties)
            if unexpected:
                fail(f"{label}: unexpected properties {sorted(unexpected)}")
        for name, child in properties.items():
            if name in value:
                matches_schema(value[name], child, f"{label}.{name}")
    if isinstance(value, list) and "items" in schema:
        for index, item in enumerate(value):
            matches_schema(item, schema["items"], f"{label}[{index}]")


def main():
    registry = json.loads(REGISTRY.read_text())
    bazaar = json.loads(BAZAAR.read_text())
    well_known = json.loads(WELL_KNOWN.read_text())
    for name, artifact, items_key in (("Bazaar", bazaar, "resources"), ("well-known", well_known, "endpoints")):
        if artifact.get("x402Version") != 2:
            fail(f"{name} output is not x402 payment rail v2")
        if artifact.get("bazaarResourceVersion") != "v2":
            fail(f"{name} output is not Bazaar resource version v2")
        if artifact.get("publicationState") != "metadata-candidates-not-live-validated":
            fail(f"{name} makes an unsupported live/indexing claim")
        if not isinstance(artifact.get(items_key), list):
            fail(f"{name} has no resources")
    if bazaar["resources"] != well_known["endpoints"]:
        fail("Bazaar and well-known resources diverge")

    candidate_routes = [route for route in registry["routes"] if route.get("bazaarEligibility") == "candidate"]
    expected = {(route["method"], route["path"]): route for route in candidate_routes}
    resources = bazaar["resources"]
    if len(resources) != len(expected):
        fail("Bazaar resource count does not match metadata candidates")
    if not any(any(option["network"] == SOLANA_MAINNET for option in item["accepts"]) for item in resources):
        fail("no Solana mainnet resource candidates are advertised")

    seen = set()
    for item in resources:
        key = (item.get("method"), item.get("routeTemplate"))
        if key not in expected or key in seen:
            fail(f"unknown or duplicate resource {key}")
        seen.add(key)
        route = expected[key]
        if item.get("resource") != f"{BASE_URL}{route['path']}":
            fail(f"non-canonical HTTPS resource URL for {key}")
        if item.get("resourceVersion") != "v2" or item.get("x402Version") != 2:
            fail(f"Bazaar/payment version mismatch for {key}")
        if item.get("protocolVersion") != route["version"]:
            fail(f"contract protocol version changed for {key}")
        if len(item.get("description", "")) > 500 or not item.get("description", "").strip():
            fail(f"description missing or exceeds Coinbase limit for {key}")
        accepts = item.get("accepts")
        if not isinstance(accepts, list) or not accepts:
            fail(f"missing payment options for {key}")
        if accepts != route["accepts"]:
            fail(f"payment options diverge from registry for {key}")
        networks = set()
        for option in accepts:
            if option.get("scheme") != "exact":
                fail(f"unsupported scheme for {key}")
            if option.get("network") not in SUPPORTED_NETWORKS or option["network"] in networks:
                fail(f"unsupported or duplicate network for {key}")
            networks.add(option["network"])
            if not re.fullmatch(r"[1-9][0-9]*", option.get("maxAmountRequired", "")):
                fail(f"amount is not positive atomic units for {key}")
            if not isinstance(option.get("asset"), str) or not option["asset"]:
                fail(f"asset missing for {key}")
            if not isinstance(option.get("payTo"), str) or not option["payTo"]:
                fail(f"payTo missing for {key}")
            if not isinstance(option.get("maxTimeoutSeconds"), int) or option["maxTimeoutSeconds"] <= 0:
                fail(f"maxTimeoutSeconds must be a positive explicit integer for {key}")
        if BASE_NETWORK not in networks:
            fail(f"Base payment option missing for {key}")
        solana_status = item.get("solanaPaymentStatus")
        if SOLANA_MAINNET in networks:
            if solana_status != "metadata-configured-unverified":
                fail(f"Solana option without canonical settlement status for {key}")
            solana = next(option for option in accepts if option["network"] == SOLANA_MAINNET)
            if not re.fullmatch(r"[1-9A-HJ-NP-Za-km-z]{32,44}", solana["asset"]) or not re.fullmatch(r"[1-9A-HJ-NP-Za-km-z]{32,44}", solana["payTo"]):
                fail(f"Solana option lacks canonical asset/payTo for {key}")
        elif solana_status != "read-only-api-no-solana-settlement":
            fail(f"route without Solana option must be explicitly read-only/API-on-Solana for {key}")

        extension = item.get("extensions", {}).get("bazaar")
        if not isinstance(extension, dict) or set(extension) != {"info", "schema"}:
            fail(f"missing Bazaar info/schema declaration for {key}")
        info, schema = extension["info"], extension["schema"]
        matches_schema(info, schema, f"Bazaar info {key}")
        if info.get("method") != item["method"] or info.get("path") != item["routeTemplate"]:
            fail(f"Bazaar method/path must be explicit for {key}")
        input_info = info.get("input")
        if input_info.get("type") != "http" or input_info.get("method") != item["method"]:
            fail(f"Bazaar HTTP input does not match resource for {key}")
        if item["method"] == "GET":
            required_input_key = "pathParams" if ":" in item["routeTemplate"] else "queryParams"
            if required_input_key not in input_info:
                fail(f"GET resource lacks {required_input_key} example for {key}")
        if item["method"] == "POST" and (input_info.get("bodyType") != "json" or "body" not in input_info):
            fail(f"POST resource lacks JSON body schema/example for {key}")
        if not isinstance(info.get("output", {}).get("example"), (dict, list, str, int, float, bool)):
            fail(f"output example missing for {key}")

    print(f"PASS: {len(resources)} Bazaar x402 v2 metadata candidates; {sum(any(option['network'] == SOLANA_MAINNET for option in item['accepts']) for item in resources)} include Solana mainnet payment options")


if __name__ == "__main__":
    main()
