import sys, pathlib, json, os, tempfile, subprocess, hashlib, time, urllib.request, shutil
sys.path.insert(0,'/workspace/contextra-analytics-runtime-lifecycle-refactor')
from tools.validate_deployment import load_env_file, validate_deployment_inputs
from model_tooling.deploy import snapshot
from model_tooling.medium import digest
R=pathlib.Path('/tmp/codex/ltb1021/2026-09-10_medium-runtime_r01')
REPO=pathlib.Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
ENV=pathlib.Path('/workspace/contextra-runtime-config/runtime.env')
G=json.loads((R/'gate.json').read_text()); BUILD=json.loads((R/'build-result.json').read_text())
MODEL=pathlib.Path(G['target']); CFG=MODEL/'config.pbtxt'
def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest()
def atomic(p,data,mode=0o600):
 fd,name=tempfile.mkstemp(dir=p.parent,prefix='.contextra-deploy-')
 with os.fdopen(fd,'wb') as f: os.fchmod(f.fileno(),mode); f.write(data); f.flush(); os.fsync(f.fileno())
 os.replace(name,p)
 fd=os.open(p.parent,os.O_RDONLY); os.fsync(fd); os.close(fd)
def record(data): atomic(R/'deploy-state.json',json.dumps(data,indent=2).encode())
def request(path):
 env=load_env_file(str(ENV))
 with urllib.request.urlopen('http://127.0.0.1:'+env['FASTAPI_HOST_PORT']+path,timeout=5) as r: return json.load(r)
def stopped():
 streams=request('/api/v1/streams')['streams']
 assert len(streams)==G['stream_count']
 assert all(s['state']==s['desired_state']==s['observed_state']=='stopped' for s in streams)
 return streams
def inspect(): return json.loads(subprocess.check_output(['docker','inspect','contextra-analytics-1'],text=True))[0]
def set_image(expected,new):
 raw=ENV.read_bytes(); text=raw.decode(); lines=text.splitlines(keepends=True); matches=0
 for i,line in enumerate(lines):
  if line.split('=',1)[0].strip()=='CONTEXTRA_IMAGE':
   assert line.split('=',1)[1].strip()==expected
   lines[i]='CONTEXTRA_IMAGE='+new+('\n' if line.endswith('\n') else ''); matches+=1
 assert matches==1
 atomic(ENV,''.join(lines).encode())
def recreate(label):
 validate_deployment_inputs(str(ENV))
 base=['docker','compose','--env-file',str(ENV),'-f',str(REPO/'docker-compose.yml')]
 subprocess.run(base+['config','--quiet'],check=True,timeout=30)
 with (R/(label+'-compose.log')).open('w') as f:
  subprocess.run(base+['up','-d','--no-deps','--force-recreate','--pull','never','analytics'],check=True,stdout=f,stderr=subprocess.STDOUT,timeout=120)
def readiness(image):
 end=time.monotonic()+300
 while time.monotonic()<end:
  c=inspect()
  if c['Image']!=image: raise RuntimeError('image mismatch')
  if c['RestartCount'] or c['State']['OOMKilled']: raise RuntimeError('restart or OOM')
  try:
   for p in ('/live','/ready','/health'): request(p)
   if c['State'].get('Health',{}).get('Status')=='healthy':
    streams=stopped()
    return {'image':image,'health':'healthy','restart':0,'oom':False,'stream_count':len(streams),
      'generations':[s['run_generation'] for s in streams],'run_ids_null':all(s.get('run_id') is None for s in streams)}
  except Exception: pass
  time.sleep(3)
 raise RuntimeError('readiness timeout')
def rollback(reason):
 state=json.loads((R/'deploy-state.json').read_text())
 assert not state.get('rollback_attempted')
 state.update(rollback_attempted=True,reason=reason,stage='ROLLBACK_STARTED'); record(state)
 current_image=load_env_file(str(ENV))['CONTEXTRA_IMAGE']
 if current_image==G['new_tag']:
  assert sha(ENV)==state['applied_env_sha256']; set_image(G['new_tag'],G['rollback_tag'])
 else: assert current_image==G['rollback_tag']
 current_config=sha(CFG)
 if current_config!=G['model_snapshot']['files']['config.pbtxt']:
  assert current_config==state['applied_config_sha256']; atomic(CFG,(R/'model-config-before.pbtxt').read_bytes(),state['config_mode'])
 recreate('rollback')
 result=readiness(G['rollback_image'])
 state.update(stage='ROLLED_BACK',rollback=result); record(state); print(json.dumps({'stage':'ROLLED_BACK','readiness':result}))
if __name__=='__main__':
 if len(sys.argv)>1 and sys.argv[1]=='rollback':
  try: rollback('PIPELINE_VALIDATION_FAILED')
  except Exception as e: print('FAIL_ROLLBACK',type(e).__name__); raise SystemExit(1)
  raise SystemExit(0)
 assert not (R/'deploy-state.json').exists(), 'deployment state already exists; inspect before resume'
 assert json.loads((R/'image-verify.json').read_text())['status']=='PASS'
 assert subprocess.check_output(['git','-C',str(REPO),'rev-parse','HEAD'],text=True).strip()==G['source']
 assert not subprocess.check_output(['git','-C',str(REPO),'status','--porcelain'],text=True).strip()
 assert sha(ENV)==G['env_sha256'] and digest(snapshot(MODEL))==G['model_cas']
 assert inspect()['Image']==G['rollback_image']; stopped()
 version=MODEL/str(G['version']); assert not version.exists()
 source=pathlib.Path('/data/contextra/model-artifacts/builds/medium-193-20260909-v4/build/model.plan')
 assert sha(source)=='5fdf1e9c55ab947a3b7d5f7f6bdceaf1e36a9e52efdf86f5a15d3b99b8631d1e'
 os.umask(0o077)
 state={'stage':'STAGING','rollback_attempted':False,'config_mode':CFG.stat().st_mode & 0o777}
 atomic(R/'model-config-before.pbtxt',CFG.read_bytes()); record(state)
 try:
  version.mkdir(mode=0o755)
  partial=version/'model.plan.partial'
  with source.open('rb') as src, partial.open('xb') as dst: shutil.copyfileobj(src,dst); dst.flush(); os.fsync(dst.fileno())
  assert sha(partial)==sha(source); partial.chmod(0o644); os.replace(partial,version/'model.plan')
  assert sha(ENV)==G['env_sha256'] and sha(CFG)==G['model_snapshot']['files']['config.pbtxt']
  candidate=(R/'model-config-candidate.pbtxt').read_bytes()
  state.update(stage='APPLY_STARTED',applied_config_sha256=hashlib.sha256(candidate).hexdigest()); record(state)
  atomic(CFG,candidate,state['config_mode'])
  set_image(G['rollback_tag'],G['new_tag'])
  state.update(stage='CONFIG_ENV_APPLIED',applied_env_sha256=sha(ENV)); record(state)
  recreate('apply')
  ready=readiness(BUILD['image_id'])
  state.update(stage='INFRA_READY',readiness=ready); record(state)
  print(json.dumps({'stage':'INFRA_READY','version':G['version'],'readiness':ready}))
 except Exception as e:
  print('DEPLOY_FAILED',type(e).__name__)
  if state['stage'] in ('APPLY_STARTED','CONFIG_ENV_APPLIED'): rollback('DEPLOY_READINESS_FAILED')
  else: state.update(stage='STAGING_FAILED');record(state)
  raise SystemExit(1)
