"""Export synthetic evidence only; never exports provider internal logs or credentials."""
from __future__ import annotations
import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import shutil
import zipfile

from provider import dump,PROFILES,utc
from verify_evidence import verify

ROOT=Path(__file__).resolve().parent
ALLOWED_PROVIDER_FILES={'input.txt','answer.txt','metadata.json','started.json','replay-provenance.json'}
SUSPICIOUS=[re.compile(rb'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----'),re.compile(rb'"(?:access_token|refresh_token|api_key)"\s*:\s*"[^"\r\n]{8,}"',re.I),re.compile(rb'Bearer\s+[A-Za-z0-9_-]{30,}')]

def h(path):return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def read(path):return json.loads(Path(path).read_text(encoding='utf-8-sig'))

def event_audit(source,destination):
 """Expose event kinds and tool lifecycle only, without internal message content."""
 raw=source/'stdout.txt'
 if not raw.exists():return
 rows=[]
 for line in raw.read_text(encoding='utf-8').splitlines():
  try:event=json.loads(line)
  except ValueError:continue
  row={k:event[k] for k in ['type','event','num_turns','is_error','subtype'] if k in event}
  if isinstance(event.get('item'),dict):row['item_type']=event['item'].get('type')
  if event.get('event')=='step_update':
   step=event.get('step_update',{});row.update({k:step[k] for k in ['step_index','step_type','state','tool_name','duration_seconds'] if k in step})
   if step.get('step_type')=='tool':
    info=step.get('tool_info',{});row['tool_name']=info.get('name',row.get('tool_name'))
    recipient=info.get('parameters',{}).get('Recipient')
    if recipient is not None:row['recipient']=recipient
    if isinstance(info.get('error'),dict):row['error_type']=info['error'].get('type')
  if row:rows.append(row)
 dump(destination/'provider-event-audit.json',{'raw_stream_sha256':h(raw),'derived_event_census':rows,'note':'Derived from private provider stream; payload text/internal reasoning and tool arguments other than Recipient omitted. Not a provider signature or complete backend transcript.'})

def export_run(source,dest):
 source=Path(source);dest=Path(dest);dest.mkdir(parents=True,exist_ok=False)
 excluded=[]
 for p in sorted(source.rglob('*')):
  if not p.is_file():continue
  rel=p.relative_to(source)
  if 'workspace' in rel.parts or 'worker-logs' in rel.parts or p.name in ['internal-agy.log','stdout.txt','stderr.txt']:
   excluded.append({'relative_path':rel.as_posix(),'sha256':h(p),'reason':'provider internal diagnostics/raw event stream; final response and task/usage evidence exported separately'})
   continue
  if 'provider' in rel.parts and p.name not in ALLOWED_PROVIDER_FILES:
   excluded.append({'relative_path':rel.as_posix(),'sha256':h(p),'reason':'outside provider evidence allowlist'})
   continue
  if p.suffix not in ['.json','.jsonl','.txt']:continue
  data=p.read_bytes()
  if any(pattern.search(data) for pattern in SUSPICIOUS):raise RuntimeError('Potential credential in export: '+str(rel))
  target=dest/rel;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(data)
 for metadata in source.glob('episodes/*/provider/*/metadata.json'):
  event_audit(metadata.parent,dest/metadata.parent.relative_to(source))
 dump(dest/'export-policy.json',{'exported_at':utc(),'source_run':source.name,'exact_saved_experimental_inputs_and_final_answers':True,'excluded':excluded,'notes':['Credentials and unrelated provider internal logs are not exported.','Adapter hashes are hashes of UTF-8 logical strings; the bundle manifest separately hashes physical file bytes.','A2A messages are exact synthetic local wire records; no provider backend attestation is claimed.','A replay is not a new model inference.']})
 result=verify(dest,dest/'verification.json')
 if not result['passed']:raise RuntimeError('Public evidence verification failed: '+json.dumps(result['errors']))
 return result

def make_manifest(folder):
 return {p.relative_to(folder).as_posix():{'sha256':h(p),'bytes':p.stat().st_size} for p in sorted(folder.rglob('*')) if p.is_file() and p.name!='MANIFEST.json'}

