"""Offline public-only verification of the frozen xpub privacy experiment.

Python3.10+ standard library. No wallet, node, network, private key or seed.
This verifies recorded data with educational public math, not wallet security.
"""
from pathlib import Path
import csv
import hashlib
import importlib.machinery
import importlib.util
import io
import json
import sys

sys.dont_write_bytecode = True

BASE_PUBLIC = {"results.json", "addresses.csv", "nested-controls.csv", "rpc-trace.json", "rpc-help.json", "cleanup.json", "public-derivation.py.txt", "reproduce.py.txt", "verify.py.txt", "README.txt", "sources.json", "sharing-checklist.txt"}
OPTIONAL_PUBLIC = {"reviewed-source-index.json", "reviewed-source-snapshots.zip", "artwork-provenance.json"}
CHECKS = 0


def check(name, condition):
    global CHECKS
    CHECKS += 1
    if not condition:
        raise AssertionError(name)


def reject(name, operation):
    try:
        operation()
    except ValueError:
        check(name, True)
    else:
        check(name, False)


def main(folder):
    seen = set()
    manifest = (folder / "SHA256SUMS.txt").read_text(encoding="utf-8")
    check("Bounded checksum manifest", len(manifest.encode()) <= 65536)
    for line in manifest.splitlines():
        expected, name = line.split("  ", 1)
        check("Only explicitly allowed public artifact names", name in BASE_PUBLIC | OPTIONAL_PUBLIC and name not in seen)
        seen.add(name)
        path = (folder / name).resolve()
        check("Manifest path is local and bounded", path.is_relative_to(folder.resolve()) and path.stat().st_size <= 8 * 1024 * 1024)
        check("SHA256 " + name, hashlib.sha256(path.read_bytes()).hexdigest() == expected)
    check("All required public artifacts included", BASE_PUBLIC <= seen)
    loader = importlib.machinery.SourceFileLoader("public_derivation_verifier", str(folder / "public-derivation.py.txt"))
    math = importlib.util.module_from_spec(importlib.util.spec_from_loader(loader.name, loader))
    loader.exec_module(math)
    result = json.loads((folder / "results.json").read_text(encoding="utf-8"))
    fixture = result["fixture"]
    check("Pinned public parent exactly official fixture", fixture["public_parent_xpub"] == math.FIXTURE_PARENT)
    check("Official known non-hardened child public vector", math.child(math.FIXTURE_PARENT, 1) == math.FIXTURE_CHILD_ONE == fixture["published_next_child_xpub"])
    tpub = fixture["public_parent_tpub"]
    branch0 = fixture["receiving_branch0_tpub"]
    check("Only4version bytes changed in payload", math.decode_extended(tpub)[:4] == math.TPUB and math.decode_extended(tpub)[4:] == math.decode_extended(math.FIXTURE_PARENT)[4:])
    check("Public receiving branch exactly reproduced", math.child(tpub, 0) == branch0)
    check("Regtest with tested runtime", result["network"] == "regtest" and result["core_version"] == "/LitecoinCore:0.21.5.8/")
    check("Wallet support disabled", "-disablewallet=1" in result["node_options"])
    rows = result["addresses"]
    csv_rows = list(csv.DictReader(io.StringIO((folder / "addresses.csv").read_text(encoding="utf-8"))))
    check("Fifteen bounded address records", len(rows) == len(csv_rows) == 15)
    check("Ten receiving and five change", [row["branch"] for row in rows] == [0] * 10 + [1] * 5)
    for row, csv_row in zip(rows, csv_rows):
        key = math.derive(tpub, [row["branch"], row["index"]])
        expected = math.address(tpub, [row["branch"], row["index"]])
        check(row["path_from_parent"] + ": independent address matches actual Core", row["address"] == expected)
        check(row["path_from_parent"] + ": compressed public key matches", row["compressed_public_key_hex"] == math.decode_extended(key)[45:78].hex())
        check(row["path_from_parent"] + ": only regtest witness candidates", expected.startswith("rltc1q"))
        check(row["path_from_parent"] + ": exact path label", row["path_from_parent"] == "P/" + str(row["branch"]) + "/" + str(row["index"]))
        check(row["path_from_parent"] + ": CSV matches JSON", all(str(row[k]) == csv_row[k] for k in row))
    check("Fifteen unique declared candidates", len({row["address"] for row in rows}) == 15)
    branch_rows = result["branch_only_same_addresses"]
    for index in range(10):
        check("Receiving branch-only derivation " + str(index), math.address(branch0, [index]) == rows[index]["address"] == branch_rows[index])
    controls = result["nested_controls"]
    csv_controls = list(csv.DictReader(io.StringIO((folder / "nested-controls.csv").read_text(encoding="utf-8"))))
    check("Five bounded nested controls", len(controls) == len(csv_controls) == 5)
    for row, csv_row in zip(controls, csv_controls):
        index = row["index"]
        check("Nested descendant independently derived", math.address(branch0, [1, index]) == math.address(tpub, [0, 1, index]) == row["nested_address"])
        check("Sibling change independently derived", math.address(tpub, [1, index]) == row["sibling_change_address"])
        check("Nested is not sibling for this fixture", row["nested_address"] != row["sibling_change_address"] and row["different"] is True)
        check("Nested CSV matches JSON", all(str(row[k]) == csv_row[k] for k in row))
    script = result["script_comparison"]
    check("Same-key witness script matches", math.address(tpub, [0, 0]) == script["wpkh_address"])
    check("Same-key legacy script matches and differs", math.address(tpub, [0, 0], "pkh") == script["pkh_address"] != script["wpkh_address"])
    check("Same compressed key in script comparison", script["compressed_public_key_hex"] == rows[0]["compressed_public_key_hex"])
    hard = result["hardened_attempt"]
    check("Actual hardened deriveaddresses rejection", hard["failed_method"] == "deriveaddresses" and hard["response"]["code"] == -5 and "Cannot derive script without private keys" in hard["response"]["message"])
    reject("Public hardened index rejected independently", lambda: math.child(tpub, 0x80000000))
    reject("Boolean not silently accepted as child index", lambda: math.child(tpub, True))
    broken = tpub[:-1] + ("1" if tpub[-1] != "1" else "2")
    reject("Altered extended-key checksum rejected", lambda: math.decode_extended(broken))
    reject("Private extended version refused", lambda: math.encode_extended(bytes.fromhex("04358394") + math.decode_extended(tpub)[4:]))
    public = bytes.fromhex(rows[0]["compressed_public_key_hex"])
    check("Wrong network gives different address", math.witness_address(public, "ltc") != rows[0]["address"])
    changed = bytearray(math.decode_extended(tpub))
    changed[13] ^= 1
    other = math.encode_extended(bytes(changed))
    check("Different chain code does not preserve descendant", math.address(other, [0, 0]) != rows[0]["address"])
    for descriptor in result["descriptors"].values():
        check("Actual descriptor reported no private keys", descriptor["info"]["hasprivatekeys"] is False)
    check("Declared summary counts", result["summary"] == {"receiving_count": 10, "change_count": 5, "core_math_matches": 15, "branch_only_matches": 10, "nested_not_sibling_count": 5, "descriptor_private_keys_present": False})
    calls = json.loads((folder / "rpc-trace.json").read_text(encoding="utf-8"))["calls"]
    check("No wallet, seed, private key, import or payment RPC", not any(row["method"] in {"createwallet", "loadwallet", "importaddress", "importprivkey", "dumpprivkey", "dumpwallet", "getnewaddress", "walletprocesspsbt", "sendtoaddress", "sendrawtransaction", "signmessage"} for row in calls))
    check("Recorded final chain height0", all(row["result"] == 0 for row in calls if row["method"] == "getblockcount"))
    check("Recorded peers always0", all(row["result"] == 0 for row in calls if row["method"] == "getconnectioncount"))
    check("Recorded empty mempool", all(row["result"] == [] for row in calls if row["method"] == "getrawmempool"))
    check("All57 actual assertions passed", result["assertions_passed"] == len(result["assertions"]) == 57 and all(row["passed"] is True for row in result["assertions"]))
    check("Recorded clean shutdown and fresh-directory removal", result["cleanup"] == {"own_node_stopped": True, "fresh_directory_removed": True, "exit_code": 0})
    print(json.dumps({"success": True, "checks_passed": CHECKS, "core_math_matches": 15, "branch_only_matches": 10, "nested_controls": 5, "optional_publisher_files": len(seen & OPTIONAL_PUBLIC), "scope": "offline public-only frozen fixture verification"}))


if __name__ == "__main__":
    main(Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parent)
