#!/usr/bin/env python3
"""Build and capture the real Electron app at two supplied checkouts; no live T3 data."""
import argparse,datetime,json,os,pathlib,subprocess,time,urllib.request
p=argparse.ArgumentParser();p.add_argument('--base',required=True);p.add_argument('--head',required=True);p.add_argument('--output',required=True);p.add_argument('--skip-build',action='store_true');p.add_argument('--rows',default='A1,A2,H1,R1,RK');args=p.parse_args()
scripts=pathlib.Path(__file__).resolve().parent
root=pathlib.Path(args.output).resolve()/datetime.datetime.now(datetime.timezone.utc).strftime('run-%Y%m%d-%H%M%S');root.mkdir(parents=True)
children=[];logs=[]
env=dict(os.environ,PROOF_ROOT=str(root),T3CODE_DEV_SEED='0',T3CODE_TELEMETRY_ENABLED='false')
for k in ['T3_SERVICE_LAUNCHER_CONTEXT','T3_BOOT_SERVICE_UNIT','ELECTRON_RUN_AS_NODE','VITE_HTTP_URL','VITE_WS_URL','T3CODE_DESKTOP_WS_URL','T3CODE_DEV_AUTH_TOKEN']:env.pop(k,None)
def run(argv,cwd=None,extra=None):
 result=subprocess.run(argv,cwd=cwd,env={**env,**(extra or {})},capture_output=True,text=True)
 if result.returncode:raise RuntimeError(str(argv[:2])+': '+result.stderr)
 return result.stdout
def start(argv,cwd,name,extra=None):
 f=open(root/(name+'.log'),'w');logs.append(f);c=subprocess.Popen(argv,cwd=cwd,env={**env,**(extra or {})},stdout=f,stderr=subprocess.STDOUT);children.append(c);return c
def waitport(port):
 end=time.monotonic()+90
 while time.monotonic()<end:
  try:urllib.request.urlopen('http://127.0.0.1:'+str(port)+'/json/list',timeout=1);return
  except Exception:time.sleep(.25)
 raise RuntimeError('Inspector did not start: '+str(port))
def cdp(port,expression):
 x=json.loads(run(['node',str(scripts/'cdp.mjs'),str(port),expression]))
 if x.get('result',{}).get('exceptionDetails'):raise RuntimeError(str(x))
 return x.get('result',{}).get('result',{}).get('value')
def descendants(pid):
 result=[]
 try:
  for c in pathlib.Path('/proc/'+str(pid)+'/task/'+str(pid)+'/children').read_text().split():result.extend(descendants(int(c)));result.append(int(c))
 except FileNotFoundError:pass
 return result
try:
 fixture=root/'fixture-project';fixture.mkdir();(fixture/'fixture.txt').write_text('Synthetic focus fixture.\n');run(['git','init','--quiet',str(fixture)])
 if not args.skip_build:
  for checkout in [args.base,args.head]:
   run(['vp','i'],checkout);run(['node','node_modules/electron/install.js'],str(pathlib.Path(checkout)/'apps/desktop'));run(['vp','run','build:desktop'],checkout)
 start(['node',str(scripts/'fixture-server.mjs')],str(root),'fixture')
 configs={}
 for side,checkout,web,backend,debug,main in [('before',args.base,6895,14935,19224,19324),('after',args.head,7775,15815,19223,19323)]:
  checkout=str(pathlib.Path(checkout).resolve());home=root/(side+'-home');home.mkdir()
  extra={'PROOF_CHECKOUT':checkout,'T3CODE_HOME':str(home),'XDG_CONFIG_HOME':str(root/(side+'-xdg')),'VITE_DEV_SERVER_URL':'http://127.0.0.1:'+str(web),'T3CODE_PORT':str(backend)}
  start(['vp','dev'],checkout+'/apps/web',side+'-vite',{**extra,'HOST':'127.0.0.1','PORT':str(web),'T3CODE_SINGLE_ORIGIN_DEV':'1'})
  start(['headless-desktop','node_modules/electron/dist/electron','--no-sandbox','--disable-gpu','--inspect='+str(main),'--remote-debugging-port='+str(debug),'--t3code-dev-root='+checkout+'/apps/desktop','dist-electron/main.cjs'],checkout+'/apps/desktop',side+'-electron',extra)
  waitport(main);waitport(debug)
  run(['node',str(scripts/'setup-app.mjs'),str(debug)],extra={'PROOF_CHECKOUT':checkout})
  cdp(debug,'window.proofRoot='+json.dumps(str(root))+';window.proofFixture='+json.dumps(str(scripts/'pi-scope-fixture.py')))
  cdp(debug,'@'+str(scripts/'seed-reproduction.js'))
  cdp(main,'@'+str(scripts/'native-observer.js'))
  cdp(debug,'@'+str(scripts/'fix-theme.js'));cdp(debug,'@'+str(scripts/'prepare-renderer.js'))
  native=cdp(main,'({display:process.env.DISPLAY,auth:process.env.XAUTHORITY})')
  sha=run(['git','rev-parse','HEAD'],checkout).strip()
  cfg={'port':debug,'server':backend,'display':native['display'],'auth':native['auth'],'sha':sha};configs[side]=(checkout,cfg)
  (root/(side+'-config.json')).write_text(json.dumps(cfg))
  credential=root/('private-'+str(backend)+'.json')
  end=time.monotonic()+30
  while not credential.exists() and time.monotonic()<end:time.sleep(.1)
  opened=run(['node',str(scripts/'mcp.mjs'),str(credential),'preview_open',json.dumps({'url':'http://127.0.0.1:18923/'})])
  (root/(side+'-final-open.json')).write_text(opened)
 for row in args.rows.split(','):
  for side,(checkout,cfg) in configs.items():
   print(run(['node',str(scripts/'capture.mjs'),side,row],extra={'PROOF_CHECKOUT':checkout}).strip(),flush=True)
 print('OUTPUT '+str(root),flush=True)
finally:
 for port in [19323,19324]:
  try:cdp(port,'process.mainModule.require("electron").app.quit()')
  except Exception:pass
 for c in reversed(children):
  pids=descendants(c.pid)
  for pid in pids:
   try:os.kill(pid,15)
   except ProcessLookupError:pass
  if c.poll() is None:c.terminate()
 for c in children:
  try:c.wait(timeout=5)
  except subprocess.TimeoutExpired:c.kill()
 for f in logs:f.close()
