#!/usr/bin/env python3
"""Validate frozen V2 ABI payloads, coverage, ordering, and provenance."""
from __future__ import annotations

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

ROOT = Path(__file__).resolve().parents[1]
PACKAGE = ROOT / "legacy/v2"
MANIFEST_PATH = PACKAGE / "manifest.json"
DEPLOYMENTS = ROOT / "baseline/deployments.json"


def canonical(value: object) -> bytes:
    return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()


def digest(payload: bytes) -> str:
    return hashlib.sha256(payload).hexdigest()


def solidity_type(item: dict[str, Any]) -> str:
    value = item["type"]
    if value.startswith("tuple"):
        suffix = value[len("tuple") :]
        value = "(" + ",".join(solidity_type(component) for component in item.get("components", [])) + ")" + suffix
    return value


def signature(entry: dict[str, Any]) -> str:
    return f"{entry.get('name', '')}({','.join(solidity_type(i) for i in entry.get('inputs', []))})"


def fail(message: str) -> None:
    raise SystemExit(message)


def verify_verified_deployed_source(provenance: dict[str, Any], contract: str) -> None:
    """Verify a local, frozen explorer response without performing network I/O."""
    required = ("provider", "url", "retrievedAt", "chainId", "address", "compilerVersion", "sourceCodeSha256", "abiFieldSha256")
    if any(not provenance.get(key) for key in required):
        fail(f"incomplete verified deployed-source provenance: {contract}")
    files: dict[str, bytes] = {}
    for key in ("response", "fetchEnvelope", "metadata"):
        reference = provenance.get(key)
        if not isinstance(reference, dict) or not isinstance(reference.get("path"), str) or len(reference.get("sha256", "")) != 64:
            fail(f"invalid verified deployed-source file reference: {contract}: {key}")
        path = ROOT / reference["path"]
        if not path.is_file() or digest(path.read_bytes()) != reference["sha256"]:
            fail(f"verified deployed-source file drift: {contract}: {reference['path']}")
        files[key] = path.read_bytes()
    try:
        metadata = json.loads(files["metadata"])
        envelope = json.loads(files["fetchEnvelope"])
        response = json.loads(files["response"])
        fetched = envelope["results"][0]
        source = response["result"][0]
    except (KeyError, IndexError, TypeError, json.JSONDecodeError) as exc:
        fail(f"invalid frozen verified deployed-source evidence: {contract}: {exc}")
    if (
        metadata.get("schemaVersion") != "1.0.0"
        or response.get("status") != "1"
        or response.get("message") != "OK"
        or len(response.get("result", [])) != 1
        or fetched.get("error") is not None
        or fetched.get("is_cached") is not False
        or fetched.get("raw_html", "").encode() != files["response"]
        or fetched.get("url") != provenance["url"]
        or metadata.get("rawResponse", {}).get("sha256") != provenance["response"]["sha256"]
        or metadata.get("rawResponse", {}).get("bytes") != len(files["response"])
        or metadata.get("rawResponse", {}).get("path") != provenance["response"]["path"]
        or metadata.get("retrieval", {}).get("contentFetchEnvelopeSha256") != provenance["fetchEnvelope"]["sha256"]
        or metadata.get("retrieval", {}).get("contentFetchEnvelope") != provenance["fetchEnvelope"]["path"]
        or metadata.get("retrieval", {}).get("url") != provenance["url"]
        or metadata.get("retrieval", {}).get("method") != "pplx_sdk.content.fetch"
        or metadata.get("retrieval", {}).get("cacheEnabled") is not False
        or metadata.get("retrieval", {}).get("returnHtml") is not True
        or metadata.get("retrieval", {}).get("returnedField") != "raw_html"
        or metadata.get("retrieval", {}).get("isCached") is not False
        or source.get("Address", "").lower() != provenance["address"]
        or source.get("ContractName") != contract
        or source.get("CompilerVersion") != provenance["compilerVersion"]
        or digest(source.get("SourceCode", "").encode()) != provenance["sourceCodeSha256"]
        or digest(source.get("ABI", "").encode()) != provenance["abiFieldSha256"]
        or metadata.get("verifiedContract", {}).get("address", "").lower() != provenance["address"]
        or metadata.get("verifiedContract", {}).get("contractName") != contract
        or metadata.get("verifiedContract", {}).get("chainId") != provenance["chainId"]
        or metadata.get("verifiedContract", {}).get("compilerVersion") != provenance["compilerVersion"]
        or metadata.get("verifiedContract", {}).get("sourceCodeSha256") != provenance["sourceCodeSha256"]
        or metadata.get("verifiedContract", {}).get("abiFieldSha256") != provenance["abiFieldSha256"]
    ):
        fail(f"verified deployed-source content mismatch: {contract}")


