import hashlib
import json
import os
from pathlib import Path
import shlex
import subprocess
import tempfile
import time
import urllib.request

os.umask(0o077)
root=Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
ev=Path('/tmp/codex/ltb1021/20260909-identity-retry')
baseline=json.loads((ev/'baseline.json').read_text())
env_path=Path('/workspace/contextra-runtime-config/runtime.env')
old_tag='contextra-analytics:smoke-be367b7'
new_tag='contextra-analytics:smoke-b3868cf'
old_id='sha256:fc7ab200686839877989c7ea2c05426c6079377b4f4dc8448a29d068df054d8f'
new_id='sha256:d8931648af3b12fdff3e5eb87e2597d38818bb145c2590ae0f910711da72ca17'
compose=['docker','compose','--env-file',str(env_path),'-f',str(root/'docker-compose.yml')]
def call(args):
    return subprocess.check_output(args,text=True,stderr=subprocess.PIPE).strip()
def write_state(phase,**extra):
    (ev/'deploy_state.json').write_text(json.dumps({'phase':phase,**extra},indent=2))
def get(path):
    with urllib.request.urlopen(f'http://127.0.0.1:{baseline["port"]}{path}',timeout=5) as r:
        assert r.status==200
        return json.load(r)
def streams():
    data=get('/api/v1/streams')['streams']
    if isinstance(data,dict): data=list(data.values())
    assert len(data)==baseline['stream_count']
    assert all(s.get('state')=='stopped' and s.get('desired_state')=='stopped'
               and s.get('observed_state')=='stopped' for s in data)
    return data
def swap(expected,replacement):
    content=env_path.read_text()
    lines=content.splitlines(keepends=True)
    indices=[i for i,line in enumerate(lines) if line.startswith('CONTEXTRA_IMAGE=')]
    assert len(indices)==1
    i=indices[0]
    assert lines[i].split('=',1)[1].strip().strip('"\'')==expected, 'env CAS drift'
    lines[i]='CONTEXTRA_IMAGE='+replacement+('\n' if lines[i].endswith('\n') else '')
    fd,tmp=tempfile.mkstemp(prefix='.codex-image-',dir=env_path.parent)
    try:
        with os.fdopen(fd,'w') as f:
            f.write(''.join(lines)); f.flush(); os.fsync(f.fileno())
        assert env_path.read_text()==content, 'env content changed during CAS'
        os.replace(tmp,env_path)
    finally:
        if os.path.exists(tmp): os.unlink(tmp)
def verify_ready(expected):
    end=time.monotonic()+300
    while time.monotonic()<end:
        try:
            data=json.loads(call(['docker','inspect','contextra-analytics-1','--format',
                '{{json .State}}']))
            if data.get('Health',{}).get('Status')!='healthy':
                time.sleep(2); continue
            assert call(['docker','inspect','contextra-analytics-1','--format','{{.Image}}'])==expected
            get('/live')
            for path in ('/ready','/health'):
                health=get(path)
                assert health['ready'] and health['model_name']=='RFDETRMedium'
            return streams()
        except Exception:
            time.sleep(2)
    raise RuntimeError('readiness timeout')
def protect_snapshot():
    actual={}
    fmt='{{json .Name}} {{json .Id}} {{json .Image}} {{.RestartCount}} {{.State.OOMKilled}} {{.State.Status}} {{if index .State "Health"}}{{.State.Health.Status}}{{else}}none{{end}}'
    for cid in call(['docker','ps','-aq']).splitlines():
        name,identifier,image,restarts,oom,status,health=shlex.split(call(['docker','inspect','--format',fmt,cid]))
        if name=='/contextra-analytics-1': continue
        actual[hashlib.sha256(name.encode()).hexdigest()]={'id':identifier,'image':image,'restart':int(restarts),'oom':oom,'status':status,'health':health}
    return actual

assert not (ev/'deploy_state.json').exists(), 'deployment already attempted; inspect state before resume'
assert call(['git','-C',str(root),'rev-parse','HEAD'])=='b3868cfe2d1287ecb49296e6119174fa2b9036e8'
assert not call(['git','-C',str(root),'status','--porcelain'])
assert call(['docker','image','inspect',new_tag,'--format','{{.Id}}'])==new_id
assert call(['docker','image','inspect',old_tag,'--format','{{.Id}}'])==old_id
assert call(['docker','inspect','contextra-analytics-1','--format','{{.Id}}'])==baseline['analytics']['id']
assert call(['docker','inspect','contextra-analytics-1','--format','{{.Image}}'])==old_id
assert protect_snapshot()==baseline['protected']
streams()
networks=call(['docker','network','ls','-q'])
volumes=call(['docker','volume','ls','-q'])
(ev/'old_image.txt').write_text(old_tag+'\n')
write_state('prechange')
changed=False
try:
    swap(old_tag,new_tag)
    changed=True
    write_state('env_switched')
    call(['python3',str(root/'tools/validate_deployment.py'),'--env-file',str(env_path)])
    call(compose+['config','--quiet'])
    write_state('recreate_requested')
    call(compose+['up','-d','--no-deps','--force-recreate','--pull','never','analytics'])
    data=verify_ready(new_id)
    assert all(s.get('run_generation')==0 and 'run_id' in s and s['run_id'] is None for s in data)
    assert protect_snapshot()==baseline['protected']
    assert call(['docker','network','ls','-q'])==networks
    assert call(['docker','volume','ls','-q'])==volumes
    assert env_path.stat().st_mode & 0o777==0o600
    write_state('PASS',image_id=new_id,stream_count=len(data),stream_stopped=True,
                run_generation=0,run_id=None,protected_containers_unchanged=len(baseline['protected']))
    print('PASS: exact image, readiness, stopped generation 0, protected containers and networks/volumes unchanged')
except Exception as error:
    write_state('failure',error_type=type(error).__name__)
    if changed:
        try:
            swap(new_tag,old_tag)
            write_state('rollback_requested')
            call(compose+['up','-d','--no-deps','--force-recreate','--pull','never','analytics'])
            verify_ready(old_id)
            write_state('ROLLED_BACK',error_type=type(error).__name__)
            print('ROLLED_BACK: previous Analytics image ready')
        except Exception as rollback_error:
            write_state('FAIL_ROLLBACK',error_type=type(rollback_error).__name__)
            print('FAIL_ROLLBACK')
    else:
        print('BLOCKED before env transition')
    raise SystemExit(1)
