"""Fresh zero-funded Litecoin Core regtest message-signing experiment.

Python3 standard library. Usage: python reproduce.py.txt litecoind litecoin-cli output
No existing wallet, datadir, config, private key or RPC credential is accepted.
"""
from datetime import datetime, timezone, timedelta
from decimal import Decimal
from pathlib import Path
import base64
import csv
import hashlib
import json
import re
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import unicodedata

PINS = {"litecoind": "63fb440cdc5e2e7d46144b8e3a47dab4725fa9c056a77b8b12b11854504f4838", "litecoin-cli": "d185011cc0dbb5d2dd20611e9dc060dbb0a5d9be31a03f57da5dbd3dad4c3769"}
NONCE = hashlib.sha256(b"Litecoin.watch message-signing fixture v1 nonce1").hexdigest()
NEXT_NONCE = hashlib.sha256(b"Litecoin.watch message-signing fixture v1 nonce2").hexdigest()


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), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def challenge_message(fields):
    return "\n".join(["Litecoin.watch message-signing demonstration", "Site: " + fields["site"], "Purpose: " + fields["purpose"], "Address: " + fields["address"], "Nonce: " + fields["nonce"], "Expires UTC: " + fields["expires_utc"]])


def application_gate(case):
    """Explicitly modeled application rule, NOT a Litecoin Core feature."""
    fields, expected = case["challenge"], case["expected_context"]
    if case["cryptographic_valid"] is not True:
        return "reject_signature"
    if any(fields[key] != expected[key] for key in ("site", "purpose", "address", "nonce")):
        return "reject_context"
    expiry = datetime.fromisoformat(fields["expires_utc"].replace("Z", "+00:00"))
    now = datetime.fromisoformat(case["checked_at_utc"].replace("Z", "+00:00"))
    if now >= expiry:
        return "reject_expired"
    if fields["nonce"] in case["used_nonces"]:
        return "reject_replay"
    return "accept_once"


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("Binary bytes do not match the pinned release.")
    output.mkdir(parents=True, exist_ok=True)
    datadir = Path(tempfile.mkdtemp(prefix="lw-message-signing-regtest-")).resolve()
    temp_parent = Path(tempfile.gettempdir()).resolve()
    if datadir.parent != temp_parent or not datadir.name.startswith("lw-message-signing-regtest-"):
        raise RuntimeError("Fresh-directory guard failed.")
    socket_reservation = socket.socket()
    socket_reservation.bind(("127.0.0.1", 0))
    port = socket_reservation.getsockname()[1]
    flags = ["-regtest", "-server=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)]
    process = logfile = result = None
    trace, assertions, cases = [], [], []
    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, passed):
        assertions.append({"name": name, "passed": bool(passed)})
        if not passed:
            raise AssertionError(name)

    def rpc(method, *args, wallet=None, allow_error=False, wait=False):
        command = base + (["-rpcwait"] if wait else []) + (["-rpcwallet=" + wallet] if wallet else [])
        params = [json.dumps(value, ensure_ascii=False, separators=(",", ":")) if isinstance(value, (bool, list, dict)) else str(value) for value in args]
        response = subprocess.run(command + [method, *params], capture_output=True, text=True, encoding="utf-8", timeout=12)
        call = {"at_utc": utc(), "method": method, "wallet": wallet, "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 verify_case(case_id, name, address, signature, message, expected):
        observed = rpc("verifymessage", address, signature, message)
        check(case_id + ": actual verifier result", observed is expected)
        cases.append({"case_id": case_id, "name": name, "operation": "verifymessage", "address": address, "message": message, "message_utf8_hex": message.encode("utf-8").hex(), "signature_base64": signature, "observed": observed})

    try:
        socket_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("Core0.21.5.8 runtime", version == "/LitecoinCore:0.21.5.8/")
        rpc("setnetworkactive", False)
        check("No peers", rpc("getconnectioncount") == 0)
        check("Peer network disabled", rpc("getnetworkinfo")["networkactive"] is False)
        rpc("createwallet", "signer")
        rpc("createwallet", "watch-only", True)
        help_text = {method: rpc("help", method) for method in ("signmessage", "verifymessage", "importaddress", "getbalance")}
        address = rpc("getnewaddress", "zero-funded-message-key", "legacy", wallet="signer")
        wrong_address = rpc("getnewaddress", "other-zero-funded-key", "legacy", wallet="signer")
        segwit = rpc("getnewaddress", "ordinary-segwit", "bech32", wallet="signer")
        check("Signer starts with zero balance", rpc("getbalance", wallet="signer") == 0)
        fields = {"site": "litecoin.watch", "purpose": "demonstrate address control, not authorize a payment", "address": address, "nonce": NONCE, "expires_utc": "2026-10-15T00:00:00Z"}
        message = challenge_message(fields)
        signature = rpc("signmessage", address, message, wallet="signer")
        check("Signature is65-byte compact encoding", len(base64.b64decode(signature, validate=True)) == 65)
        verify_case("S01", "Exact legacy address/message/signature", address, signature, message, True)
        verify_case("S02", "Purpose text modified", address, signature, message.replace("demonstrate", "claim"), False)
        verify_case("S03", "Wrong legacy address", wrong_address, signature, message, False)
        verify_case("S04", "Identical triple replayed", address, signature, message, True)
        verify_case("S05", "Fresh nonce substituted without new signature", address, signature, message.replace(NONCE, NEXT_NONCE), False)
        verify_case("S06", "Trailing newline appended", address, signature, message + "\n", False)
        verify_case("S07", "Line endings changed from LF toCRLF", address, signature, message.replace("\n", "\r\n"), False)
        verify_case("S08", "Trailing space appended", address, signature, message + " ", False)
        unicode_message = message + "\nNote: café — zażółć"
        unicode_signature = rpc("signmessage", address, unicode_message, wallet="signer")
        verify_case("S09", "Exact UTF8 text", address, unicode_signature, unicode_message, True)
        unicode_changed = unicodedata.normalize("NFD", unicode_message)
        check("Unicode normalization changes bytes", unicode_changed.encode("utf-8") != unicode_message.encode("utf-8"))
        verify_case("S10", "Visually similar Unicode normalized toNFD", address, unicode_signature, unicode_changed, False)
        unsupported = rpc("signmessage", segwit, message, wallet="signer", allow_error=True)
        check("Native SegWit sign operation returns actual error", isinstance(unsupported, dict) and unsupported.get("kind") == "rpc_error")
        rpc("importaddress", address, "public-address-only", False, wallet="watch-only")
        watch_info = rpc("getaddressinfo", address, wallet="watch-only")
        check("Watch-only wallet knows public address", watch_info["iswatchonly"] is True)
        watch_signature = rpc("signmessage", address, message, wallet="watch-only", allow_error=True)
        check("Public-address-only wallet returns actual signing error", isinstance(watch_signature, dict) and watch_signature.get("kind") == "rpc_error")
        check("Watch-only wallet has private keys disabled", rpc("getwalletinfo", wallet="watch-only")["private_keys_enabled"] is False)
        check("Signer still has zero balance", rpc("getbalance", wallet="signer") == 0)
        check("No blockchain transaction created", rpc("getrawmempool") == [] and rpc("getblockcount") == 0 and rpc("listtransactions", "*", 100, wallet="signer") == [])
        check("Still zero peers at end", rpc("getconnectioncount") == 0)
        checked = datetime.now(timezone.utc)
        future_fields = {**fields, "nonce": NEXT_NONCE, "expires_utc": (checked + timedelta(minutes=10)).strftime("%Y-%m-%dT%H:%M:%SZ")}
        future_message = challenge_message(future_fields)
        future_signature = rpc("signmessage", address, future_message, wallet="signer")
        verify_case("S11", "New readable challenge signed separately", address, future_signature, future_message, True)
        check("Original fixture deadline already passed", checked >= datetime.fromisoformat(fields["expires_utc"].replace("Z", "+00:00")))
        context = {key: future_fields[key] for key in ("site", "purpose", "address", "nonce")}
        base_case = {"challenge": future_fields, "expected_context": context, "checked_at_utc": checked.isoformat(), "used_nonces": [], "cryptographic_valid": True, "evidence_kind": "modeled application policy, not Core or a real service"}
        app_cases = [
            {**base_case, "case_id": "M01", "name": "Fresh expected challenge once", "expected": "accept_once"},
            {**base_case, "case_id": "M02", "name": "Same valid signature after nonce used", "used_nonces": [NEXT_NONCE], "expected": "reject_replay"},
            {**base_case, "case_id": "M03", "name": "Old challenge with passed deadline", "challenge": fields, "expected_context": {key: fields[key] for key in context}, "expected": "reject_expired"},
            {**base_case, "case_id": "M04", "name": "Signature valid for different expected site", "expected_context": {**context, "site": "different.example"}, "expected": "reject_context"},
            {**base_case, "case_id": "M05", "name": "Expected nonce differs", "expected_context": {**context, "nonce": NONCE}, "expected": "reject_context"},
            {**base_case, "case_id": "M06", "name": "Cryptographic check failed", "cryptographic_valid": False, "expected": "reject_signature"},
        ]
        for item in app_cases:
            item["observed_model_result"] = application_gate(item)
            check(item["case_id"] + ": deterministic application model", item["observed_model_result"] == item["expected"])
        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, "work_budget_seconds": 75,
                  "scope": "Fresh zero-funded signer and watch-only wallets on one peer-disabled node; legacy compact signed messages only. No mainnet/account/login/reserve/identity/transaction-signing test.",
                  "legacy_address": address, "wrong_legacy_address": wrong_address, "ordinary_segwit_address": segwit, "original_challenge": fields, "original_message": message, "original_signature_base64": signature,
                  "cases": cases, "unsupported_signing": {"ordinary_native_segwit": unsupported, "watch_only_legacy": watch_signature},
                  "balances_ltc": {"signer_before": "0.00000000", "signer_after": "0.00000000", "watch_only_after": serial(rpc("getbalance", wallet="watch-only"))},
                  "application_model_cases": app_cases, "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()
        socket_reservation.close()
        if cleanup["own_node_stopped"] and datadir.parent == temp_parent and datadir.name.startswith("lw-message-signing-regtest-"):
            shutil.rmtree(datadir)
            cleanup["fresh_directory_removed"] = not datadir.exists()
        write(output / "cleanup.json", cleanup)
    if result is None:
        raise RuntimeError("No complete experiment result")
    result["cleanup"] = cleanup
    result["completed_at_utc"] = utc()
    write(output / "results.json", result)
    write(output / "rpc-trace.json", {"scope": "Public zero-funded regtest fixture addresses, signatures, readable messages and wallet metadata only; no secret material.", "calls": trace})
    write(output / "application-model.json", result["application_model_cases"])
    (output / "challenge.txt").write_text(result["original_message"], encoding="utf-8")
    with (output / "cases.csv").open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=["case_id", "name", "operation", "address", "observed", "message_utf8_sha256"])
        writer.writeheader()
        for case in cases:
            writer.writerow({**{key: case[key] for key in ("case_id", "name", "operation", "address", "observed")}, "message_utf8_sha256": hashlib.sha256(case["message"].encode("utf-8")).hexdigest()})
    print(json.dumps({"success": True, "assertions_passed": len(assertions), "verifier_cases": len(cases), "unsupported_signing": result["unsupported_signing"], "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())
