#!/usr/bin/env python3
"""mesh_pipeline.py — flux image -> bg_remove -> image->GLB, per spec entry.

Usage: mesh_pipeline.py tools/mesh-wave-1.json assets/meshes/
Retries each farm job up to 3x (the m4 node's broken mflux venv 126s any flux
job routed to it — resubmission re-rolls the load-balancer). Skips any entry
whose .glb already exists, so reruns are cheap.
"""
import json, os, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mb

RETRIES = 3


def run_job(operator, params=None, asset_id=None, label=''):
    for attempt in range(1, RETRIES + 1):
        jid = None
        while jid is None:
            jid = mb.submit(operator, params, asset_id=asset_id)
            if jid is None:
                print(f'[pipe] {label}: throttled, 15s', flush=True)
                time.sleep(15)
        st = mb.wait(jid, f'{label} ({operator} try {attempt})', interval=10)
        if st == 'done':
            return jid
        time.sleep(5)
    raise RuntimeError(f'{label}: {operator} failed {RETRIES}x')


PRIMARY = 'm3ultra@100.89.131.57'


def scp_from_outdir(jid, out, suffix='.glb'):
    """Fallback for a farm bug (2026-07-17): hunyuan jobs run on remote workers
    ship outputs back to the primary's job outdir but never register them as
    assets. Pull the textured mesh (not *_shape.glb) straight from outdir."""
    import subprocess
    j = mb.req(f'/api/jobs/{jid}')
    outdir = j.get('outdir')
    if not outdir:
        return False
    ls = subprocess.run(['ssh', '-o', 'ConnectTimeout=8', '-o', 'BatchMode=yes',
                         PRIMARY, f'ls {outdir}'], capture_output=True, text=True)
    names = [n for n in ls.stdout.split()
             if n.endswith(suffix) and not n.endswith('_shape.glb')]
    if not names:
        return False
    r = subprocess.run(['scp', '-o', 'ConnectTimeout=8', '-o', 'BatchMode=yes',
                        f'{PRIMARY}:{outdir}/{names[0]}', out], capture_output=True)
    return r.returncode == 0 and os.path.exists(out)


def fetch_output(jid, out, suffix=None):
    outs = mb.outputs_for(jid, suffix)
    if outs:
        n = mb.fetch(outs[0]['id'], out)
        print(f'[pipe] wrote {out} ({n//1024}KB)', flush=True)
        return
    if suffix and scp_from_outdir(jid, out, suffix):
        print(f'[pipe] wrote {out} ({os.path.getsize(out)//1024}KB, outdir fallback)', flush=True)
        return
    raise RuntimeError(f'no output asset for {jid} (api + outdir fallback both empty)')


def main(spec_path, outdir):
    specs = json.load(open(spec_path))
    srcdir = os.path.join(outdir, 'src')
    os.makedirs(srcdir, exist_ok=True)
    failed = []
    for s in specs:
        name = s['name']
        glb = os.path.join(outdir, f'{name}.glb')
        if os.path.exists(glb):
            print(f'[pipe] {name}: glb exists, skip', flush=True)
            continue
        try:
            img = os.path.join(srcdir, f'{name}.png')
            if not os.path.exists(img):
                jid = run_job('flux_local', {'prompt': s['prompt']}, label=name)
                fetch_output(jid, img)
            cut = os.path.join(srcdir, f'{name}-cut.png')
            if not os.path.exists(cut):
                aid = mb.upload(img)
                jid = run_job('bg_remove_local', {}, asset_id=aid, label=name)
                fetch_output(jid, cut)
            aid = mb.upload(cut)
            jid = run_job('hunyuan3d_mlx', {}, asset_id=aid, label=name)
            fetch_output(jid, glb, suffix='.glb')
        except Exception as e:
            print(f'[pipe] {name}: FAILED — {e}', flush=True)
            failed.append(name)
    ok = len(specs) - len(failed)
    print(f'[pipe] mesh wave complete: {ok}/{len(specs)} ok'
          + (f' — failed: {failed}' if failed else ''), flush=True)


if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2])
