import os,sys,json,time,subprocess,pathlib,urllib.request,http.server,threading,re,socket
root=pathlib.Path(__file__).parent
run=root/time.strftime('%Y%m%d-%H%M%S');run.mkdir()
# A live unrelated app makes the configured loopback route occupied.
class App(http.server.BaseHTTPRequestHandler):
 def do_GET(self):
  self.send_response(200);self.end_headers();self.wfile.write(b'OTHER APP: existing service on port 8080\n')
 def log_message(self,*a): pass
app=http.server.HTTPServer(('127.0.0.1',8080),App);threading.Thread(target=app.serve_forever,daemon=True).start()
try:
 for side,checkout in [('before',sys.argv[1]),('after',sys.argv[2])]:
  out=run/side;out.mkdir();home=out/'home';fake=out/'bin';fake.mkdir();calls=out/'calls.txt'
  route={'Web':{'host-a.tailnet-name.ts.net:443':{'Handlers':{'/':{'Proxy':'http://127.0.0.1:8080'}}}}}
  (fake/'tailscale').write_text('#!/usr/bin/env python3\nimport os,sys,json\na=sys.argv[1:]\nwith open(os.environ["CALL_LOG"],"a") as f:f.write("tailscale "+" ".join(a)+"\\n")\nif a==["status","--json"]: print('+repr(json.dumps({'BackendState':'Running','Self':{'DNSName':'host-a.tailnet-name.ts.net.','TailscaleIPs':['100.64.0.1'],'CapMap':{'https':None}}}))+')\nelif a==["serve","status","--json"]:print('+repr(json.dumps(route))+')\n')
  (fake/'tailscale').chmod(0o755)
  env={**os.environ,'PATH':str(fake)+':'+os.environ['PATH'],'CALL_LOG':str(calls),'T3CODE_DEV_SEED':'0','NO_COLOR':'1'}
  events=[];start=time.monotonic()
  def emit(s):
   s=re.sub(r'Token: .*', 'Token: [REDACTED]', s)
   s=re.sub(r'App link: .*', 'App link: [REDACTED]', s)
   s=re.sub(r'Pairing with .*? \(', 'Pairing with synthetic server (', s)
   if any(c in s for c in '█▀▄'):return
   s=re.sub(r'token=[^\s\x1b]+','token=[REDACTED]',s)
   s=s.replace(str(home),'<fresh-home>').replace(checkout,'<checkout>')
   s=re.sub(r'\x1b\[[0-9;]*[A-Za-z]','',s)
   events.append([round(time.monotonic()-start,3),s]);print(side+': '+s,flush=True)
  sha=subprocess.check_output(['git','rev-parse','HEAD'],cwd=checkout,text=True).strip()
  emit('Linux CLI | '+side+' | commit '+sha[:10]+'\nFresh isolated home; real Node server; fake Tailscale CLI.')
  emit('Existing route: https://host-a.tailnet-name.ts.net:443/ -> http://127.0.0.1:8080')
  emit('GET http://127.0.0.1:8080 -> '+urllib.request.urlopen('http://127.0.0.1:8080').read().decode().strip())
  dns=subprocess.run(['getent','hosts','host-a.tailnet-name.ts.net'],capture_output=True,text=True)
  emit('getent hosts host-a.tailnet-name.ts.net -> exit '+str(dns.returncode)+' (no DNS result)')
  log=open(out/'server-private.log','w')
  server=subprocess.Popen(['node','src/bin.ts','serve','--base-dir',str(home),'--port','47483','--host','127.0.0.1'],cwd=checkout+'/apps/server',env=env,stdout=log,stderr=log,start_new_session=True)
  try:
   for _ in range(300):
    try:
     assert (home/'userdata/server-runtime.json').exists()
     desc=json.load(urllib.request.urlopen('http://127.0.0.1:47483/.well-known/t3/environment',timeout=1));break
    except Exception:
     if server.poll()!=None:raise RuntimeError((out/'server-private.log').read_text())
     time.sleep(.2)
   else:raise RuntimeError('server startup timeout')
   emit('Real server listening at 127.0.0.1:47483; environment descriptor protocolVersion='+str(desc['orchestrationProtocolVersion']))
   emit('$ node src/bin.ts pair --base-dir <fresh-home> --tailscale')
   pair=subprocess.Popen(['node','src/bin.ts','pair','--base-dir',str(home),'--tailscale'],cwd=checkout+'/apps/server',env=env,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
   for line in pair.stdout:
    if line.strip():emit(line.rstrip())
   pair.wait(timeout=60);emit('Pair command exit: '+str(pair.returncode))
   emit('Tailscale invocations:\n'+calls.read_text().strip())
   emit('Route replacement command attempted: '+str('serve --bg --https=443' in calls.read_text()))
   emit('Existing app still live: '+urllib.request.urlopen('http://127.0.0.1:8080').read().decode().strip())
  finally:
   os.killpg(server.pid,15);server.wait(timeout=30);log.close()
  (out/'recording.json').write_text(json.dumps(events,indent=2))
  (out/'transcript.txt').write_text('\n\n'.join(x[1] for x in events)+'\n')
finally:app.shutdown();app.server_close()
print('RUN='+str(run))
