import hashlib
import json
import os
from pathlib import Path
import time
import urllib.request
from prepare import HERE, REPO, PRIVATE, cmd, save, inspect, api, compose, ledger

os.umask(0o077)
TARGETS = ('contextra-ai-gateway-1','contextra-ai-control-sidecar-1','contextra-analytics-1')
intent=json.loads((HERE/'build-intent.json').read_text())
for p,h in intent['hashes'].items():
    assert hashlib.sha256((REPO/p).read_bytes()).hexdigest()==h, 'Source changed since build'
before=json.loads((HERE/'before.json').read_text())
images=json.loads((HERE/'images.json').read_text())
original=json.loads((HERE/'compose-before.json').read_text())
candidate=json.loads(cmd(compose()+['config','--format','json']))
expected=json.loads(json.dumps(original))
expected['services']['analytics']['image']=images['analytics']['id']
expected['services']['ai-control-sidecar']['image']=images['sidecar']['id']
assert candidate==expected, 'Unexpected compose change'
env=candidate['services']['analytics']['environment']
assert env['CONTEXTRA_EVENT_ARTIFACT_ENABLED']=='1' and env['CONTEXTRA_ARTIFACT_HTTP_ENABLED']=='1'
assert candidate['services']['ai-gateway']['environment']['AI_ARTIFACT_ACCESS_POLICY']=='lan_public'
states=api('/api/v1/streams')['streams']
assert all(s['state']=='stopped' and not s.get('child_pid') and not s.get('cleanup_pending') for s in states)
assert ledger()==before['ledger'] and not before['ledger']['reservations']
assert not (HERE/'reflect-intent.json').exists(), 'Do not repeat reflection'
save('reflect-intent.json',dict(time=time.time(),targets=TARGETS,channels='all_stopped',images=images))
changed=[]
try:
    for name in TARGETS:
        assert inspect(name)['Id']==before['containers'][name]['Id']
        changed.append(name)
        # Never escalate this maintenance stop to SIGKILL.
        cmd(['docker','stop','--signal=SIGTERM','--timeout=-1',name],timeout=90)
        assert not inspect(name)['State']['Running']
        print('STOPPED',name,flush=True)
    cmd(compose()+['up','-d','--no-deps','--no-build','--pull','never','analytics'],timeout=150)
    deadline=time.monotonic()+120
    while time.monotonic()<deadline:
        try:
            ready=api('/ready')
            break
        except Exception:
            time.sleep(2)
    else:
        raise RuntimeError('ANALYTICS_READINESS_TIMEOUT')
    cmd(compose()+['up','-d','--no-deps','--no-build','--pull','never','ai-control-sidecar'],timeout=90)
    deadline=time.monotonic()+40
    while time.monotonic()<deadline:
        if inspect('contextra-ai-control-sidecar-1')['State'].get('Health',{}).get('Status')=='healthy':
            break
        time.sleep(2)
    else:
        raise RuntimeError('SIDECAR_HEALTH_TIMEOUT')
    cmd(compose()+['up','-d','--no-deps','--no-build','--pull','never','ai-gateway'],timeout=90)
    nginx=cmd(['docker','exec','contextra-ai-gateway-1','nginx','-t','-c','/tmp/nginx.conf'])
    model_script='''import urllib.request,json
print(json.dumps({m:urllib.request.urlopen("http://127.0.0.1:8000/v2/models/"+m+"/ready",timeout=3).status for m in ["RFDETRMedium","RFDETRKeypointPreview","RFDETRFireSmoke"]}))'''
    models=json.loads(cmd(['docker','exec','contextra-analytics-1','python3','-c',model_script]))
    bridge_script='''import urllib.request,json,os
from pathlib import Path
token=Path(os.environ["CONTEXTRA_CONTROL_TOKEN_FILE"]).read_text().strip()
request=urllib.request.Request("http://localhost:8080/internal/bridge-status",headers={"Authorization":"Bearer "+token})
print(json.dumps(json.load(urllib.request.urlopen(request,timeout=3))))'''
    bridge=None
    deadline=time.monotonic()+45
    while time.monotonic()<deadline:
        bridge=json.loads(cmd(['docker','exec','contextra-ai-control-sidecar-1','python3','-c',bridge_script]))
        if bridge['wss'].get('connected') or bridge['wss'].get('active'):
            break
        time.sleep(3)
    states=api('/api/v1/streams')['streams']
    assert all(s['state']=='stopped' and not s.get('child_pid') for s in states)
    assert len(states)==len(before['channels'])
    after_ledger=ledger()
    assert after_ledger==before['ledger'], 'Historical ledger drift'
    preservation={}
    for n,old in before['containers'].items():
        if n in TARGETS:
            continue
        new=inspect(n)
        preservation[n]=all(new[k]==old[k] for k in ('Id','Image')) and new['State']['StartedAt']==old['State']['StartedAt']
    assert all(preservation.values()), 'Unrelated service changed'
    artifacts=api('/api/v1/metadata/status')['artifacts']
    save('reflected.json',dict(ready=ready,models=models,bridge=bridge,images=images,channels_stopped=len(states),
        ledger=after_ledger,non_target_preserved=preservation,artifacts=artifacts))
    print('REFLECTED',json.dumps(dict(models=models,bridge=bridge,channels_stopped=len(states),non_target_preserved=all(preservation.values()))),flush=True)
except Exception as exc:
    save('reflection-failed.json',dict(error=type(exc).__name__,reason=str(exc)[:150],changed=changed))
    # Do not repeat an already stopped service or automatically roll back config.
    for name in reversed(changed):
        try:
            if inspect(name)['State']['Running']:
                cmd(['docker','stop','--time=-1',name],timeout=90)
        except Exception:
            pass
    raise
