import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import time

ROOT=Path('/data/contextra/deployment-artifacts/icerti-full-20260919-r01')
REPO=Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
os.umask(0o077)
def digest(p):
    h=hashlib.sha256()
    with p.open('rb') as f:
        for chunk in iter(lambda:f.read(1024**2),b''):h.update(chunk)
    return h.hexdigest()
def save(p,obj):
    with p.open('x') as f: json.dump(obj,f,indent=2)
images=json.loads((ROOT/'private/image-inputs.json').read_text())
probe='import pathlib,hashlib,json; r=pathlib.Path(%r); print(json.dumps({str(p.relative_to(r)):hashlib.sha256(p.read_bytes()).hexdigest() for p in r.rglob("*") if p.is_file() and p.suffix in (".py",".tmpl",".txt",".json")}))'
checks={}
for service,local,remote in [('analytics','app','/opt/contextra/app'),('ai-control-sidecar','control_sidecar','/opt/bridge/control_sidecar')]:
    raw=subprocess.check_output(['docker','run','--rm','--pull','never','--network','none','--cpus','1','--memory','1g','--entrypoint','python3',images[service]['id'],'-B','-c',probe%remote],text=True)
    actual=json.loads(raw)
    expected={str(p.relative_to(REPO/local)):digest(p) for p in (REPO/local).rglob('*') if p.is_file() and p.suffix in ('.py','.tmpl','.txt','.json')}
    delta=[n for n in set(actual)|set(expected) if actual.get(n)!=expected.get(n)]
    checks[service]={'image_id':images[service]['id'],'files':len(actual),'differences':delta}
save(ROOT/'evidence/image-source-check.json',checks)
assert all(not v['differences'] for v in checks.values()),checks
inputs={
    'medium':Path('/data/contextra/model-artifacts/builds/medium-193-20260909-v4/export/model.onnx'),
    'pose':Path('/data/contextra/model-artifacts/candidates/batch8-6367102/sources/RFDETRKeypointPreview.onnx'),
    'fire':Path('/data/contextra/model-artifacts/builds/fire-smoke-193-20260913-r01/model.onnx')}
models={}
for name,p in inputs.items():
    dst=ROOT/'shared/models'/name/'model.onnx'; dst.parent.mkdir(parents=True)
    shutil.copyfile(p,dst)
    models[name]={'source':str(p),'file':str(dst.relative_to(ROOT/'shared')),'sha256':digest(dst),'size':dst.stat().st_size}
save(ROOT/'private/model-inputs.json',models)
builder='contextra-model-tooling:icerti-80ad438-20260919-r01'
bd=json.loads(subprocess.check_output(['docker','image','inspect',builder],text=True))[0]
save(ROOT/'private/builder.json',{'reference':builder,'id':bd['Id'],'size':bd['Size'],'engine_execution':'NOT_RUN'})
tags=list(dict.fromkeys([i['reference'] for i in images.values()]+[builder]))
archive=ROOT/'shared/images/runtime-and-tooling.tar'; archive.parent.mkdir()
intent={'status':'INTENT','tags':tags,'output':str(archive),'timeout_s':900,'disk_floor_gib':15}
save(ROOT/'evidence/image-save-intent.json',intent)
with (ROOT/'evidence/image-save-stderr.log').open('xb') as err:
    p=subprocess.Popen(['docker','save','-o',str(archive),*tags],stdout=subprocess.DEVNULL,stderr=err)
    deadline=time.monotonic()+900
    try:
        while p.poll() is None:
            if time.monotonic()>deadline or min(shutil.disk_usage('/').free,shutil.disk_usage(ROOT).free)<15*1024**3:
                p.terminate(); raise RuntimeError('SAVE_TIME_OR_CAPACITY_LIMIT_NO_RETRY')
            time.sleep(1)
        assert p.returncode==0,'SAVE_FAILED_NO_RETRY'
    finally:
        if p.poll() is None: p.terminate()
        p.wait(timeout=10)
save(ROOT/'evidence/image-save-result.json',{'status':'PASS','sha256':digest(archive),'size':archive.stat().st_size})
print(json.dumps({'image_source':checks,'image_archive_bytes':archive.stat().st_size}))