def verify_source_inputs(manifest: dict[str, Any], v2_root: Path) -> None:
    expected = (ROOT / "baseline/V2_HEAD_COMMIT.txt").read_text().strip()
    result = subprocess.run(["git", "-C", str(v2_root), "rev-parse", "HEAD"], capture_output=True, text=True)
    if result.returncode or result.stdout.strip() != expected:
        fail(f"pinned V2 checkout drift at {v2_root}")
    for record in manifest["records"]:
        provenance = record["provenance"]
        if provenance["kind"] == "canonical-abi-json":
            path = v2_root / provenance["source"]
            if not path.is_file() or digest(path.read_bytes()) != provenance["sourceSha256"]:
                fail(f"canonical ABI input drift: {record['contract']}")
        elif provenance["kind"] == "pinned-solidity-source":
            for source in provenance["sourceFiles"]:
                path = v2_root / source["path"]
                if not path.is_file() or digest(path.read_bytes()) != source["sha256"]:
                    fail(f"Solidity source input drift: {record['contract']}: {source['path']}")
        for evidence in provenance.get("deploymentEvidence", []):
            path = v2_root / evidence["path"]
            if not path.is_file() or digest(path.read_bytes()) != evidence["sha256"]:
                fail(f"deployment evidence drift: {record['contract']}: {evidence['path']}")
    for blocked in manifest["blockedDeployedContracts"]:
        for evidence in blocked.get("evidenceSha256", []):
            if evidence["path"].startswith("v2:"):
                path = v2_root / evidence["path"].removeprefix("v2:")
                if not path.is_file() or digest(path.read_bytes()) != evidence["sha256"]:
                    fail(f"blocked V2 evidence drift: {blocked['contract']}: {evidence['path']}")


def verify_runtime_bytecode_binding(record: dict[str, Any], deployments: list[dict[str, Any]]) -> None:
    """Bind an explicitly compiled source ABI to every frozen deployed runtime observation."""
    contract = record["contract"]
    provenance = record["provenance"]
    compiler_input = provenance.get("compilerInput")
    if not compiler_input:
        return
    if not isinstance(compiler_input, dict):
        fail(f"invalid compiler input provenance: {contract}")
    source_root = compiler_input.get("sourceRoot")
    settings = compiler_input.get("settings")
    version = compiler_input.get("solcVersion")
    binding = compiler_input.get("runtimeBytecodeBinding")
    if (
        not isinstance(source_root, str)
        or not source_root
        or not isinstance(settings, dict)
        or not settings
        or not isinstance(version, str)
        or not version
        or not isinstance(binding, dict)
    ):
        fail(f"incomplete runtime-bytecode binding: {contract}")
    if version not in provenance["solcVersion"]:
        fail(f"compiler version mismatch in runtime-bytecode binding: {contract}")
    prefix = source_root.rstrip("/") + "/"
    sources: dict[str, dict[str, str]] = {}
    for entry in provenance["sourceFiles"]:
        path = entry["path"]
        if not path.startswith(prefix):
            fail(f"runtime-bytecode source root mismatch: {contract}: {path}")
        sources[path.removeprefix(prefix)] = {"content": (ROOT / path).read_text()}
    standard_input = {"language": "Solidity", "sources": sources, "settings": settings}
    if digest(canonical(standard_input)) != provenance["standardInputSha256"]:
        fail(f"runtime-bytecode compiler input drift: {contract}")
    if binding.get("sourceUnit") != provenance["source"].removeprefix(prefix):
        fail(f"runtime-bytecode source-unit mismatch: {contract}")
    compiled_runtime = binding.get("compiledRuntime")
    if (
        not isinstance(compiled_runtime, dict)
        or not isinstance(compiled_runtime.get("bytes"), int)
        or len(compiled_runtime.get("sha256", "")) != 64
    ):
        fail(f"invalid compiled runtime record: {contract}")
    observed = binding.get("deployments")
    if not isinstance(observed, list) or not observed:
        fail(f"missing deployed runtime bindings: {contract}")
    expected = sorted(
        [
            {
                "chain": item["chain"],
                "chainId": item["chainId"],
                "address": item["address"].lower(),
                "runtimeBytecodeBytes": item["onchain"]["bytecodeBytes"],
                "runtimeBytecodeSha256": item["onchain"]["bytecodeSha256"],
            }
            for item in deployments
            if item["name"] == contract
        ],
        key=lambda item: (item["chain"], item["chainId"], item["address"]),
    )
    actual = sorted(observed, key=lambda item: (item.get("chain"), item.get("chainId"), item.get("address", "")))
    if actual != expected:
        fail(f"deployed runtime binding drift: {contract}")
    if any(
        item["runtimeBytecodeBytes"] != compiled_runtime["bytes"]
        or item["runtimeBytecodeSha256"] != compiled_runtime["sha256"]
        for item in actual
    ):
        fail(f"compiled runtime does not match every deployed address: {contract}")


