from pathlib import Path
import os,sys,hashlib,json,platform,shutil,subprocess,tomllib
here=Path(__file__).resolve().parent; root=here/'working'/'polyglot'

def h(p): return hashlib.sha256(Path(p).read_bytes()).hexdigest()
def ident_exe(exe,args=['--version'],cwd=None):
    if not exe: return {'status':'UNAVAILABLE','executable':None,'version':None,'rc':None}
    p=subprocess.run([exe,*args],cwd=cwd,capture_output=True,text=True)
    return {'status':'OBSERVED' if p.returncode==0 else 'ERROR','executable':str(Path(exe).resolve()),'version':(p.stdout or p.stderr).strip().splitlines()[0] if (p.stdout or p.stderr).strip() else None,'rc':p.returncode}
def ident(cmd,args=['--version'],cwd=None): return ident_exe(shutil.which(cmd),args,cwd)
def inv(d):
    d=Path(d); files=[]
    if not d.exists(): return {'exists':False,'count':0,'sha256':None,'files':[]}
    for p in sorted(d.rglob('*')):
        if p.is_file(): files.append({'path':p.relative_to(d).as_posix(),'sha256':h(p),'bytes':p.stat().st_size})
    canon=''.join(f"{x['path']}\t{x['sha256']}\t{x['bytes']}\n" for x in files).encode()
    return {'exists':True,'count':len(files),'sha256':hashlib.sha256(canon).hexdigest(),'files':files}
def rel_to_poly(p):
    try: return p.resolve().relative_to(root.resolve()).as_posix()
    except Exception: return p.resolve().as_posix()
def rust_path_deps():
    rust=root/'rust'; ws=tomllib.loads((rust/'Cargo.toml').read_text()); members=ws.get('workspace',{}).get('members',[])
    member_paths={(rust/m).resolve() for m in members}; out=[]
    for member in members:
        mf=rust/member/'Cargo.toml'
        if not mf.exists(): continue
        data=tomllib.loads(mf.read_text())
        for sec in ('dependencies','dev-dependencies','build-dependencies'):
            for name,spec in data.get(sec,{}).items():
                if isinstance(spec,dict) and 'path' in spec:
                    target=(mf.parent/spec['path']).resolve()
                    out.append({'member':member,'dependency':name,'section':sec,'declared_path':spec['path'],'resolved_path':rel_to_poly(target),'exists':target.exists(),'is_workspace_member':target in member_paths,'inventory':inv(target)})
    return out
if not root.exists(): raise SystemExit('run setup_fixture.py first')
cfg=json.load(open(root/'closure_config.json'))
py=ident_exe(sys.executable)
tools={'node':ident('node'),'pnpm':ident('pnpm'),'python':py,'uv':ident('uv'),'cargo':ident('cargo',cwd=root/'rust'),'rustc':ident('rustc',cwd=root/'rust'),'rustup':ident('rustup',cwd=root/'rust')}
active=ident('rustup',['show','active-toolchain'],cwd=root/'rust') if shutil.which('rustup') else {'status':'UNAVAILABLE','executable':None,'version':None,'rc':None}
out={
 'schema':'TOL006_OBSERVED_CLOSURE_v2',
 'target':{'system':platform.system(),'release':platform.release(),'machine':platform.machine(),'python_platform':platform.platform()},
 'target_assumptions_declared':cfg.get('target_assumptions',{}),
 'environment':{'MAKNOW_PROFILE':os.environ.get('MAKNOW_PROFILE'),'MAKNOW_NOISE_observed_but_out_of_claim':os.environ.get('MAKNOW_NOISE'),'relevant_expected':cfg['relevant_environment']},
 'tools':tools,
 'rustup_active_toolchain':active,
 'authority':{
   'js_workspace':{'pnpm_workspace_sha256':h(root/'js'/'pnpm-workspace.yaml'),'pnpm_lock_sha256':h(root/'js'/'pnpm-lock.yaml')},
   'python_project':{'pyproject_sha256':h(root/'python'/'pyproject.toml'),'uv_lock_sha256':h(root/'python'/'uv.lock')},
   'rust_workspace':{'cargo_toml_sha256':h(root/'rust'/'Cargo.toml'),'cargo_lock_sha256':h(root/'rust'/'Cargo.lock'),'rust_toolchain_sha256':h(root/'rust'/'rust-toolchain.toml')},
   'closure_config_sha256':h(root/'closure_config.json')},
 'local_artifacts':{
   'pnpm_prepared_store':inv(root/'js'/'prepared_store'),
   'python_wheelhouse':inv(root/'python'/'wheelhouse'),
   'rust_workspace_crates':inv(root/'rust'/'crates'),
   'rust_path_dependencies':rust_path_deps()}
}
out['relevant_environment_match']=all(out['environment'].get(k)==v for k,v in cfg['relevant_environment'].items())
out['tool_expectation_mismatches']=[]
for nm,expected in cfg.get('tool_expectations',{}).items():
    if expected=='OBSERVE_AND_RECORD': continue
    observed=(out.get('tools',{}).get(nm) or {}).get('version','UNAVAILABLE')
    if expected not in str(observed): out['tool_expectation_mismatches'].append({'tool':nm,'expected':expected,'observed':observed})
print(json.dumps(out,indent=2))
