#!/usr/bin/env python3
"""Real built-server lifecycle evidence. No mocks or imported application internals.
Usage: PYTHONPATH=<deps> python3 lifecycle.py WORKTREE SDK OUTPUT_ROOT
Requires websocket-client; Android SDK platform-tools, emulator, cmdline-tools.
Each invocation creates fresh state and records only synthetic, public output.
"""
import sys, os, json, time, pathlib, subprocess, signal, socket, secrets, urllib.request
import websocket
root, sdk, outroot = map(pathlib.Path, sys.argv[1:4])
run = outroot / (time.strftime('%Y%m%d-%H%M%S') + '-' + root.name)
run.mkdir(parents=True)
state = root / '.t3' / ('evidence-' + time.strftime('%Y%m%d-%H%M%S'))
sha = subprocess.check_output(['git','rev-parse','HEAD'],cwd=root,text=True).strip()
log = (run/'transcript.txt').open('w')
def say(msg):
    print(msg,flush=True); log.write(msg+'\n'); log.flush()
def waitfor(fn, seconds=120):
    deadline=time.monotonic()+seconds
    while time.monotonic()<deadline:
        try:
            value=fn()
            if value: return value
        except (OSError,ValueError,websocket.WebSocketException): pass
        time.sleep(.2)
    raise RuntimeError('Timed out awaiting runtime milestone')
def process_state(pid):
    try:
        data=pathlib.Path(f'/proc/{pid}/stat').read_text().split(') ')[1].split()
        return data[0],data[19]
    except FileNotFoundError: return None
records={}
def alive(pid):
    value=process_state(pid)
    return bool(value and value[0]!='Z' and value[1]==records[pid])
def record(pid):
    records[pid]=process_state(pid)[1]
def health(port):
    try:
        with urllib.request.urlopen(f'http://127.0.0.1:{port}/health',timeout=1) as r: return r.status==200
    except OSError:return False
with socket.socket() as s:
    s.bind(('127.0.0.1',0)); port=s.getsockname()[1]
token=secrets.token_urlsafe(40)
env={**os.environ,'T3CODE_DEV_SEED':'0','T3CODE_DEV_AUTH_TOKEN':token,'ANDROID_HOME':str(sdk),'JAVA_HOME':str(sdk.parent/'jdk'),'NO_COLOR':'1','FORCE_COLOR':'0'}
for key in list(env):
    if key.startswith('AGENT_DEVICE_'):env.pop(key)
isolated_home=run/'isolated-home'; isolated_home.mkdir()
env['HOME']=str(isolated_home)
with socket.socket() as adb_socket:
    adb_socket.bind(('127.0.0.1',0)); adb_port=adb_socket.getsockname()[1]
env['ADB_SERVER_SOCKET']=f'tcp:127.0.0.1:{adb_port}'
env['ANDROID_ADB_SERVER_PORT']=str(adb_port)
servers=[]; daemons=[]; hubs=[]; sockets=[]
def start(label):
    raw=(run/(label+'-server-private.log')).open('w')
    p=subprocess.Popen(['node','apps/server/dist/bin.mjs','--base-dir',str(state),'--host','127.0.0.1','--port',str(port),'--dev-url','http://127.0.0.1:5173','--no-browser'],cwd=root,env=env,stdout=raw,stderr=subprocess.STDOUT)
    servers.append(p)
    def connect():
        if p.poll() is not None:raise RuntimeError('Server exited; inspect private log')
        return websocket.create_connection(f'ws://127.0.0.1:{port}/ws?orchestrationProtocol=2',header={'Authorization':'Bearer '+token},suppress_origin=True,timeout=5)
    ws=waitfor(connect); sockets.append(ws)
    say(f'{label}: real server PID {p.pid}, authenticated RPC ready')
    return p,ws
counter=0
def rpc(ws,method,payload):
    global counter
    counter+=1; ident=str(counter)
    ws.send(json.dumps({'_tag':'Request','id':ident,'tag':method,'payload':payload,'headers':[]}))
    ws.settimeout(600)
    while True:
        result=json.loads(ws.recv())
        for item in result if isinstance(result,list) else [result]:
            if item.get('requestId')==ident and item.get('_tag')=='Exit':
                ex=item['exit']
                if ex['_tag']!='Success':raise RuntimeError('RPC failure: '+json.dumps(ex))
                return ex['value']
