"""Fresh wallet-disabled Litecoin Core regtest public-derivation experiment.

Python3 standard library. Usage: python reproduce.py.txt litecoind litecoin-cli output
Uses only a standardized PUBLIC fixture. Never accepts wallet, seed or private key.
"""
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
import csv
import hashlib
import importlib.machinery
import importlib.util
import json
import re
import shutil
import signal
import socket
import subprocess
import sys
import tempfile

PINS = {"litecoind": "63fb440cdc5e2e7d46144b8e3a47dab4725fa9c056a77b8b12b11854504f4838", "litecoin-cli": "d185011cc0dbb5d2dd20611e9dc060dbb0a5d9be31a03f57da5dbd3dad4c3769"}
loader = importlib.machinery.SourceFileLoader("public_derivation", str(Path(__file__).with_name("public-derivation.py.txt")))
math = importlib.util.module_from_spec(importlib.util.spec_from_loader(loader.name, loader))
loader.exec_module(math)


def utc():
    return datetime.now(timezone.utc).isoformat()


def serial(value):
    if isinstance(value, Decimal):
        return format(value, "f")
    if isinstance(value, dict):
        return {key: serial(item) for key, item in value.items()}
    if isinstance(value, (list, tuple)):
        return [serial(item) for item in value]
    return value


def write(path, value):
    path.write_text(json.dumps(serial(value), indent=2, ensure_ascii=False) + "\n", encoding="utf-8")