def validate_record(record: dict[str, Any]) -> None:
    contract = record.get("contract", "<unknown>")
    path = PACKAGE / record["file"]
    if not path.is_file():
        fail(f"ABI payload missing: {contract}: {record['file']}")
    payload = path.read_bytes()
    if payload != canonical(json.loads(payload)):
        fail(f"ABI payload is not canonical JSON: {contract}")
    if digest(payload) != record["sha256"]:
        fail(f"ABI hash drift: {contract}")
    abi = json.loads(payload)
    if not isinstance(abi, list):
        fail(f"ABI payload is not an array: {contract}")
    if len(abi) != record["entries"]:
        fail(f"ABI entry drift: {contract}")
    for key, kind in (("functions", "function"), ("events", "event"), ("errors", "error")):
        actual = sorted(signature(item) for item in abi if item.get("type") == kind)
        if actual != record[key]:
            fail(f"ABI {key} drift: {contract}")
    provenance = record.get("provenance")
    if not isinstance(provenance, dict) or provenance.get("kind") not in {"canonical-abi-json", "pinned-solidity-source", "verified-deployed-source"}:
        fail(f"missing or unsupported provenance: {contract}")
    if provenance["kind"] == "canonical-abi-json":
        if not isinstance(provenance.get("source"), str) or len(provenance.get("sourceSha256", "")) != 64:
            fail(f"incomplete canonical-ABI provenance: {contract}")
    elif provenance["kind"] == "pinned-solidity-source":
        if not isinstance(provenance.get("source"), str) or not isinstance(provenance.get("sourceContract"), str):
            fail(f"incomplete Solidity provenance: {contract}")
        sources = provenance.get("sourceFiles")
        if not isinstance(sources, list) or not sources:
            fail(f"missing Solidity source closure: {contract}")
        if [entry["path"] for entry in sources] != sorted(entry["path"] for entry in sources):
            fail(f"non-deterministic Solidity source closure: {contract}")
        if any(len(entry.get("sha256", "")) != 64 for entry in sources):
            fail(f"invalid Solidity source hash: {contract}")
        if not isinstance(provenance.get("solcVersion"), str) or not provenance["solcVersion"]:
            fail(f"missing solc version: {contract}")
        if len(provenance.get("standardInputSha256", "")) != 64:
            fail(f"missing solc input hash: {contract}")
        evidence = provenance.get("deploymentEvidence", [])
        if evidence:
            if not isinstance(evidence, list) or [entry.get("path") for entry in evidence] != sorted(entry.get("path") for entry in evidence):
                fail(f"non-deterministic deployment evidence: {contract}")
            if any(not isinstance(entry.get("path"), str) or len(entry.get("sha256", "")) != 64 for entry in evidence):
                fail(f"invalid deployment evidence hash: {contract}")
        elif not provenance.get("compilerInput", {}).get("runtimeBytecodeBinding"):
            fail(f"missing deployment evidence: {contract}")
    else:
        verify_verified_deployed_source(provenance, contract)


