#!/usr/bin/env python3
"""Read-only cleanup verification, synthetic-file allowlist, hashes and upload bundles."""
import datetime, hashlib, json, pathlib, re, socket, subprocess, tarfile

ROOT=pathlib.Path('/tmp/lead-32913-r2/evidence-capture-1')
SESSION=pathlib.Path((ROOT/'latest-runs.txt').read_text())
BUILD=pathlib.Path((ROOT/'latest-build.txt').read_text())
ENV=ROOT/'environment-20261110T124600Z/environment.txt'
DRIVERS=['build_capture.py','capture_missing.py','report_missing.py','finalize_missing.py','fixture_server.py']

def main():
    all_sessions=sorted(ROOT.glob('runs-*'))
    prelim=[s for s in all_sessions if s!=SESSION]
    pids=set(); ports=set(); count=0; final_count=0
    for session in all_sessions:
        for path in session.glob('*/before/events.json'):
            for side in ['before','after']:
                events=json.loads((path.parent.parent/side/'events.json').read_text())
                count+=1;final_count+=session==SESSION
                closed=[e for e in events if e['kind']=='FIXTURE_CLOSED']
                assert closed and closed[-1]['data']['thread_alive'] is False
                ports.add(closed[-1]['data']['port'])
                state=next(e['data'] for e in events if e['kind']=='PROCESS_STATE')
                assert state['remaining_tracked_processes']==[]
                pids.update(state['tracked_pids'])
    live=[pid for pid in sorted(pids) if pathlib.Path(f'/proc/{pid}').exists()]
    assert not live, live
    port_states=[]
    for port in sorted(ports):
        with socket.socket() as sock:
            sock.settimeout(1)
            code=sock.connect_ex(('127.0.0.1',port))
            assert code!=0, port
            port_states.append({'port':port,'connect_errno':code})
    before=pathlib.Path('/home/user/wt/pr-32913-before')
    assert not before.exists()
    worktrees=subprocess.check_output(['git','-C','/home/user/t3code','worktree','list','--porcelain'],text=True)
    assert str(before) not in worktrees
    head=subprocess.check_output(['git','-C','/home/user/wt/pr-32913','rev-parse','HEAD'],text=True).strip()
    status=subprocess.check_output(['git','-C','/home/user/wt/pr-32913','status','--short'],text=True)
    assert head=='1af59792b3ca105da634cdf90c392bae9ad1ff91' and status==''
    disk=subprocess.check_output(['df','-BG','--output=avail','/'],text=True)
    assert int(re.search(r'(\d+)G',disk)[1])>=5
    cleanup={'utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'final_executions':final_count,'preliminary_executions':count-final_count,'all_fixture_threads_closed':True,'tracked_process_count':len(pids),'remaining_tracked_pids':live,'server_ports_closed':port_states,'temporary_before_worktree_removed':True,'head':head,'worktree_status':status,'disk':disk,'live_T3_home_accessed':False}
    (ROOT/'cleanup-verification.txt').write_text(json.dumps(cleanup,indent=2)+'\n')
    tools={
      'node':{'url':'https://nodejs.org/dist/v20.11.1/node-v20.11.1-linux-x64.tar.xz','path':'tools/node-v20.11.1-linux-x64.tar.xz'},
      'powershell':{'url':'https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-x64.tar.gz','path':'tools/powershell-7.4.6-linux-x64.tar.gz'}}
    for tool in tools.values():
        payload=ROOT/tool['path'];tool['bytes']=payload.stat().st_size;tool['sha256']=hashlib.sha256(payload.read_bytes()).hexdigest()
    (ROOT/'tools.json').write_text(json.dumps(tools,indent=2))
    (ROOT/'environment.txt').write_bytes(ENV.read_bytes())
    (ROOT/'evidence-sheet.md').write_text((ROOT/'evidence-sheet.template.md').read_text().replace('ARTIFACT_LINKS','Final transcripts/diffs and build provenance are in this archive; public download links are in the separately uploaded sheet.'))
    (ROOT/'README.md').write_text('''# Synthetic CLI evidence

Final evidence: 4 rows, 14 pairs, 28 executions. See evidence-sheet.md.
All before/after pair directories are at this archive root. Sources, wrappers,
per-request UTC logs, output, file listings, JSON results, and three diff views
are included in each pair. Generated curl/sleep wrappers forward to the real
/usr/bin programs; they only log execution. No transport mocks or real T3 data.

Reproduction requires the repository and its installed dependencies. Recreate
the temporary before checkout at the merge-base using the command in the brief.
Link both node_modules and scripts/node_modules from the existing head checkout
into that temporary checkout. Run build_capture.py (the real repository build),
then capture_missing.py, then report_missing.py. The tools/ tarball URLs and
hashes are in tools.json; extract them into the paths named in the drivers.
Remove the temporary before checkout with the brief's literal worktree-remove
command. finalize_missing.py only verifies cleanup and packages the evidence.

Fixture reused without modification from the prior capture index:
https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0b131aebda907dab/fixture_server.py
Final capture calls artifacts() once, sharing byte-identical payloads across
all sides. Preliminary captures used one payload per execution; their gzip/zip
creation timestamps may differ. The preliminary archive preserves those earlier
same-row runs; final evidence supersedes them.
''')
    final_files={}
    def add(path, arc):
        if path.is_file() and not path.is_symlink(): final_files[arc]=path
    for path in SESSION.rglob('*'):
        relative=path.relative_to(SESSION)
        # Exclude PowerShell startup caches; retain synthetic T3 runtime and TMPDIR.
        if '/home/' in '/'+str(relative) and '/home/t3/' not in '/'+str(relative):continue
        add(path,str(relative))
    for path in BUILD.rglob('*'):
        if 'home' in path.relative_to(BUILD).parts:continue
        add(path,'build/'+str(path.relative_to(BUILD)))
    for name in DRIVERS:add(ROOT/name,'drivers/'+name)
    for name in ['environment.txt','tools.json','cleanup-verification.txt','evidence-sheet.md','README.md']:add(ROOT/name,name)
    prelim_files={}
    for session in prelim:
        for path in session.rglob('*'):
            relative=path.relative_to(session)
            if '/home/' in '/'+str(relative) and '/home/t3/' not in '/'+str(relative):continue
            if path.is_file() and not path.is_symlink():prelim_files[session.name+'/'+str(relative)]=path
    # No private operational identifiers or credentials are allowed in published text.
    pattern=re.compile(rb'(?i)(?:gpt|claude|gemini|o[134])[-_][0-9][a-z0-9_.-]*')
    hits=[]
    for arc,path in list(final_files.items())+list(prelim_files.items()):
        if path.suffix in ['.py','.mjs','.ps1','.sh','.txt','.json','.jsonl','.log','.diff','.md']:
            if pattern.search(path.read_bytes()):hits.append(arc)
    assert not hits, hits
    privacy={'text_files_scanned':sum(p.suffix in ['.py','.mjs','.ps1','.sh','.txt','.json','.jsonl','.log','.diff','.md'] for p in list(final_files.values())+list(prelim_files.values())),'private_operational_identifier_matches':0,'file_selection':'synthetic captures, executed installer sources, generated fixture wrappers, build artifacts, environment and drivers only; no database, credentials, real home, or binary tool distribution included'}
    (ROOT/'privacy-scan.json').write_text(json.dumps(privacy,indent=2));final_files['privacy-scan.json']=ROOT/'privacy-scan.json'
    inventory={arc:{'bytes':p.stat().st_size,'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for arc,p in sorted(final_files.items())}
    (ROOT/'inventory.json').write_text(json.dumps(inventory,indent=2));final_files['inventory.json']=ROOT/'inventory.json'
    for name,files in [('requested-evidence.tar.gz',final_files),('preliminary-captures.tar.gz',prelim_files)]:
        with tarfile.open(ROOT/name,'w:gz') as tf:
            for arc,path in sorted(files.items()):tf.add(path,arcname=arc,recursive=False)
        print(name, len(files), 'files', (ROOT/name).stat().st_size,'bytes')
    print(json.dumps({'final':final_count,'preliminary':count-final_count,'remaining_processes':live,'closed_ports':len(ports),'before_worktree_removed':True,'worktree_clean':True,'disk':disk.strip()},indent=2))

if __name__=='__main__':main()
