#!/usr/bin/env python3
from __future__ import annotations
import hashlib, json, os, stat, zipfile
from pathlib import Path
from typing import Any
ROOT=Path(__file__).resolve().parent
HERMES=Path('/home/cube/projects/richard/hermes-agent')
PROFILE=Path('/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli')
BASE=Path('/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-strict-final-candidate-successor-v3-st_01a00f35')
BASE_CANDIDATE='b6d78bc1e68ead7340d92b534fe3a6d0257c8aa2b02cd3d58e43215fbd8a3443'
BASE_PRODUCT='dac4e81281e8ab7f5c46461e79d405998e0273fe9f98dc09ff67b73528083092'
EXCLUDED={'.git','.venv','venv','.venv-task26-preterminal-retained','node_modules','.pytest-cache','.pytest_cache','.ruff_cache','__pycache__','build','dist','.gjc','.plans','.task26-evidence','.task26-owner-v1-repair-v4','.task26-owner-v1-repair-v5','.task26-owner-v1-repair-v6','.task26-owner-v1-snapshots','hermes_agent.egg-info','physique_checkin_cli.egg-info'}
def sha(p:Path)->str:return hashlib.sha256(p.read_bytes()).hexdigest()
def canon(v:Any)->bytes:return (json.dumps(v,sort_keys=True,separators=(',',':'))+'\n').encode()
def inventory(base:Path,label:str)->list[dict[str,Any]]:
 rows=[]
 for cur,dirs,files in os.walk(base,followlinks=False):
  dirs[:]=sorted(d for d in dirs if d not in EXCLUDED and not d.endswith('.egg-info'))
  for name in sorted(files):
   p=Path(cur)/name; rel=p.relative_to(base).as_posix()
   if any(x in EXCLUDED or x.endswith('.pyc') for x in Path(rel).parts):continue
   st=p.lstat()
   if not stat.S_ISREG(st.st_mode):raise RuntimeError(f'unsupported input {p}')
   rows.append({'root':label,'path':rel,'size':st.st_size,'sha256':sha(p),'mode':f'{stat.S_IMODE(st.st_mode):04o}'})
 return rows
def wheel_parity(wheel:Path, roots:dict[str,Path])->dict[str,Any]:
 compared=0; missing=[]; mismatch=[]
 with zipfile.ZipFile(wheel) as z:
  for member in z.namelist():
   if not member.endswith('.py') or '.dist-info/' in member or '.data/' in member:continue
   candidates=[]
   for root in roots.values():
    p=root/member
    if p.is_file():candidates.append(p)
   if not candidates:
    missing.append(member);continue
   compared+=1
   if all(hashlib.sha256(z.read(member)).hexdigest()!=sha(p) for p in candidates):mismatch.append(member)
 return {'compared':compared,'unmapped':missing,'mismatches':mismatch,'status':'PASS' if not missing and not mismatch else 'FAIL'}
def main()->None:
 base_rows=json.loads((BASE/'product-inventory.json').read_text())
 rows=inventory(HERMES,'hermes')+inventory(PROFILE,'profile')
 base={(r['root'],r['path']):r for r in base_rows}; now={(r['root'],r['path']):r for r in rows}
 changed=[{'root':k[0],'path':k[1],'before_sha256':base[k]['sha256'],'after_sha256':now[k]['sha256']} for k in sorted(base.keys()&now.keys()) if base[k]['sha256']!=now[k]['sha256']]
 added=[{'root':k[0],'path':k[1]} for k in sorted(now.keys()-base.keys())]
 removed=[{'root':k[0],'path':k[1]} for k in sorted(base.keys()-now.keys())]
 expected=['gateway/platforms/dualcoach_activation_cutover.py','gateway/platforms/telegram_nutrition_onboarding_runtime.py','tests/gateway/test_dualcoach_activation_cutover.py','tests/gateway/test_telegram_nutrition_onboarding.py']
 if [r['path'] for r in changed]!=expected or added or removed:raise RuntimeError(f'unexpected source delta changed={changed} added={added} removed={removed}')
 hw=ROOT/'artifacts/hermes_agent-0.17.0-py3-none-any.whl'; pw=ROOT/'artifacts/physique_checkin_cli-0.1.0-py3-none-any.whl'
 parity=wheel_parity(hw,{'hermes':HERMES})
 if parity['status']!='PASS':raise RuntimeError(f'wheel parity failed: {parity}')
 profile_sha=sha(pw)
 if profile_sha!='fb48a1931828ee60ab3812faf795550bf4e672a54c56f78cd10dcf674c45560d':raise RuntimeError('profile wheel changed')
 inv_digest=hashlib.sha256(canon(rows)).hexdigest(); delta_digest=hashlib.sha256(canon(changed)).hexdigest()
 product_bindings={'inventory_digest':inv_digest,'hermes_wheel_sha256':sha(hw),'profile_wheel_sha256':profile_sha,'predecessor_product':BASE_PRODUCT,'source_delta_digest':delta_digest}
 product=hashlib.sha256(canon(product_bindings)).hexdigest()
 receipt_names=('diagnostics.log','related-gateway.log','profile-full.log','full-gateway-pytest.log','wheel-hashes.txt')
 receipts={name:sha(ROOT/'receipts'/name) for name in receipt_names}
 candidate_bindings={'product_digest':product,'predecessor_candidate':BASE_CANDIDATE,'predecessor_product':BASE_PRODUCT,'receipts':receipts,'profile_source_changed':False,'zero_unindexed_source':not added and not removed}
 candidate=hashlib.sha256(canon(candidate_bindings)).hexdigest()
 (ROOT/'product-inventory.json').write_bytes(canon(rows)); (ROOT/'source-delta.json').write_bytes(canon({'changed':changed,'added':added,'removed':removed,'status':'PASS_EXACT_FOUR'}))
 manifest={'schema':'task26-onboarding-package-origin-successor-v1','status':'SEALED_VERIFIED','successor_candidate_digest':candidate,'successor_product_digest':product,'lineage':{'candidate':BASE_CANDIDATE,'product':BASE_PRODUCT},'inventory_count':len(rows),'inventory_digest':inv_digest,'source_delta':changed,'zero_unindexed_source':True,'wheels':{'hermes':sha(hw),'profile':profile_sha},'profile_wheel_preserved_exact':True,'source_wheel_parity':parity,'tests':{'changed':'69 passed','focused_related_gateway':'296 passed','profile':'683 passed','full_gateway':'8191 passed, 59 skipped'},'bindings':candidate_bindings}
 (ROOT/'candidate-manifest.json').write_bytes(canon(manifest)); (ROOT/'SEAL.json').write_bytes(canon({'schema':'task26-successor-seal-v1','status':'SEALED_VERIFIED','candidate':candidate,'product':product,'manifest_sha256':sha(ROOT/'candidate-manifest.json'),'inventory_sha256':sha(ROOT/'product-inventory.json'),'source_delta_sha256':sha(ROOT/'source-delta.json'),'wheels':manifest['wheels']}))
 print(json.dumps({'candidate':candidate,'product':product,'inventory':inv_digest,'count':len(rows),'hermes':sha(hw),'profile':profile_sha},sort_keys=True))
if __name__=='__main__':main()