def main(daemon, cli, output):
    started = utc()
    hashes = {name: hashlib.sha256(path.read_bytes()).hexdigest() for name, path in (("litecoind", daemon), ("litecoin-cli", cli))}
    if hashes != PINS:
        raise RuntimeError("Pinned binary SHA mismatch")
    output.mkdir(parents=True, exist_ok=True)
    datadir = Path(tempfile.mkdtemp(prefix="lw-xpub-public-regtest-")).resolve()
    temp_parent = Path(tempfile.gettempdir()).resolve()
    if datadir.parent != temp_parent or not datadir.name.startswith("lw-xpub-public-regtest-"):
        raise RuntimeError("Fresh directory guard failed")
    reservation = socket.socket()
    reservation.bind(("127.0.0.1", 0))
    port = reservation.getsockname()[1]
    flags = ["-regtest", "-server=1", "-disablewallet=1", "-listen=0", "-connect=0", "-dnsseed=0", "-dns=0", "-discover=0", "-listenonion=0", "-upnp=0", "-dbcache=32", "-par=1", "-maxmempool=5", "-persistmempool=0", "-rpcbind=127.0.0.1", "-rpcallowip=127.0.0.1"]
    base = [str(cli), "-regtest", "-datadir=" + str(datadir), "-rpcconnect=127.0.0.1", "-rpcport=" + str(port)]
    trace, assertions, descriptors = [], [], {}
    process = logfile = result = None
    cleanup = {"own_node_stopped": False, "fresh_directory_removed": False}

    def deadline(signum, frame):
        raise TimeoutError("75-second experiment work budget exceeded")

    signal.signal(signal.SIGALRM, deadline)
    signal.signal(signal.SIGTERM, deadline)
    signal.alarm(75)

    def check(name, condition):
        assertions.append({"name": name, "passed": bool(condition)})
        if not condition:
            raise AssertionError(name)

    def rpc(method, *args, allow_error=False, wait=False):
        params = [json.dumps(value, separators=(",", ":")) if isinstance(value, (list, dict, bool)) else str(value) for value in args]
        command = base + (["-rpcwait"] if wait else []) + [method, *params]
        response = subprocess.run(command, capture_output=True, text=True, timeout=12)
        call = {"at_utc": utc(), "method": method, "params": params, "return_code": response.returncode}
        if response.returncode:
            error = response.stderr.strip()
            number = re.search(r"error code:\s*(-?\d+)", error)
            value = {"kind": "rpc_error", "code": int(number.group(1)) if number else None, "message": error}
            call["error"] = value
            trace.append(call)
            if allow_error:
                return value
            raise RuntimeError(method + ": " + error)
        try:
            value = json.loads(response.stdout, parse_float=Decimal) if response.stdout.strip() else None
        except json.JSONDecodeError:
            value = response.stdout.strip()
        call["result"] = serial(value)
        trace.append(call)
        return value

    def core_addresses(name, raw, count=None):
        info = rpc("getdescriptorinfo", raw)
        check(name + ": no private key in actual descriptor info", info["hasprivatekeys"] is False)
        addresses = rpc("deriveaddresses", info["descriptor"], [0, count - 1]) if count is not None else rpc("deriveaddresses", info["descriptor"])
        descriptors[name] = {"input": raw, "info": info, "addresses": addresses}
        return addresses

    try:
        reservation.close()
        logfile = (datadir / "process.log").open("ab")
        process = subprocess.Popen([str(daemon), *flags, "-datadir=" + str(datadir), "-rpcport=" + str(port)], stdout=logfile, stderr=logfile)
        chain = rpc("getblockchaininfo", wait=True)
        check("Fresh regtest height0", chain["chain"] == "regtest" and chain["blocks"] == 0)
        version = rpc("getnetworkinfo")["subversion"]
        check("Pinned runtime0.21.5.8", version == "/LitecoinCore:0.21.5.8/")
        rpc("setnetworkactive", False)
        check("Zero peers", rpc("getconnectioncount") == 0)
        check("Networking disabled", rpc("getnetworkinfo")["networkactive"] is False)
        help_text = {method: rpc("help", method) for method in ("getdescriptorinfo", "deriveaddresses")}
        parent_xpub = math.FIXTURE_PARENT
        check("Public derivation matches published BIP32 next child", math.child(parent_xpub, 1) == math.FIXTURE_CHILD_ONE)
        parent = math.as_tpub(parent_xpub)
        check("Version conversion preserves remaining74payload bytes", math.decode_extended(parent)[4:] == math.decode_extended(parent_xpub)[4:])
        branch0 = math.child(parent, 0)
        receive = core_addresses("parent_receiving", "wpkh(" + parent + "/0/*)", 10)
        change = core_addresses("parent_change", "wpkh(" + parent + "/1/*)", 5)
        branch_receive = core_addresses("receiving_branch_only", "wpkh(" + branch0 + "/*)", 10)
        nested = core_addresses("nested_branch_is_not_sibling_change", "wpkh(" + branch0 + "/1/*)", 5)
        legacy = core_addresses("same_key_different_script", "pkh(" + parent + "/0/0)")[0]
        rows = []
        for branch, values in ((0, receive), (1, change)):
            for index, actual in enumerate(values):
                expected = math.address(parent, [branch, index])
                child = math.derive(parent, [branch, index])
                check("Core vs independent wpkh P/" + str(branch) + "/" + str(index), actual == expected)
                check("Only regtest witness addresses", actual.startswith("rltc1q"))
                rows.append({"role_in_fixture": "receiving" if branch == 0 else "change", "path_from_parent": "P/" + str(branch) + "/" + str(index), "branch": branch, "index": index, "address": actual, "compressed_public_key_hex": math.decode_extended(child)[45:78].hex(), "independent_match": actual == expected})
        check("Ten receiving addresses agree with branch-only descriptor", receive == branch_receive)
        controls = []
        for index, actual in enumerate(nested):
            check("Nested branch result independently matches", actual == math.address(parent, [0, 1, index]))
            check("Nested branch is different from sibling change", actual != change[index])
            controls.append({"index": index, "nested_path": "P/0/1/" + str(index), "nested_address": actual, "sibling_change_path": "P/1/" + str(index), "sibling_change_address": change[index], "different": actual != change[index]})
        hardened_raw = "wpkh(" + parent + "/0h/*)"
        hardened_info = rpc("getdescriptorinfo", hardened_raw, allow_error=True)
        if isinstance(hardened_info, dict) and hardened_info.get("kind") == "rpc_error":
            hardened = {"failed_method": "getdescriptorinfo", "descriptor": hardened_raw, "response": hardened_info}
        else:
            hardened = {"failed_method": "deriveaddresses", "descriptor": hardened_info["descriptor"], "response": rpc("deriveaddresses", hardened_info["descriptor"], [0, 0], allow_error=True)}
        check("Hardened public derivation actual error", isinstance(hardened["response"], dict) and hardened["response"].get("kind") == "rpc_error")
        check("Same public key gives distinct script addresses", legacy != receive[0] and legacy == math.address(parent, [0, 0], "pkh"))
        check("Descriptor output count matches declared bounded ranges", len(receive) == len(branch_receive) == 10 and len(change) == len(nested) == 5)
        check("All15 declared receiving/change outputs unique", len(set(receive + change)) == 15)
        check("No blocks, transactions or peers added", rpc("getblockcount") == 0 and rpc("getrawmempool") == [] and rpc("getconnectioncount") == 0)
        result = {"schema": 1, "version": "1.0.0", "started_at_utc": started, "completed_at_utc": utc(), "network": "regtest", "core_version": version, "binary_sha256": hashes, "node_options": flags,
                  "scope": "Public-only BIP32 standardized fixture and declared synthetic wpkh descriptor tree; not an exported Litecoin wallet/account, universal BIP44 layout, history/balance scan or private-key test.",
                  "fixture": {"source": "BIP32 test vector1, chain m/0 hardened", "public_parent_xpub": parent_xpub, "public_parent_tpub": parent, "published_next_child_xpub": math.FIXTURE_CHILD_ONE, "receiving_branch0_tpub": branch0},
                  "addresses": rows, "branch_only_same_addresses": branch_receive, "nested_controls": controls, "hardened_attempt": hardened, "script_comparison": {"path": "P/0/0", "compressed_public_key_hex": rows[0]["compressed_public_key_hex"], "wpkh_address": receive[0], "pkh_address": legacy}, "descriptors": descriptors,
                  "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},
                  "assertions_passed": len(assertions), "assertions": assertions}
        write(output / "rpc-help.json", help_text)
    except Exception:
        write(output / "failed-assertions.json", assertions)
        write(output / "failed-rpc-trace.json", {"calls": trace})
        raise
    finally:
        signal.alarm(0)
        if process is not None and process.poll() is None:
            try:
                rpc("stop")
                process.wait(timeout=5)
            except Exception:
                process.terminate()
                try:
                    process.wait(timeout=3)
                except subprocess.TimeoutExpired:
                    process.kill()
                    process.wait(timeout=2)
        cleanup["own_node_stopped"] = process is None or process.poll() is not None
        cleanup["exit_code"] = process.returncode if process else None
        if logfile:
            logfile.close()
        reservation.close()
        if cleanup["own_node_stopped"] and datadir.parent == temp_parent and datadir.name.startswith("lw-xpub-public-regtest-"):
            shutil.rmtree(datadir)
            cleanup["fresh_directory_removed"] = not datadir.exists()
        write(output / "cleanup.json", cleanup)
    if result is None:
        raise RuntimeError("No completed experiment")
    result["cleanup"] = cleanup
    result["completed_at_utc"] = utc()
    write(output / "results.json", result)
    write(output / "rpc-trace.json", {"scope": "Standardized PUBLIC fixture extended keys, regtest public descriptors and derived addresses only. No wallet or private-key RPC.", "calls": trace})
    for name, values in (("addresses.csv", result["addresses"]), ("nested-controls.csv", result["nested_controls"])):
        with (output / name).open("w", encoding="utf-8", newline="") as handle:
            writer = csv.DictWriter(handle, fieldnames=list(values[0]))
            writer.writeheader()
            writer.writerows(values)
    print(json.dumps({"success": True, "assertions_passed": len(assertions), "summary": result["summary"], "hardened_error": result["hardened_attempt"], "first_receiving": result["addresses"][0], "cleanup": cleanup}))


if __name__ == "__main__":
    if len(sys.argv) != 4:
        raise SystemExit("Usage: python reproduce.py.txt litecoind litecoin-cli output-directory")
    main(Path(sys.argv[1]).resolve(), Path(sys.argv[2]).resolve(), Path(sys.argv[3]).resolve())
