import hashlib, importlib.util, json, os, pathlib, shutil, signal, subprocess, sys, tarfile, time
os.umask(0o077)
spec=importlib.util.spec_from_file_location('oldgate','/tmp/codex/ltb1021/2026-09-11_service-stop_r01/gate.py')
g=importlib.util.module_from_spec(spec);spec.loader.exec_module(g)
R=pathlib.Path(__file__).parent
HEAD='769ed3a7aad18f06c2bb43e815d36fd630d43f48'
EDITED=['app/start_diagnostics.py','app/stream_lifecycle.py','app/multi_pipeline.py','app/stream_manager.py','tests/test_stop_observation.py','tests/sdk_stop_observation_probe.py']
def fingerprint():return {p:g.sha(g.REPO/p) for p in EDITED}
def source_cas():
    assert g.sh(['git','-C',str(g.REPO),'rev-parse','HEAD']).strip()==HEAD
    assert fingerprint()==json.loads((R/'source-manifest.json').read_text())['edited']
def gate():
    assert not (R/'gate.json').exists()
    assert g.sh(['git','-C',str(g.REPO),'rev-parse','HEAD']).strip()==HEAD
    assert sorted(l[3:] for l in g.sh(['git','-C',str(g.REPO),'status','--porcelain']).splitlines())==sorted(EDITED)
    assert not g.sh(['git','-C',str(g.REPO),'diff','--cached','--name-only']).strip()
    root=g.sh(['docker','info','--format','{{.DockerRootDir}}']).strip()
    free=min(shutil.disk_usage('/').free,shutil.disk_usage(root).free);assert free>=15*1024**3
    assert subprocess.run(['pgrep','-f',r'(^|/)docker (build|buildx build|pull)'],capture_output=True).returncode==1
    c=g.inspect();assert c['State']['Status']=='exited' and not c['State']['OOMKilled'] and c['RestartCount']==0
    assert c['Image']=='sha256:228fc2493dcb81454453d05a66528dd5927d74a40fb85f76d6950afbf0b56d1a'
    assert c['HostConfig']['RestartPolicy']=={'Name':'on-failure','MaximumRetryCount':3}
    e=g.load_env_file(str(g.ENV));overlay=pathlib.Path(e['CONTEXTRA_RUNTIME_STATE_ROOT'])/'streams_runtime.yaml'
    assert e['CONTEXTRA_IMAGE']=='contextra-analytics:callback-344164e-7f88bdb7'
    assert g.sha(g.CFG)=='5e5a4d22786f8b5218ac821379fbec091d9e3006f62c880ee3e3da5ced2f0e0b'
    assert g.sha(g.CFG.parent/'2/model.plan')=='5fdf1e9c55ab947a3b7d5f7f6bdceaf1e36a9e52efdf86f5a15d3b99b8631d1e'
    assert g.sha(overlay)=='ff73d70010b7dbd2c0f0edba2fc9a389b151910bc74abc4b0ba80ab2fe3ac10e'
    rows=g.yaml.safe_load(overlay.read_text())['added'];assert len(rows)==1;s=rows[0]
    assert all(s[x] is False for x in ['store','active_learning','autostart']) and s['scenarios']=='off'
    assert s['inference_profile']['roles']==['det'] and s['inference_profile']['det_model']=='RFDETRMedium'
    mem={l.split(':')[0]:int(l.split()[1]) for l in pathlib.Path('/proc/meminfo').read_text().splitlines()}
    assert mem['MemAvailable']>10*1024**2 and g.gpu()<16384
    edited=fingerprint();digest=hashlib.sha256(json.dumps(edited,sort_keys=True).encode()).hexdigest()
    tag='contextra-analytics:stop60-769ed3a-'+digest[:8]
    assert subprocess.run(['docker','image','inspect',tag],capture_output=True).returncode!=0
    from tools.validate_deployment import validate_deployment_inputs
    validate_deployment_inputs(str(g.ENV))
    snap=R/'snapshot';snap.mkdir(mode=0o700)
    p=subprocess.Popen(['git','-C',str(g.REPO),'archive',HEAD],stdout=subprocess.PIPE)
    with tarfile.open(fileobj=p.stdout,mode='r|') as archive:archive.extractall(snap,filter='data')
    assert p.wait()==0
    for path in EDITED:shutil.copyfile(g.REPO/path,snap/path)
    assert fingerprint()==edited
    (R/'source-manifest.json').write_text(json.dumps(dict(base=HEAD,edited=edited,digest=digest),indent=2))
    (R/'source-hashes.json').write_text(json.dumps({str(p.relative_to(snap)):g.sha(p) for p in (snap/'app').rglob('*.py')},indent=2))
    (R/'diagnostics').mkdir(mode=0o700)
    out=dict(head=HEAD,tag=tag,source_digest=digest,container_id=c['Id'],old_image=c['Image'],old_tag=e['CONTEXTRA_IMAGE'],
             env_hash=g.sha(g.ENV),config_hash=g.sha(g.CFG),plan_hash=g.sha(g.CFG.parent/'2/model.plan'),overlay_hash=g.sha(overlay),
             original_restart=c['HostConfig']['RestartPolicy'],other_containers=g.others(),gpu_mib=g.gpu(),docker_root=root,disk_free_bytes=free)
    (R/'gate.json').write_text(json.dumps(out,indent=2))
    print(json.dumps({k:out[k] for k in ['tag','disk_free_bytes','gpu_mib','source_digest']}))
def build():
    source_cas();assert not (R/'build.json').exists()
    assert json.loads((R/'source-verify.json').read_text())['status']=='PASS'
    gate=json.loads((R/'gate.json').read_text());start=time.monotonic();reason=None
    with (R/'build.log').open('w') as log:
        p=subprocess.Popen(['docker','build','-f','docker/Dockerfile','--target','app','-t',gate['tag'],'.'],cwd=R/'snapshot',stdout=log,stderr=log,start_new_session=True,env=dict(os.environ,DOCKER_BUILDKIT='1'))
        while p.poll() is None:
            if time.monotonic()-start>1200 or min(shutil.disk_usage('/').free,shutil.disk_usage(gate['docker_root']).free)<15*1024**3:
                reason='TIME_OR_DISK_CAP';os.killpg(p.pid,signal.SIGTERM)
                try:p.wait(15)
                except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL)
                break
            time.sleep(2)
        code=p.wait()
    out=dict(returncode=code,abort_reason=reason,elapsed_s=round(time.monotonic()-start,2),tag=gate['tag'])
    if code==0 and reason is None:out['image_id']=json.loads(g.sh(['docker','image','inspect',gate['tag']]))[0]['Id']
    (R/'build.json').write_text(json.dumps(out,indent=2));print(json.dumps(out));assert code==0 and reason is None
if __name__=='__main__':
    {'gate':gate,'build':build}[sys.argv[1]]()
