#!/usr/bin/env python3
"""Deterministic local memory patch lineage and bundle composition.

Local content-addressed integrity/recovery evidence only. It does not establish
signing, authorship, timestamp authority, semantic truth, public witness
consensus, production disaster recovery, deployment, domain control, or
real-world authorization.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import argparse, hashlib, json, re, shutil, sys

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import memory_repair_transaction as tx

SCHEMA='memory-patch-lineage/1'
PROTECTED=set(tx.PROTECTED)
class LineageError(RuntimeError): pass

def sha_bytes(b:bytes)->str:return hashlib.sha256(b).hexdigest()
def load(p:Path)->dict[str,Any]:return json.loads(p.read_text(encoding='utf-8'))

def release_tuple(v:str)->tuple[int,...]:
    m=re.fullmatch(r'v(\d{4})\.(\d{2})\.(\d{2})\.(\d+)',v or '')
    if not m: raise LineageError('bad_release:'+str(v))
    return tuple(map(int,m.groups()))

def graph_material(nodes:list[dict[str,Any]],edges:list[dict[str,Any]])->bytes:
    lines=[]
    for n in sorted(nodes,key=lambda x:x['snapshot_id']):
        lines.append('\0'.join(['N',n['snapshot_id'],n['release'],n['snapshot_digest_sha256']]))
    for e in sorted(edges,key=lambda x:(x['base_snapshot_id'],x['target_snapshot_id'],x['bundle_id'])):
        lines.append('\0'.join(['E',e['bundle_id'],e['base_snapshot_id'],e['target_snapshot_id'],e['bundle_digest_sha256']]))
    return ('\n'.join(lines)+'\n').encode()

def build_graph(snapshot_docs:list[dict[str,Any]], bundle_docs:list[dict[str,Any]])->dict[str,Any]:
    nodes=[]; seen=set()
    for s in snapshot_docs:
        sid=s['snapshot_id']
        if sid in seen: raise LineageError('duplicate_snapshot')
        seen.add(sid)
        nodes.append({'snapshot_id':sid,'release':s['release'],'snapshot_digest_sha256':s['snapshot_digest_sha256'],'file_count':s['closure']['file_count']})
    edges=[]; seenb=set()
    for b in bundle_docs:
        if b['bundle_id'] in seenb: raise LineageError('duplicate_bundle')
        seenb.add(b['bundle_id'])
        edges.append({'bundle_id':b['bundle_id'],'bundle_digest_sha256':b['bundle_digest_sha256'],'base_snapshot_id':b['base_snapshot_id'],'target_snapshot_id':b['target_snapshot_id'],'base_release':b['base_release'],'target_release':b['target_release'],'action_count':len(b['actions'])})
    g={'schema':SCHEMA,'nodes':nodes,'edges':edges,'truth_boundary':'Local content-addressed lineage evidence only; not signing/authorship/timestamp authority, semantic truth, public witness consensus, production disaster recovery, deployment, domain control, or real-world authorization.'}
    validate_graph(g)
    dg=sha_bytes(graph_material(nodes,edges));g['lineage_digest_sha256']=dg;g['lineage_id']='lineage-'+dg[:20]
    return g

def validate_graph(g:dict[str,Any])->dict[str,Any]:
    if g.get('schema')!=SCHEMA: raise LineageError('schema')
    nodes={n['snapshot_id']:n for n in g.get('nodes',[])}
    if len(nodes)!=len(g.get('nodes',[])): raise LineageError('duplicate_snapshot')
    indeg={k:0 for k in nodes};outdeg={k:0 for k in nodes};adj={k:[] for k in nodes}
    seenb=set()
    for e in g.get('edges',[]):
        if e['bundle_id'] in seenb: raise LineageError('duplicate_bundle')
        seenb.add(e['bundle_id'])
        a=e['base_snapshot_id'];b=e['target_snapshot_id']
        if a not in nodes or b not in nodes: raise LineageError('missing_endpoint')
        if e.get('base_release')!=nodes[a]['release'] or e.get('target_release')!=nodes[b]['release']: raise LineageError('wrong_predecessor_metadata')
        if release_tuple(nodes[b]['release']) <= release_tuple(nodes[a]['release']): raise LineageError('downgrade_path')
        outdeg[a]+=1; indeg[b]+=1; adj[a].append(b)
        if outdeg[a]>1: raise LineageError('fork')
        if indeg[b]>1: raise LineageError('multiple_predecessors')
    color={k:0 for k in nodes}
    def dfs(v):
        color[v]=1
        for w in adj[v]:
            if color[w]==1: raise LineageError('cycle')
            if color[w]==0: dfs(w)
        color[v]=2
    for v in nodes:
        if color[v]==0: dfs(v)
    if 'lineage_digest_sha256' in g:
        calc=sha_bytes(graph_material(g['nodes'],g['edges']))
        if calc!=g['lineage_digest_sha256'] or g.get('lineage_id')!='lineage-'+calc[:20]: raise LineageError('lineage_digest')
    return {'pass':True,'node_count':len(nodes),'edge_count':len(g.get('edges',[]))}

def prove_path(g:dict[str,Any], start:str, end:str)->list[str]:
    validate_graph(g); nxt={e['base_snapshot_id']:e['target_snapshot_id'] for e in g['edges']}
    cur=start; path=[cur];seen=set()
    while cur!=end:
        if cur in seen: raise LineageError('cycle')
        seen.add(cur)
        if cur not in nxt: raise LineageError('gap')
        cur=nxt[cur];path.append(cur)
    return path

def _actions(b): return {a['path']:a for a in b['actions']}

def verify_composable(b1:dict[str,Any],b2:dict[str,Any],s0:dict[str,Any],s1:dict[str,Any],s2:dict[str,Any])->dict[str,Any]:
    if b1['base_snapshot_id']!=s0['snapshot_id'] or b1['target_snapshot_id']!=s1['snapshot_id']: raise LineageError('wrong_predecessor_first')
    if b2['base_snapshot_id']!=s1['snapshot_id'] or b2['target_snapshot_id']!=s2['snapshot_id']: raise LineageError('wrong_predecessor_second')
    a1=_actions(b1);a2=_actions(b2); overlap=sorted(set(a1)&set(a2)); conflicts=[]
    for p in overlap:
        if a1[p].get('after_sha256') != a2[p].get('before_sha256'):
            conflicts.append({'path':p,'first_after':a1[p].get('after_sha256'),'second_before':a2[p].get('before_sha256')})
    protected=sorted((set(a1)|set(a2)) & PROTECTED)
    if protected: raise LineageError('protected_anchor_conflict:'+','.join(protected))
    if conflicts: raise LineageError('intermediate_hash_conflict:'+conflicts[0]['path'])
    return {'pass':True,'overlap_count':len(overlap),'overlap_paths':overlap,'protected_anchor_changes':protected,'conflicts':conflicts}

def compose(b1,b2,s0,s1,s2,target_root:Path,payload_root:Path)->tuple[dict[str,Any],dict[str,Any]]:
    compat=verify_composable(b1,b2,s0,s1,s2)
    if payload_root.parent.exists(): shutil.rmtree(payload_root.parent)
    direct=tx.build_bundle(s0,s2,target_root,payload_root,None)
    direct['composition']={
        'schema':'memory-patch-composition/1','first_bundle_id':b1['bundle_id'],'second_bundle_id':b2['bundle_id'],
        'intermediate_snapshot_id':s1['snapshot_id'],'overlap_count':compat['overlap_count'],
        'rule':'direct bundle is emitted only after exact intermediate after/before hash reconciliation for every path touched by both predecessor bundles; protected-anchor mutations are rejected'
    }
    # composition metadata is outside legacy bundle digest material; keep canonical bundle digest stable.
    assert tx.compute_bundle_digest(direct)==direct['bundle_digest_sha256']
    result_material='\n'.join([b1['bundle_digest_sha256'],b2['bundle_digest_sha256'],direct['bundle_digest_sha256'],s0['snapshot_id'],s1['snapshot_id'],s2['snapshot_id'],*compat['overlap_paths']]).encode()
    result={'schema':'memory-patch-composition-results/1','pass':True,'first_bundle_id':b1['bundle_id'],'second_bundle_id':b2['bundle_id'],'composed_bundle_id':direct['bundle_id'],'base_snapshot_id':s0['snapshot_id'],'intermediate_snapshot_id':s1['snapshot_id'],'target_snapshot_id':s2['snapshot_id'],'overlap_count':compat['overlap_count'],'conflict_count':0,'protected_anchor_change_count':0,'composed_action_count':len(direct['actions']),'composition_digest_sha256':sha_bytes(result_material),'truth_boundary':'Local deterministic patch composition evidence only; not semantic authority, permission to change protected anchors, signing, authorship, deployment, or production recovery proof.'}
    return direct,result

def main():
    ap=argparse.ArgumentParser();ap.add_argument('--root',default=str(ROOT));ap.add_argument('--verify',action='store_true');args=ap.parse_args();r=Path(args.root)
    s4=load(r/'assets/data/snapshots/memory-state-snapshot-v2026.08.16.4.json');s5=load(r/'assets/data/snapshots/memory-state-snapshot-v2026.08.16.5.json');s6=load(r/'assets/data/snapshots/memory-state-snapshot-v2026.08.16.6.json')
    b45=load(r/'assets/data/patch-bundles/memory-patch-bundle-v16.4-to-v16.5.json');b56=load(r/'assets/data/patch-bundles/memory-patch-bundle-v16.5-to-v16.6.json')
    g=build_graph([s4,s5,s6],[b45,b56]); print(json.dumps({'graph':validate_graph(g),'path':prove_path(g,s4['snapshot_id'],s6['snapshot_id'])},ensure_ascii=False))
if __name__=='__main__':main()