def validate(manifest: dict[str, Any], v2_root: Path | None = None) -> list[dict[str, Any]]:
    if manifest.get("schemaVersion") != "2.2.0":
        fail(f"unsupported ABI manifest schema: {manifest.get('schemaVersion')}")
    artifact_search = manifest.get("compiledArtifactSearch")
    if not isinstance(artifact_search, dict) or not artifact_search.get("paths") or not artifact_search.get("result"):
        fail("compiled artifact search provenance missing")
    expected_head = (ROOT / "baseline/V2_HEAD_COMMIT.txt").read_text().strip()
    if manifest.get("v2Head") != expected_head:
        fail("V2 HEAD drift")
    records = manifest.get("records")
    if not isinstance(records, list) or not records:
        fail("ABI package contains no records")
    names = [record.get("contract") for record in records]
    if names != sorted(names) or len(names) != len(set(names)):
        fail("ABI records are not deterministically sorted and unique")
    for record in records:
        validate_record(record)
    expected_payloads = {record["file"] for record in records}
    actual_payloads = {
        path.relative_to(PACKAGE).as_posix()
        for path in (PACKAGE / "abis").glob("*.json")
    }
    if actual_payloads != expected_payloads:
        fail(f"ABI payload inventory mismatch; missing={sorted(expected_payloads - actual_payloads)}, unexpected={sorted(actual_payloads - expected_payloads)}")
    deployments = json.loads(DEPLOYMENTS.read_text())["contracts"]
    deployment_names = {entry["name"] for entry in deployments}
    blocked = manifest.get("blockedDeployedContracts")
    if not isinstance(blocked, list):
        fail("blocked deployed contract list missing")
    blocked_names = [entry.get("contract") for entry in blocked]
    if blocked_names != sorted(blocked_names) or len(blocked_names) != len(set(blocked_names)):
        fail("blocked contract records are not deterministically sorted and unique")
    for entry in blocked:
        if not isinstance(entry.get("reason"), str) or not entry["reason"]:
            fail(f"blocked contract has no reason: {entry.get('contract')}")
        evidence = entry.get("evidence")
        if not isinstance(evidence, list) or not evidence:
            fail(f"blocked contract has no evidence: {entry.get('contract')}")
        for relative in evidence:
            if not isinstance(relative, str):
                fail(f"invalid blocked-contract evidence: {entry.get('contract')}")
            if relative.startswith("v2:"):
                if not relative.removeprefix("v2:"):
                    fail(f"invalid V2 evidence path: {entry.get('contract')}")
            elif not (ROOT / relative).is_file():
                fail(f"blocked contract evidence missing: {entry.get('contract')}: {relative}")
        if not entry.get("searchedPaths"):
            fail(f"blocked contract has no pinned-search scope: {entry.get('contract')}")
        evidence_hashes = entry.get("evidenceSha256")
        if not isinstance(evidence_hashes, list) or [item.get("path") for item in evidence_hashes] != sorted(evidence):
            fail(f"blocked contract evidence hashes missing or unordered: {entry.get('contract')}")
        if any(len(item.get("sha256", "")) != 64 for item in evidence_hashes):
            fail(f"invalid blocked contract evidence hash: {entry.get('contract')}")
        for item in evidence_hashes:
            if item["path"].startswith("v2:"):
                continue
            if digest((ROOT / item["path"]).read_bytes()) != item["sha256"]:
                fail(f"blocked contract evidence drift: {entry.get('contract')}: {item['path']}")
    covered = set(names) | set(blocked_names)
    if covered != deployment_names:
        fail(f"deployment coverage mismatch; unaccounted={sorted(deployment_names - covered)}, unexpected={sorted(covered - deployment_names)}")
    if manifest.get("canonicalAbiCount") != len(records):
        fail("canonical ABI count drift")
    if manifest.get("deployedUniqueContractCount") != len(deployment_names):
        fail("deployed contract count drift")
    for record in records:
        verify_runtime_bytecode_binding(record, deployments)
    if v2_root is not None:
        verify_source_inputs(manifest, v2_root)
    return blocked


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--strict", action="store_true", help="fail if any deployed ABI remains explicitly blocked")
    parser.add_argument("--v2-root", type=Path, help="also verify pinned ABI/source input hashes against this V2 checkout")
    args = parser.parse_args()
    try:
        manifest = json.loads(MANIFEST_PATH.read_text())
    except (OSError, json.JSONDecodeError) as exc:
        fail(f"cannot read ABI manifest: {exc}")
    blocked = validate(manifest, args.v2_root)
    print(f"verified {len(manifest['records'])} frozen, provenance-backed V2 ABIs")
    if blocked:
        print("blocked traceable ABI acquisition: " + ", ".join(item["contract"] for item in blocked))
        if args.strict:
            fail(f"BLOCKED: traceable ABI unavailable for {len(blocked)} deployed V2 contract names; see legacy/v2/manifest.json blockedDeployedContracts")


if __name__ == "__main__":
    main()
