#!/usr/bin/env python3
"""Keep raw files; parse JSON checkpoints and normalize only fixture paths/random names and metadata for diffs."""
import pathlib, json, re, sys, difflib, collections, hashlib
S=pathlib.Path('/tmp/lead-25999-r1/evidence-capture-2')

def events(raw):
 text=raw.read_text(); decoder=json.JSONDecoder(); out=[]; pos=0
 while pos<len(text):
  if text[pos].isspace():pos+=1;continue
  if text[pos]=='{':
   try:
    event,end=decoder.raw_decode(text,pos)
    if isinstance(event,dict) and 'kind' in event:out.append(event);pos=end;continue
   except json.JSONDecodeError:pass
  end=text.find('\n',pos)
  if end<0:end=len(text)
  line=text[pos:end]
  if line:out.append({'kind':'console.raw','data':line})
  pos=end+1
 return out

def normalize(entry):
 raw=pathlib.Path(entry['raw']); data=events(raw); aliases={}; counters=collections.Counter()
 roots=[(entry['legacy'],'<LEGACY_ROOT>'),(entry['root'],'<RUN_ROOT>')]
 # Preserve root identity; names inside a fixture remain meaningful, including spaces and XML characters.
 def string(value):
  for root,replacement in roots:value=value.replace(root,replacement)
  value=value.replace('/tmp/lead-25999-r1/evidence-capture-2/sources/before','<CHECKOUT>').replace('/tmp/lead-25999-r1/evidence-capture-2/sources/after','<CHECKOUT>')
  value=value.replace('/home/ubuntu/wt/pr-25999-base','<CHECKOUT>').replace('/home/ubuntu/wt/pr-25999','<CHECKOUT>')
  # Stack file positions reflect changed source layout, not filesystem behavior; raw files preserve them.
  value=re.sub(r'(file://<CHECKOUT>/[^()\s]+?):[0-9]+:[0-9]+',r'\1:<line>:<column>',value)
  # makeTempDirectory/makeTempFile random identities are assigned in first-observation order.
  for pattern,kind in [(r'launcher\.next-(?!leftover-|orphan)[A-Za-z0-9]{6}(?![A-Za-z0-9])','stage'),(r'\.service-write-[A-Za-z0-9]{6}/[a-f0-9]+','durable-temp')]:
   def replace(m):
    key=(kind,m.group(0))
    if key not in aliases:counters[kind]+=1;aliases[key]=f'<{kind}-{counters[kind]}>'
    return aliases[key]
   value=re.sub(pattern,replace,value)
  # Fiber ids are scheduler metadata; retain interruption kind and operation ordering.
  value=re.sub(r'#[0-9]+','<fiber-id>',value)
  return value
 def walk(x):
  if isinstance(x,str):return string(x)
  if isinstance(x,list):return [walk(i) for i in x]
  if isinstance(x,dict):return {k:walk(v) for k,v in x.items()}
  return x
 normalized=[]
 for ev in data:
  if ev.get('kind')=='checkpoint':
   summary=ev['data'].get('launcherAndSiblings')
   if isinstance(summary,dict) and 'manifest' in summary:
    manifest=raw.parent/summary['manifest'];entries=json.loads(manifest.read_text())
    assert hashlib.sha256(manifest.read_bytes().removesuffix(b'\n')).hexdigest()==summary['manifestSHA256']
    selected=[item for item in entries if item['path'].endswith('/t3') or item['path'].endswith('/client/index.html') or item['path'].endswith('/.install-complete')]
    ev['data']['runtimeHashesFromManifest']={'source':'driver-written full manifest, SHA256 verified by normalizer','entries':selected}
  ev=walk(ev)
  if ev['kind']=='header':ev['data']['runRoot']='<RUN_ROOT>'
  normalized.append(ev)
 dest=raw.with_name(raw.name.replace('.raw.txt','.transcript.txt'))
 dest.write_text('\n\n'.join(json.dumps(ev,indent=2,ensure_ascii=False) for ev in normalized)+'\n')
 # Metadata remains present in primary transcripts. Omit only known provenance metadata from comparisons.
 def compare_walk(x):
  if isinstance(x,list):return [compare_walk(i) for i in x]
  if isinstance(x,dict):return {k:compare_walk(v) for k,v in x.items() if k not in {'sourceRevision','side','driverSHA256','manifest'}}
  return x
 comparison=[compare_walk(ev) for ev in normalized]
 compare=dest.with_name(dest.name.replace('.transcript.txt','.comparison.txt'))
 compare.write_text('\n\n'.join(json.dumps(ev,indent=2,ensure_ascii=False) for ev in comparison)+'\n')
 failures=[ev for ev in normalized if ev['kind']=='DRIVER_FAILURE' or (ev['kind']=='assert' and not ev['data']['pass'])]
 checkpoints=[{'row':ev['data']['row'],'label':ev['data']['label'],'current':ev['data']['status'].get('current'),'installed':ev['data']['status'].get('installed'),'version':ev['data']['status'].get('installedVersion'),'marker':ev['data']['restartMarker'],'program':ev['data']['parsedProgram']} for ev in normalized if ev['kind']=='checkpoint']
 return {**entry,'transcript':str(dest),'comparison':str(compare),'failureCount':len(failures),'checkpoints':checkpoints,'kinds':dict(collections.Counter(ev['kind'] for ev in normalized))}

if __name__=='__main__':
 manifests=[pathlib.Path(p) for p in sys.argv[1:]]
 entries=[]
 for manifest in manifests:
  for entry in json.loads(manifest.read_text()):entries.append(normalize(entry))
 result={}; grouped=collections.defaultdict(dict)
 for ent in entries:grouped[ent['bundle']][ent['side']]=ent
 for bundle,sides in grouped.items():
  diff=S/(bundle+'.diff')
  if 'before' in sides and 'after' in sides:
   before=pathlib.Path(sides['before']['comparison']);after=pathlib.Path(sides['after']['comparison'])
   lines=list(difflib.unified_diff(before.read_text().splitlines(keepends=True),after.read_text().splitlines(keepends=True),fromfile=bundle+'-before@36d4d711b79c',tofile=bundle+'-after@7a90b56c54a9'))
   diff.write_text(''.join(lines) if lines else '# No observable differences after path/provenance normalization.\n')
  else:diff.write_text('# Head-only branches: no merge-base implementation exists; no Before capture or comparison is claimed.\n')
  result[bundle]={'sides':sides,'diff':str(diff)}
 (S/'normalized-manifest.json').write_text(json.dumps(result,indent=2)+'\n')
 print(json.dumps({k:{s:{'failures':v['failureCount'],'checkpoints':len(v['checkpoints']),'asserts':v['kinds'].get('assert',0)} for s,v in x['sides'].items()} for k,x in result.items()},indent=2))
