"""Four ordinary regtest sends: wallet-local comments/labels versus serialized transactions.
Python3 standard library; pass two verified Core0.21.5.8 binaries and a NEW output directory.
No existing node, configuration, wallet, keys, seed or RPC credential is accepted.
"""
from datetime import datetime,timezone
from decimal import Decimal
from pathlib import Path
import csv,hashlib,json,shutil,socket,subprocess,sys,tempfile,time

PINS={"litecoind":"63fb440cdc5e2e7d46144b8e3a47dab4725fa9c056a77b8b12b11854504f4838",
      "litecoin-cli":"d185011cc0dbb5d2dd20611e9dc060dbb0a5d9be31a03f57da5dbd3dad4c3769"}
def utc():return datetime.now(timezone.utc).isoformat()
def serial(value):
    if isinstance(value,Decimal):return format(value,"f")
    if isinstance(value,dict):return {k:serial(v)for k,v in value.items()}
    if isinstance(value,list):return [serial(v)for v 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 names(info):return [v.get("name")if isinstance(v,dict)else v for v in info.get("labels",[])]

def main(daemon,cli,output):
    output=output.resolve()
    if output.exists():raise ValueError("Choose a new output directory; existing output is never overwritten.")
    output.mkdir(parents=True)
    began=utc();start=time.monotonic();trace=[];assertions=[];lifecycle=[];cases=[];proc=None;log=None;root=None
    def check(name,value,detail=None):
        assertions.append({"name":name,"passed":bool(value),"details":serial(detail)})
        if not value:raise AssertionError(name)
    versions={};hashes={}
    for name,binary in [("litecoind",daemon),("litecoin-cli",cli)]:
        hashes[name]=hashlib.sha256(binary.read_bytes()).hexdigest()
        check(name+" matches pinned binary",hashes[name]==PINS[name])
        result=subprocess.run([str(binary),"-version"],capture_output=True,encoding="utf-8",timeout=8)
        check(name+" version command succeeds",result.returncode==0)
        versions[name]=result.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()
    data=root/"datadir";data.mkdir();reservation=socket.socket();reservation.bind(("127.0.0.1",0));port=reservation.getsockname()[1]
    flags=["-regtest","-connect=0","-listen=0","-dnsseed=0","-dns=0","-discover=0","-listenonion=0","-upnp=0",
           "-onlynet=ipv4","-dbcache=32","-par=1","-fallbackfee=0.00001","-acceptnonstdtxn=0",
           "-datadir="+str(data),"-rpcport="+str(port),"-rpcbind=127.0.0.1","-rpcallowip=127.0.0.1"]
    base=["-regtest","-datadir="+str(data),"-rpcport="+str(port)]
    def rpc(method,*args,wallet=None,wait=False):
        if time.monotonic()-start>75 and method!="stop":raise TimeoutError("75second experiment budget exhausted.")
        command=[str(cli),*base]
        if wallet:command.append("-rpcwallet="+wallet)
        if wait:command.append("-rpcwait")
        params=[json.dumps(v,separators=(",",":"),ensure_ascii=False)if isinstance(v,(list,dict,bool))else str(v)for v in args]
        result=subprocess.run([*command,method,*params],capture_output=True,encoding="utf-8",timeout=20 if wait else 8)
        entry={"time_utc":utc(),"wallet":wallet,"method":method,"params":params}
        if result.returncode:
            entry["error"]=result.stderr.strip();trace.append(entry);raise RuntimeError(method+": "+entry["error"])
        try:value=json.loads(result.stdout,parse_float=Decimal)if result.stdout.strip()else None
        except json.JSONDecodeError:value=result.stdout.strip()
        if method=="getaddressinfo":
            entry["result"]={k:serial(value[k])for k in ["address","scriptPubKey","ismine","iswitness","witness_version","labels"]if k in value}
            entry["result_scope"]="Selected public fields only; no HD metadata is published."
        else:entry["result"]=serial(value)
        trace.append(entry);return value
    def launch(restart=False):
        nonlocal proc,log
        if not restart:reservation.close()
        log=(root/"process.log").open("ab")
        proc=subprocess.Popen([str(daemon),*flags,*(["-wallet=sender","-wallet=recipient"]if restart else [])],stdout=log,stderr=log)
        rpc("getblockchaininfo",wait=True)
        lifecycle.append({"time_utc":utc(),"event":"restarted"if restart else"started","pid":proc.pid})
        check("Node is regtest after"+("restart"if restart else"first start"),rpc("getblockchaininfo")["chain"]=="regtest")
        check("No peers after"+("restart"if restart else"first start"),rpc("getconnectioncount")==0)
    def stop():
        nonlocal log
        if proc and proc.poll()is None:
            rpc("stop");proc.wait(timeout=8)
            lifecycle.append({"time_utc":utc(),"event":"stopped","pid":proc.pid,"exit_code":proc.returncode})
        if log:log.close();log=None
    def note_state(wallet,txid):
        result=rpc("gettransaction",txid,wallet=wallet)
        return result,{"comment_present":"comment"in result,"comment":result.get("comment"),"to_present":"to"in result,"to":result.get("to")}
    try:
        launch()
        rpc("createwallet","sender");rpc("createwallet","recipient")
        for method in ["sendtoaddress","gettransaction","setlabel"]:
            (output/("rpc-help-"+method+".txt")).write_text(rpc("help",method)+"\n",encoding="utf-8")
        mining=rpc("getnewaddress","","bech32",wallet="sender")
        rpc("generatetoaddress",101,mining)
        check("Initial height101",rpc("getblockcount")==101)
        inputs=[
            {"id":"N01","kind":"empty control","amount":"0.01375","comment":"","comment_to":""},
            {"id":"N02","kind":"ASCII invoice markers","amount":"0.02125","comment":"LW-NOTE-DEMO-2048","comment_to":"LW-TO-DEMO-ACCOUNTS"},
            {"id":"N03","kind":"UTF8 markers","amount":"0.0325","comment":"LW-NOTE: café • 支払済み ✓","comment_to":"LW-TO: Łódź • 店舗 ✓"},
            {"id":"N04","kind":"long repeated markers","amount":"0.0475","comment":"LW-NOTE-ONLY|"*64,"comment_to":"LW-TO-NOT-ON-CHAIN|"*32}
        ]
        for item in inputs:
            address=rpc("getnewaddress","","bech32",wallet="recipient")
            address_info=rpc("getaddressinfo",address,wallet="recipient")
            check(item["id"]+"ordinary native SegWit recipient",address_info["ismine"]is True and address_info["iswitness"]is True and address_info["witness_version"]==0)
            txid=rpc("sendtoaddress",address,item["amount"],item["comment"],item["comment_to"],wallet="sender")
            rpc("generatetoaddress",1,mining)
            sent,sm=note_state("sender",txid);received,rm=note_state("recipient",txid)
            raw=bytes.fromhex(sent["hex"]);decoded=rpc("decoderawtransaction",sent["hex"])
            check(item["id"]+"both wallets return same published bytes",sent["hex"]==received["hex"])
            check(item["id"]+"decoded txid matches",decoded["txid"]==txid)
            check(item["id"]+"receiver has no sender comment",not rm["comment_present"])
            check(item["id"]+"receiver has no sender comment_to",not rm["to_present"])
            for field,key in [("comment","comment"),("comment_to","to")]:
                check(item["id"]+"sender retains"+field,sent.get(key,"")==item[field])
            matches=[v for v in decoded["vout"]if v["scriptPubKey"]["hex"]==address_info["scriptPubKey"]]
            check(item["id"]+"one exact recipient output",len(matches)==1 and matches[0]["value"]==Decimal(item["amount"]))
            check(item["id"]+"ordinary outputs have no OP_RETURN",all(v["scriptPubKey"]["type"]!="nulldata"for v in decoded["vout"]))
            searches={}
            for field in ["comment","comment_to"]:
                text=item[field].encode("utf-8")
                searches[field]={"utf8_bytes":len(text),"full_string_found":text in raw if text else None,
                                 "prefix24_found":text[:24]in raw if text else None}
                if text:
                    check(item["id"]+"raw bytes omit full"+field,text not in raw)
                    check(item["id"]+"raw bytes omit distinctive prefix"+field,text[:24]not in raw)
            record={**item,"recipient_address":address,"recipient_script":address_info["scriptPubKey"],"txid":txid,
                    "sender_metadata_before_restart":sm,"recipient_metadata_before_restart":rm,"searches":searches,
                    "raw_hex":sent["hex"],"decoded":decoded,"recipient_vout":matches[0]["n"],"observed_height":rpc("getblockcount")}
            cases.append(record)
        target=cases[1];slabel="LOCAL-SENDER-CONTACT-2048";rlabel="LOCAL-RECIPIENT-ORDER-2048"
        rpc("setlabel",target["recipient_address"],slabel,wallet="sender")
        rpc("setlabel",target["recipient_address"],rlabel,wallet="recipient")
        label_sender=rpc("getaddressinfo",target["recipient_address"],wallet="sender")
        label_receiver=rpc("getaddressinfo",target["recipient_address"],wallet="recipient")
        check("Each wallet has its own address label",names(label_sender)==[slabel]and names(label_receiver)==[rlabel])
        s_after,sm_after=note_state("sender",target["txid"]);r_after,rm_after=note_state("recipient",target["txid"])
        check("Address relabeling leaves published transaction byte-for-byte unchanged",s_after["hex"]==r_after["hex"]==target["raw_hex"])
        check("Relabeling leaves sender note unchanged",sm_after==target["sender_metadata_before_restart"])
        check("Relabeling does not deliver sender note",rm_after==target["recipient_metadata_before_restart"])
        labels={"case_id":target["id"],"txid":target["txid"],"sender_label":slabel,"recipient_label":rlabel,
                "hex_before":target["raw_hex"],"hex_after":s_after["hex"],"sender_metadata_after":sm_after,"recipient_metadata_after":rm_after}
        check("Height105 before restart",rpc("getblockcount")==105)
        check("Recipient exact total before restart",rpc("getbalance",wallet="recipient")==Decimal("0.115"))
        stop();check("First process cleanly exited",proc.returncode==0)
        launch(restart=True);check("Both fresh wallets loaded after restart",set(rpc("listwallets"))=={"sender","recipient"})
        for item in cases:
            sent,sm=note_state("sender",item["txid"]);received,rm=note_state("recipient",item["txid"])
            item["sender_metadata_after_restart"]=sm;item["recipient_metadata_after_restart"]=rm
            item["sender_confirmations_after_restart"]=sent["confirmations"];item["recipient_confirmations_after_restart"]=received["confirmations"]
            check(item["id"]+"sender metadata survives restart",sm==item["sender_metadata_before_restart"])
            check(item["id"]+"recipient still lacks sender metadata",rm==item["recipient_metadata_before_restart"])
            check(item["id"]+"raw transaction survives restart unchanged",sent["hex"]==received["hex"]==item["raw_hex"])
            expected=105-item["observed_height"]+1
            check(item["id"]+"confirmation depth unchanged",sent["confirmations"]==received["confirmations"]==expected)
            check(item["id"]+"recipient exact received amount",rpc("getreceivedbyaddress",item["recipient_address"],1,wallet="recipient")==Decimal(item["amount"]))
        check("Receiver address label persists locally",names(rpc("getaddressinfo",target["recipient_address"],wallet="recipient"))==[rlabel])
        check("Sender contact label persists separately",names(rpc("getaddressinfo",target["recipient_address"],wallet="sender"))==[slabel])
        balance=rpc("getbalance",wallet="recipient");check("Exact total0.115LTC after restart",balance==Decimal("0.115"))
        check("No peer connections at completion",rpc("getconnectioncount")==0)
        stop();check("Restarted process cleanly exited",proc.returncode==0)
        result={"schema":1,"version":"1.0.0","started_at_utc":began,"completed_at_utc":utc(),
                "platform":sys.platform,"core_versions":versions,"binary_sha256":hashes,"network":"regtest",
                "node_options":flags[:13],"rpc_bound":"127.0.0.1","rpc_port":port,
                "scope":"One new isolated node; two independent fresh wallet databases. Four ordinary transparent native-SegWit sends, per-wallet comment/label inspection and one daemon restart. Not two networked nodes or a complete mobile/GUI wallet study.",
                "cases":cases,"labels":labels,"recipient_total_ltc":balance,"peer_lifecycle":lifecycle,
                "assertions_passed":len(assertions),"assertions":assertions}
        write(output/"results.json",result);write(output/"rpc-trace.json",{"scope":"Generated test wallets only; no private-key/seed/cookie calls; address-info result field subset stated per call.","calls":trace})
        with(output/"cases.csv").open("w",encoding="utf-8",newline="")as h:
            cols=["case_id","kind","amount_ltc","comment_chars","comment_utf8_bytes","comment_to_chars","comment_to_utf8_bytes","sender_note_preserved","receiver_comment_present","receiver_to_present","comment_in_raw","comment_to_in_raw","vsize","confirmations"]
            writer=csv.DictWriter(h,fieldnames=cols);writer.writeheader()
            for item in cases:
                writer.writerow(dict(zip(cols,[item["id"],item["kind"],item["amount"],len(item["comment"]),len(item["comment"].encode()),len(item["comment_to"]),len(item["comment_to"].encode()),item["sender_metadata_before_restart"]==item["sender_metadata_after_restart"],item["recipient_metadata_after_restart"]["comment_present"],item["recipient_metadata_after_restart"]["to_present"],item["searches"]["comment"]["full_string_found"],item["searches"]["comment_to"]["full_string_found"],item["decoded"]["vsize"],item["recipient_confirmations_after_restart"]])))
        print(json.dumps({"ok":True,"assertions":len(assertions),"elapsed_seconds":round(time.monotonic()-start,3),"output_files":["results.json","rpc-trace.json","cases.csv","rpc-help-sendtoaddress.txt","rpc-help-gettransaction.txt","rpc-help-setlabel.txt"]}))
    except BaseException:
        write(output/"failed-assertions.json",assertions);write(output/"failed-rpc-trace.json",{"calls":trace});raise
    finally:
        reservation.close()
        if proc and proc.poll()is None:
            try:stop()
            except Exception:
                proc.terminate()
                try:proc.wait(timeout=5)
                except subprocess.TimeoutExpired:proc.kill();proc.wait(timeout=5)
        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 disposable cleanup path")
            if proc and proc.poll()is None:raise RuntimeError("Refusing cleanup while own process remains 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]))
