#!/usr/bin/env python3
"""Capture actual independently running base/head apps through agent-browser.
The seed provenance is recorded separately: composer T50/E50/SHORT and real plan action.
No title or app string is substituted. normalize.py only prepares metadata.
"""
import argparse,datetime,json,pathlib,re,shutil,sqlite3,subprocess,time
from PIL import Image,ImageChops
P=argparse.ArgumentParser();P.add_argument('side',choices=['base','head']);P.add_argument('--pair',action='store_true');A=P.parse_args()
ROOT=pathlib.Path(__file__).parent
OUT=ROOT/('web-'+A.side+'-'+datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ'));OUT.mkdir()
checkout=pathlib.Path('/home/ubuntu/wt/pr-24611'+('-base' if A.side=='base' else ''))
port=6729 if A.side=='base' else 8497
session='evidence-24611'+('-base' if A.side=='base' else '')
BIN='/home/ubuntu/.npm/_npx/6de2aa2fded2970c/node_modules/agent-browser/bin/agent-browser-linux-x64'
PREFIX=[BIN,'--session',session]
CHROME='/home/ubuntu/.t3/tools/chrome-headless-shell/linux64/156.0.8078.70/chrome-headless-shell'
log=[]
def ab(*args,secret=False):
 r=subprocess.run(PREFIX+list(args),text=True,capture_output=True)
 log.append({'args':['<pairing redacted>'] if secret else args,'stdout':'<redacted>' if secret else r.stdout,'stderr':'<redacted>' if secret else r.stderr,'exit':r.returncode})
 (OUT/'transcript.json').write_text(json.dumps(log,ensure_ascii=True,indent=2))
 if r.returncode:raise RuntimeError(r.stdout+r.stderr)
 return r.stdout
if A.pair:
 startup=(ROOT/(A.side+'-server-final.log')).read_text()
 token=re.findall(r'pairingUrl: .*#token=([^\s]+)',startup)[-1]
 ab('--executable-path',CHROME,'open',f'http://localhost:{port}/pair')
 ab('wait','--text','Pairing token')
 ab('find','role','textbox','fill','--name','Pairing token',token,'--exact',secret=True)
 ab('find','role','button','click','--name','Continue','--exact')
 ab('wait','--fn','!!document.querySelector("[aria-label=Threads]")')
con=sqlite3.connect(f'file:{checkout}/.t3/userdata/statev2.sqlite?mode=ro',uri=True)
thread=next(json.loads(r[0]) for r in con.execute('select payload_json from orchestration_v2_projection_threads') if json.loads(r[0])['title'].startswith('Implement Plan'))
environment=json.loads(subprocess.check_output(['curl','-fsS',f'http://localhost:{port}/.well-known/t3/environment'],text=True))
(OUT/'environment-public.json').write_text(json.dumps(environment,indent=2))
envid=environment.get('environmentId') or environment.get('id')
assert envid,environment
ab('set','viewport','1440','1000','1')
ab('open',f'http://localhost:{port}/{envid}/{thread["id"]}')
ab('wait','--fn','Array.from(document.querySelectorAll("h2")).some(x=>x.textContent.startsWith("Implement Plan")) && !!document.querySelector("[aria-label=Threads]") && document.body.innerText.includes("Reply OK.")')
if 'true' in ab('eval',"document.querySelector('[aria-label=\"Toggle thread details panel\"]')?.getAttribute('aria-pressed')=== 'true'"):
 ab('find','role','button','click','--name','Toggle thread details panel','--exact')
ab('eval',"(() => { const D=Date, fixed=D.parse('2026-10-27T15:00:00.000Z'); globalThis.Date=class extends D {constructor(...a){super(...(a.length?a:[fixed]))}static now(){return fixed}};document.activeElement?.blur();return true })()")
for width,height in [(1440,1000),(390,844)]:
 ab('set','viewport',str(width),str(height),'1')
 if width==390:
  ab('find','role','button','click','--name','Toggle main sidebar','--exact')
 for theme,key in [('light','L'),('dark','D')]:
  scene=f'W-{width}-{key}'
  ab('set','media',theme,'reduced-motion')
  ab('eval','document.fonts.ready.then(()=>true)')
  ab('wait','--load','networkidle')
  ab('mouse','move',str(width-4),'54')
  ab('eval','document.activeElement?.blur()')
  # Both fonts and app-specific visible state must be ready before the equality gate.
  ab('wait','--fn','document.body.innerText.includes("short 😀") && document.body.innerText.includes("Keep ") && document.body.innerText.includes("Text ") && !document.body.innerText.includes("Regenerating title") && !document.body.innerText.includes("reconnecting")')
  for n in range(12):
   a=OUT/f'{scene}-{A.side}-{n}-a.png';b=OUT/f'{scene}-{A.side}-{n}-b.png'
   ab('screenshot',str(a));time.sleep(.5);ab('screenshot',str(b))
   diff=ImageChops.difference(Image.open(a).convert('RGB'),Image.open(b).convert('RGB'))
   log.append({'scene':scene,'settled':not diff.getbbox(),'bbox':diff.getbbox(),'gapSeconds':.5})
   if not diff.getbbox():
    assert Image.open(b).size==(width,height)
    shutil.copyfile(b,ROOT/f'{scene}-{A.side}.png');break
  else:raise RuntimeError(scene+' never settled')
  (OUT/(scene+'-environment.txt')).write_text(ab('eval','JSON.stringify({viewport:[innerWidth,innerHeight],dpr:devicePixelRatio,userAgent:navigator.userAgent,colorScheme:matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light",reducedMotion:matchMedia("(prefers-reduced-motion: reduce)").matches,fontScale:1,clock:new Date().toISOString(),scroll:[scrollX,scrollY],headings:[...document.querySelectorAll("h2")].map(x=>Array.from(x.textContent).map(c=>c.codePointAt(0))),fonts:document.fonts.status})'))
(OUT/'transcript.json').write_text(json.dumps(log,ensure_ascii=True,indent=2))
print(str(OUT))
