import hashlib
import json
import os
from pathlib import Path
import shutil
import signal
import subprocess
import tarfile
import time
import urllib.request

HERE = Path(__file__).parent
REPO = Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
PRIVATE = Path('/workspace/contextra-runtime-config')
os.umask(0o077)

def cmd(args, timeout=30, **kwargs):
    return subprocess.run(args, check=True, capture_output=True, text=True, timeout=timeout, **kwargs).stdout

def save(name, data):
    with (HERE/name).open('x') as target:
        json.dump(data, target, indent=2)

def inspect(name):
    return json.loads(cmd(['docker','inspect',name]))[0]

def api(path):
    with urllib.request.urlopen('http://127.0.0.1:35000'+path, timeout=5) as r:
        return json.load(r)

def compose():
    return ['docker','compose','-p','contextra','--project-directory',str(REPO),
        '--env-file',str(PRIVATE/'runtime.env'),'--env-file',str(PRIVATE/'secure-bridge.env'),
        '--env-file',str(PRIVATE/'web-start-candidate-20260918/candidate.env'),
        '-f',str(REPO/'docker-compose.yml'),'-f',str(PRIVATE/'metadata-active/compose.yaml'),
        '-f',str(REPO/'deploy/secure-bridge.compose.yml'),'-f',str(REPO/'deploy/web-start.compose.yml')]

def ledger():
    sql="SELECT json_build_object('count',count(*),'attempts',sum(attempts),'reservations',sum(reservation),'fingerprint',md5(string_agg(md5(t::text),'' ORDER BY event_id))) FROM contextra_event_artifacts t;"
    return json.loads(cmd(['docker','exec','-i','contextra-postgresql-1','sh','-c',
        'exec psql -X -qAt -U "$POSTGRES_USER" -d "$POSTGRES_DB"'], input=sql))

if __name__ == '__main__':
    head = cmd(['git','-C',str(REPO),'rev-parse','HEAD']).strip()
    assert head == 'f2a12013b7521d7f7960b58dd0d04a6974ffd214'
    assert shutil.disk_usage('/').free > 15*1024**3
    assert not (HERE/'build-intent.json').exists(), 'Resume from existing intent, do not duplicate build'
    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)
    names = cmd(['docker','ps','-a','--format','{{.Names}}']).splitlines()
    before = {n:{k:inspect(n)[k] for k in ('Id','Image','State')} for n in names}
    save('before.json', dict(containers=before, channels=[{k:s.get(k) for k in ('stream_id','state','run_id','run_generation','child_pid','cleanup_pending')} for s in states],ledger=ledger()))
    config = json.loads(cmd(compose()+['config','--format','json']))
    save('compose-before.json', config)
    paths = cmd(['git','-C',str(REPO),'diff','--name-only']).splitlines()+cmd(['git','-C',str(REPO),'ls-files','--others','--exclude-standard']).splitlines()
    hashes = {p:hashlib.sha256((REPO/p).read_bytes()).hexdigest() for p in paths if (REPO/p).is_file()}
    digest = hashlib.sha256(json.dumps(hashes,sort_keys=True).encode()).hexdigest()[:10]
    snapshot=HERE/'snapshot'
    snapshot.mkdir()
    p=subprocess.Popen(['git','-C',str(REPO),'archive',head],stdout=subprocess.PIPE)
    with tarfile.open(fileobj=p.stdout,mode='r|') as archive:
        archive.extractall(snapshot,filter='data')
    assert p.wait()==0
    for path in paths:
        dst=snapshot/path
        if (REPO/path).is_file():
            dst.parent.mkdir(parents=True,exist_ok=True)
            shutil.copy2(REPO/path,dst)
        elif dst.exists():
            dst.unlink()
    base = config['services']['ai-control-sidecar']['build']['args']['SIDECAR_BASE_IMAGE']
    cmd(['docker','image','inspect',base])
    save('build-intent.json',dict(head=head,hashes=hashes,digest=digest,sidecar_base=base))
    images={}
    for name, dockerfile, context, extra in (
        ('analytics','docker/Dockerfile','.', ['--target','app']),
        ('sidecar','control_sidecar/Dockerfile','control_sidecar',['--build-arg','SIDECAR_BASE_IMAGE='+base])):
        tag=f'contextra-{name}:fall-f2a1201-{digest}'
        with (HERE/(name+'-build.log')).open('x') as log:
            proc=subprocess.Popen(['docker','build','--pull=false','--network=default','-f',dockerfile,'-t',tag,*extra,context],
                cwd=snapshot,stdout=log,stderr=log,start_new_session=True)
            started=time.monotonic()
            lowest=shutil.disk_usage('/').free
            while proc.poll() is None:
                lowest=min(lowest,shutil.disk_usage('/').free)
                if time.monotonic()-started>900 or lowest<15*1024**3:
                    os.killpg(proc.pid,signal.SIGTERM)
                    proc.wait(20)
                    raise RuntimeError('BUILD_RESOURCE_BOUND')
                time.sleep(2)
        assert proc.returncode==0, name+' build failed; inspect owned build log'
        image=json.loads(cmd(['docker','image','inspect',tag]))[0]['Id']
        images[name]=dict(tag=tag,id=image,lowest_root_bytes=lowest)
        save(name+'-image.json',images[name])
        print('BUILT',name,tag,image,flush=True)
    save('images.json',images)
