import os,sys,subprocess,json,time,socket,pathlib,datetime
root=pathlib.Path(__file__).parent
checkout=pathlib.Path(sys.argv[1]); side=sys.argv[2]
out=root/(side+'-'+datetime.datetime.now().strftime('%Y%m%dT%H%M%S'));out.mkdir()
def command(args,**kw): return subprocess.check_output(args,text=True,**kw)
ip=command(['tailscale','ip','-4']).strip().splitlines()[0]
status=json.loads(command(['tailscale','status','--json']))
prefs=json.loads(command(['tailscale','debug','prefs']))
subprocess.run(['ip','link','set','lo','up'],check=True)
subprocess.run(['ip','addr','add',ip+'/32','dev','lo'],check=True)
priv=out/'privsep';priv.mkdir();os.chmod(priv,0o755)
subprocess.run(['mount','--bind',str(priv),'/run/sshd'],check=True)
key=out/'hostkey';subprocess.run(['ssh-keygen','-q','-t','ed25519','-N','','-f',str(key)],check=True)
config=out/'sshd.conf';config.write_text(f'Port 22\nListenAddress {ip}\nHostKey {key}\nPidFile {out}/sshd.pid\nPasswordAuthentication no\nUsePAM no\n')
sshlog=open(out/'sshd.log','w');serverlog=open(out/'server.log','w')
sshd=subprocess.Popen(['/usr/sbin/sshd','-D','-e','-f',str(config)],stdout=sshlog,stderr=sshlog)
server=None
try:
 def probe(host):
  try:
   with socket.create_connection((host,22),timeout=1) as s:return 'OPEN: '+s.recv(256).decode().strip()
  except OSError as e:return 'REFUSED' if e.errno==111 else str(e)
 for _ in range(100):
  if probe(ip).startswith('OPEN'):break
  if sshd.poll()!=None:raise RuntimeError((out/'sshd.log').read_text())
  time.sleep(.1)
 home=out/'home'
 env=dict(os.environ,T3CODE_DEV_SEED='0',T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD='0',T3CODE_ANALYTICS_DISABLED='1')
 server=subprocess.Popen(['node','apps/server/src/bin.ts','serve','--host','127.0.0.1','--port','18739','--base-dir',str(home)],cwd=checkout,env=env,stdout=serverlog,stderr=serverlog)
 for _ in range(1800):
  try:
   with socket.create_connection(('127.0.0.1',18739),timeout=.2):break
  except OSError:
   if server.poll()!=None:raise RuntimeError((out/'server.log').read_text()[-8000:])
   time.sleep(.1)
 token=command(['node','apps/server/src/bin.ts','auth','session','issue','--base-dir',str(home),'--token-only'],cwd=checkout)
 tokenfile=out/'token';tokenfile.write_text(token)
 rpc=command(['node',str(root/'rpc.mjs'),'http://127.0.0.1:18739',str(tokenfile)],cwd=checkout)
 targets=json.loads(rpc)['remoteOpenTargets']
 # Only aliases go into public evidence; discovery itself uses the real daemon and address.
 clean=[{**t,'host':t['host'].replace(status['Self']['DNSName'].rstrip('.'),'ssh-host.example-tailnet.ts.net'),'username':'[redacted]'} for t in targets]
 transcript={'commit':command(['git','rev-parse','HEAD'],cwd=checkout).strip(),'environment':'Linux; private user/mount/network namespace; real OpenSSH and Tailscale daemon; fresh isolated T3 database; real T3 server and authenticated WebSocket server.getConfig; no mocks','tailscaleBackendState':status['BackendState'],'tailscaleRunSSH':prefs['RunSSH'],'magicDnsName':'ssh-host.example-tailnet.ts.net (privacy alias)','listenerTailnetIPv4':probe(ip),'listener127.0.0.1':probe('127.0.0.1'),'listenerIPv6Loopback':probe('::1'),'remoteOpenTargets':clean,'privacy':'Host DNS name, IP and username are omitted or aliased; discovery used the actual values.'}
 (out/'transcript.json').write_text(json.dumps(transcript,indent=2)+'\n')
 print(json.dumps(transcript,indent=2));print('OUTPUT_DIRECTORY='+str(out))
finally:
 for process in [server,sshd]:
  if process is not None:
   process.terminate()
   try:process.wait(timeout=20)
   except subprocess.TimeoutExpired:process.kill();process.wait()