def zip_tree(source,destination):
 with zipfile.ZipFile(destination,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=9) as z:
  for p in sorted(source.rglob('*')):
   if p.is_file():
    info=zipfile.ZipInfo(p.relative_to(source).as_posix(),date_time=(2026,9,6,0,0,0));info.compress_type=zipfile.ZIP_DEFLATED
    z.writestr(info,p.read_bytes(),compresslevel=9)

def create_browser_data(run,label,webroot):
 episodes=[]
 for status in sorted((run/'episodes').glob('*/status.json')):
  state=read(status);episode=status.parent;j=state['job'];ident=label+'--'+j['id']
  obj={'phase':label,'job':j,'status':state,'model_profiles':PROFILES,'decisions':[]}
  for name,key in [('market-history.json','market_history'),('transcript.json','transcript'),('observation.json','calibration')]:
   if (episode/name).exists():obj[key]=read(episode/name)
  for p in (episode/'provider').glob('*/metadata.json'):
   m=read(p);obj['decisions'].append({'invocation_id':m['id'],'profile':m['profile'],'requested_model':m['model'],'started_at':m['started_at'],'ended_at':m['ended_at'],'elapsed_seconds':m['elapsed_seconds'],'status':m['status'],'failure':m.get('failure'),'usage':m.get('usage'),'model_usage':m.get('model_usage'),'model_identity_evidence':m.get('model_identity_evidence'),'replayed_without_new_inference':(p.parent/'replay-provenance.json').exists() or (episode/'recovery-import.json').exists(),'prompt':(p.parent/'input.txt').read_text(encoding='utf-8'),'answer':(p.parent/'answer.txt').read_text(encoding='utf-8')})
  obj['decisions'].sort(key=lambda x:x['started_at'])
  filename=f'episodes/{ident}.json';dump(webroot/filename,obj)
  episodes.append({'id':ident,'phase':label,'job':j,'status':state['status'],'metrics':state.get('metrics',{}),'data_path':filename})
 return episodes

def main(args):
 out=Path(args.out).resolve()
 if args.append:index=read(out/'experiment-index.json')
 else:
  out.mkdir(parents=True,exist_ok=False)
  index={'created_at':utc(),'profiles':PROFILES,'phases':[],'episodes':[],'bundles':[]}
 for item in args.run:
  label,src=item.split('=',1)
  if any(b['phase']==label for b in index['bundles']):raise RuntimeError('Phase already exported: '+label)
  source=Path(src).resolve();package=out/'packages'/label;package.mkdir(parents=True)
  run=package/'runs'/label;report=export_run(source,run)
  for name in ['verify_evidence.py','analysis.py','replication_analysis.py','followup_analysis.py','experiment.py','runtime_followup.py','provider.py','a2a_harness.py','agent-instructions.txt','requirements.lock.txt','reproduce.py']:
   shutil.copy2(ROOT/name,package/name)
  plan=read(source/'plan.json');phase=plan['phase']
  if (ROOT/'protocols'/phase).exists():shutil.copytree(ROOT/'protocols'/phase,package/'protocols'/phase)
  documentation=ROOT/'publication'/'evidence-README.md'
  if documentation.exists():shutil.copy2(documentation,package/'README.md')
  dump(package/'MANIFEST.json',make_manifest(package))
  archive=out/(label+'-evidence.zip');zip_tree(package,archive)
  index['phases'].append({'id':label,'plan':plan,'verification':report})
  index['bundles'].append({'phase':label,'filename':archive.name,'sha256':h(archive),'bytes':archive.stat().st_size})
  index['episodes']+=create_browser_data(run,label,out)
 dump(out/'experiment-index.json',index)
 dump(out/'FILE-HASHES.json',make_manifest(out))
 print(json.dumps({'episodes':len(index['episodes']),'bundles':index['bundles']},indent=2))

if __name__=='__main__':
 ap=argparse.ArgumentParser();ap.add_argument('--run',action='append',required=True,help='public-label=private-run-path');ap.add_argument('--out',required=True);ap.add_argument('--append',action='store_true');main(ap.parse_args())
