"""Pinned Core pruning configuration boundaries, not a blockchain-size/pruning benchmark.
Python3 standard library. New disposable regtest datadirs; no peers, wallet, mining or real funds.
Usage: python reproduce.py.txt /path/litecoind /path/litecoin-cli NEW-output-directory
"""
from datetime import datetime,timezone
from pathlib import Path
import csv,hashlib,json,re,shutil,socket,subprocess,sys,tempfile,time

PINS={"litecoind":"63fb440cdc5e2e7d46144b8e3a47dab4725fa9c056a77b8b12b11854504f4838",
      "litecoin-cli":"d185011cc0dbb5d2dd20611e9dc060dbb0a5d9be31a03f57da5dbd3dad4c3769"}
def utc():return datetime.now(timezone.utc).isoformat()
def write(path,value):path.write_text(json.dumps(value,indent=2,ensure_ascii=False)+"\n",encoding="utf-8")

def main(daemon,cli,output):
    output=output.resolve()
    if output.exists():raise ValueError("Use a new output directory; existing results are never overwritten.")
    output.mkdir(parents=True);began=utc();started=time.monotonic();deadline=started+100
    trace=[];assertions=[];cases=[];lifecycle=[];proc=None;log=None;root=None;used_ports=set()
    def check(name,value,details=None):
        assertions.append({"name":name,"passed":bool(value),"details":details})
        if not value:raise AssertionError(name)
    hashes={};versions={}
    for name,binary in [("litecoind",daemon),("litecoin-cli",cli)]:
        hashes[name]=hashlib.sha256(binary.read_bytes()).hexdigest();check(name+"matches pinned SHA",hashes[name]==PINS[name])
        done=subprocess.run([str(binary),"-version"],capture_output=True,encoding="utf-8",timeout=8)
        check(name+"version command succeeded",done.returncode==0);versions[name]=done.stdout.splitlines()[0]
        check(name+"version0.21.5.8","0.21.5.8"in versions[name])
    root=Path(tempfile.mkdtemp(prefix="node-",dir=output.parent)).resolve()
    common=["-regtest","-connect=0","-listen=0","-dnsseed=0","-dns=0","-discover=0","-listenonion=0","-upnp=0",
            "-onlynet=ipv4","-disablewallet=1","-dbcache=32","-par=1","-rpcbind=127.0.0.1","-rpcallowip=127.0.0.1"]
    definitions=[
      {"id":"P01","prune":0,"txindex":0,"expect_started":True},
      {"id":"P02","prune":549,"txindex":0,"expect_started":False},
      {"id":"P03","prune":550,"txindex":0,"expect_started":True},
      {"id":"P04","prune":1,"txindex":0,"expect_started":True},
      {"id":"P05","prune":550,"txindex":1,"expect_started":False}]
    try:
        helpdir=root/"help";helpdir.mkdir()
        help_run=subprocess.run([str(daemon),"-datadir="+str(helpdir),"-help"],capture_output=True,encoding="utf-8",timeout=8)
        check("Daemon help command succeeded",help_run.returncode==0)
        (output/"daemon-help.txt").write_text(help_run.stdout,encoding="utf-8")
        for definition in definitions:
            if time.monotonic()>deadline:raise TimeoutError("100second configuration experiment budget exceeded.")
            name=definition["id"];datadir=root/name;datadir.mkdir()
            reservation=socket.socket();reservation.bind(("127.0.0.1",0));port=reservation.getsockname()[1]
            while port in used_ports:
                reservation.close();reservation=socket.socket();reservation.bind(("127.0.0.1",0));port=reservation.getsockname()[1]
            used_ports.add(port);reservation.close()
            options=["-prune="+str(definition["prune"]),"-txindex="+str(definition["txindex"])]
            flags=[*common,*options,"-datadir="+str(datadir),"-rpcport="+str(port)]
            base=["-regtest","-datadir="+str(datadir),"-rpcport="+str(port)]
            logpath=root/(name+"-process.log");log=logpath.open("wb");case_start=utc()
            proc=subprocess.Popen([str(daemon),*flags],stdout=log,stderr=log)
            lifecycle.append({"case":name,"event":"started_process","time_utc":case_start,"pid":proc.pid,"configuration":options})
            def raw_cli(method,*args):
                if time.monotonic()>deadline and method!="stop":raise TimeoutError("Experiment budget exceeded.")
                params=[str(v)for v in args]
                return subprocess.run([str(cli),*base,method,*params],capture_output=True,encoding="utf-8",timeout=4)
            def rpc(method,*args):
                done=raw_cli(method,*args)
                if done.returncode:raise RuntimeError(method+": "+done.stderr.strip())
                try:value=json.loads(done.stdout)if done.stdout.strip()else None
                except json.JSONDecodeError:value=done.stdout.strip()
                trace.append({"case":name,"time_utc":utc(),"method":method,"params":[str(v)for v in args],"result":value})
                return value
            ready=None;last_poll_error="";startup_deadline=min(deadline,time.monotonic()+15)
            while time.monotonic()<startup_deadline and proc.poll()is None:
                done=raw_cli("getblockchaininfo")
                if done.returncode==0:
                    ready=json.loads(done.stdout);trace.append({"case":name,"time_utc":utc(),"method":"getblockchaininfo","params":[],"result":ready});break
                last_poll_error=done.stderr.strip()
                time.sleep(.08)
            observed_started=ready is not None
            check(name+"observed expected startup boundary",observed_started==definition["expect_started"])
            row={**definition,"started_at_utc":case_start,"startup":"started"if observed_started else"rejected",
                 "rpc_port":port,"blockchain_info":ready,"pruned":None,"automatic_pruning":None,"prune_target_size":None,"diagnostic_lines":[]}
            if observed_started:
                check(name+"regtest chain",ready["chain"]=="regtest")
                check(name+"no generated/downloaded blocks",ready["blocks"]==ready["headers"]==0)
                net=rpc("getnetworkinfo");check(name+"RPC version0.21.5.8","0.21.5.8"in net["subversion"])
                check(name+"zero peers",rpc("getconnectioncount")==0);check(name+"empty peer metadata",rpc("getpeerinfo")==[])
                row["pruned"]=ready["pruned"];row["automatic_pruning"]=ready.get("automatic_pruning");row["prune_target_size"]=ready.get("prune_target_size")
                if name=="P01":
                    check("Prune0 reports prunedfalse",ready["pruned"]is False)
                    check("Prune0 has no pruning target","prune_target_size"not in ready)
                if name=="P03":
                    check("Prune550 reports prunedtrue",ready["pruned"]is True)
                    check("Prune550 automatictrue",ready.get("automatic_pruning")is True)
                    check("Prune550 exact binary target",ready.get("prune_target_size")==550*1024*1024)
                    for method in ["getblockchaininfo","pruneblockchain"]:
                        (output/("rpc-help-"+method+".txt")).write_text(rpc("help",method)+"\n",encoding="utf-8")
                if name=="P04":
                    check("Prune1 reports prunedtrue",ready["pruned"]is True)
                    check("Prune1 automaticfalse",ready.get("automatic_pruning")is False)
                    check("Prune1 has no automatic target","prune_target_size"not in ready)
                check(name+"prune height at untouched genesis",not ready["pruned"]or ready.get("pruneheight")==0)
                rpc("stop");proc.wait(timeout=8);check(name+"clean exit0",proc.returncode==0)
            else:
                if proc.poll()is None:raise TimeoutError(name+" neither started nor exited in15seconds: "+last_poll_error[:100])
                check(name+"startup rejection has nonzero exit",proc.returncode!=0)
            log.close();log=None
            text=logpath.read_text(encoding="utf-8",errors="replace")
            row["diagnostic_lines"]=[line for line in text.splitlines()if "Error:"in line]
            if name=="P02":
                check("Prune549 actual minimum error",any("minimum of 550 MiB"in line for line in row["diagnostic_lines"]))
            if name=="P05":
                check("Prune550 txindex actual incompatibility",any("Prune mode is incompatible with -txindex"in line for line in row["diagnostic_lines"]))
            row["exit_code"]=proc.returncode;row["completed_at_utc"]=utc();cases.append(row)
            lifecycle.append({"case":name,"event":"exited","time_utc":utc(),"pid":proc.pid,"exit_code":proc.returncode})
            trace.append({"case":name,"time_utc":utc(),"event":"startup_diagnostics","process_exit_code":proc.returncode,"diagnostic_lines":row["diagnostic_lines"]})
            proc=None
        result={"schema":1,"version":"1.0.0","started_at_utc":began,"completed_at_utc":utc(),"elapsed_seconds":round(time.monotonic()-started,3),
                "network":"regtest","platform":sys.platform,"core_versions":versions,"binary_sha256":hashes,"node_options":common,
                "scope":"Five fresh sequential configuration startups, three successful and two rejected. Genesis only, wallet disabled, no mining/peers/mainnet sync. Not actual block pruning, a storage measurement, rescan or wallet restore.",
                "cases":cases,"process_lifecycle":lifecycle,"assertions_passed":len(assertions),"assertions":assertions}
        write(output/"results.json",result);write(output/"rpc-trace.json",{"scope":"RPC results from new isolated regtest nodes plus curated raw startup Error lines. Startup polling failures omitted; no wallet/auth/key commands.","events":trace})
        with(output/"cases.csv").open("w",encoding="utf-8",newline="")as h:
            cols=["case_id","prune_argument","txindex_argument","startup","pruned","automatic_pruning","prune_target_size","block_height","exit_code","diagnostic"]
            writer=csv.DictWriter(h,fieldnames=cols);writer.writeheader()
            for case in cases:writer.writerow(dict(zip(cols,[case["id"],case["prune"],case["txindex"],case["startup"],case["pruned"],case["automatic_pruning"],case["prune_target_size"],case["blockchain_info"]["blocks"]if case["blockchain_info"]else"",case["exit_code"]," | ".join(case["diagnostic_lines"])])))
        print(json.dumps({"ok":True,"assertions":len(assertions),"elapsed_seconds":result["elapsed_seconds"],"cases":len(cases)}))
    except BaseException:
        write(output/"failed-results.json",{"assertions":assertions,"cases":cases,"lifecycle":lifecycle});write(output/"failed-trace.json",trace);raise
    finally:
        if proc and proc.poll()is None:
            try:
                subprocess.run([str(cli),*base,"stop"],capture_output=True,timeout=4)
                proc.wait(timeout=5)
            except Exception:
                proc.terminate()
                try:proc.wait(timeout=4)
                except subprocess.TimeoutExpired:proc.kill();proc.wait(timeout=4)
        if log:log.close()
        if root and root.is_dir():
            if not(root.is_relative_to(output.parent.resolve())and root.name.startswith("node-")):raise RuntimeError("Unsafe fresh-directory cleanup target")
            if proc and proc.poll()is None:raise RuntimeError("Refusing cleanup while owned node still running")
            shutil.rmtree(root)
if __name__=="__main__":
    if len(sys.argv)!=4:raise SystemExit("Usage: python reproduce.py.txt /path/litecoind /path/litecoin-cli NEW-output-directory")
    main(Path(sys.argv[1]).resolve(),Path(sys.argv[2]).resolve(),Path(sys.argv[3]))