def provision(ws):
    say('RPC device.configure: local support ON, agent access ON')
    value=rpc(ws,'device.configure',{'enabled':True,'agentAccessEnabled':True,'onboardingCompleted':True})
    say('RPC device.list retry local host: initialize real agent tools')
    value=rpc(ws,'device.list',{'retryHostId':'local'})
    daemonfile=state/'userdata/device/agent-device/daemon.json'
    if not daemonfile.exists():
        (run/'device-response-private.json').write_text(json.dumps(value,indent=2))
        raise RuntimeError('No daemon record; inspect private response')
    d=json.loads(daemonfile.read_text()); pid=d['pid']; record(pid); daemons.append(pid)
    hubfile=daemonfile.with_name('hub.json')
    if hubfile.exists():
        h=json.loads(hubfile.read_text()); record(h['pid']); hubs.append(h['pid'])
    say(f'Environment daemon PID {pid}, agent-device {d.get("version")}, HTTP health={health(d["httpPort"])}')
    return d
def crash(p,ws,d):
    ws.close(); p.kill(); p.wait(timeout=30)
    say(f'SIGKILL server PID {p.pid}: exited {p.returncode}')
    say(f'Recorded daemon PID {d["pid"]} survives abrupt exit: alive={alive(d["pid"])}, health={health(d["httpPort"])}')
    assert alive(d['pid']) and health(d['httpPort'])
def observe(d,label):
    time.sleep(3)
    live=alive(d['pid']); healthy=health(d['httpPort'])
    say(f'{label}: SAME daemon PID {d["pid"]}: alive={live}, HTTP health={healthy}')
    return {'alive':live,'healthy':healthy}
try:
    say('T3 Code / agent-device lifecycle — real built server')
    say('Commit '+sha)
    say('Environment: Linux x86_64; Node 26.12.1; fresh isolated state')
    say('No copied database. Android discovery tools; no booted device.')
    say('=== A. Graceful quit after an earlier server was killed ===')
    p,ws=start('First server'); d=provision(ws); crash(p,ws,d)
    p2,ws2=start('Restart, same state')
    say('Restarted server uses NO device RPC or agent tools')
    time.sleep(1); ws2.close(); p2.send_signal(signal.SIGTERM); p2.wait(timeout=45)
    say(f'SIGTERM restarted server PID {p2.pid}: exited {p2.returncode}')
    a=observe(d,'After graceful quit')
    if alive(d['pid']):
        subprocess.run(['node',str(state/'tools/agent-device/0.21.31/node_modules/agent-device/bin/agent-device.mjs'),'daemon','stop','--state-dir',str(state/'userdata/device/agent-device')],env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=20)
    say('=== B. Disable agent access after an earlier server was killed ===')
    p3,ws3=start('Third server'); d2=provision(ws3); crash(p3,ws3,d2)
    p4,ws4=start('Restart, same state')
    say('Restarted server has not used device tools')
    say('RPC device.configure: agent access OFF (same operation as Devices settings)')
    value=rpc(ws4,'device.configure',{'agentAccessEnabled':False})
    say('Settings response: agentAccessEnabled='+str(value.get('agentAccessEnabled')))
    b=observe(d2,'After disabling access')
    ws4.close(); p4.send_signal(signal.SIGTERM); p4.wait(timeout=45)
    (run/'results.json').write_text(json.dumps({'sha':sha,'quit':a,'disable':b},indent=2))
    say('DONE. Observations saved; cleanup follows.')
except Exception as e:
    say('ERROR: '+str(e)); raise
finally:
    for ws in sockets:
        try:ws.close()
        except Exception:pass
    for p in servers:
        if p.poll() is None:
            p.send_signal(signal.SIGTERM)
            try:p.wait(timeout=45)
            except subprocess.TimeoutExpired:p.kill(); p.wait()
    for pid in daemons+hubs:
        if alive(pid):
            os.kill(pid,signal.SIGTERM)
    subprocess.run([str(sdk/'platform-tools/adb'),'kill-server'],env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10)
    say('Cleanup: all recorded server, hub and daemon processes stopped.')
