#!/usr/bin/env python3
"""Runs ON gex44 with the forge-repaint runtime (/opt/forge-repaint, its venv). One spacecraft through the repaint engine
(16 Oct 2026 spaceship batch; same steps as pilot/batch.py, but the mesh split scales with the craft: a 5 cm edge, right for
furniture, would explode a 900 m battleship):
  prepare (one primitive, unique UVs, baked colour) -> split (edge = longest side / 300) -> repaint with the guide written by
  tools/space/paint_ship.sh (GPT Image 2 through the local spool worker, or Nano Banana 2) -> transfer back onto the light copy.
usage: paint_ship_remote.py --dir RUN --key SLUG [--editor gpt|nb2] [--max-pictures 28] [--segments 3] [--views level+up]
RUN/in/SLUG.glb + RUN/in/SLUG.guide.json in; RUN/out/SLUG/ (painted.glb, sheet.jpg, report.json, row.json) out."""
import argparse, json, os, subprocess, sys, time
from pathlib import Path

R = Path('/opt/forge-repaint'); PY = str(R / 'venv/bin/python')


def main():
    ap = argparse.ArgumentParser(); ap.add_argument('--dir', type=Path, required=True); ap.add_argument('--key', required=True)
    ap.add_argument('--editor', choices=['gpt', 'nb2'], default='gpt'); ap.add_argument('--max-pictures', type=int, default=28)
    ap.add_argument('--segments', type=int, default=3)   # close-ups along long hulls (repaint.py --segments; skipped when not elongated)
    ap.add_argument('--views', default='level+up'); ap.add_argument('--tex-px', type=int, default=2048)
    a = ap.parse_args()
    d, k = a.dir, a.key
    src, prep, dense, out = d / 'in' / f'{k}.glb', d / 'prep' / f'{k}.glb', d / 'dense' / f'{k}.glb', d / 'out' / k
    for f in (prep.parent, dense.parent, out): f.mkdir(parents=True, exist_ok=True)
    row = {'key': k, 'editor': a.editor}; t0 = time.time()
    def step(name, cmd, **kw):
        r = subprocess.run(cmd, capture_output=True, text=True, **kw)
        if r.returncode:
            row['error'] = f'{name}: ' + (r.stderr or r.stdout)[-600:]
            (out / 'row.json').write_text(json.dumps(row, indent=1)); print(json.dumps(row)); sys.exit(1)
        return r
    if not prep.exists():
        step('prepare', [PY, str(R / 'prepare/prepare.py'), str(src), str(prep), '--tex-px', str(a.tex_px), '--report', str(out / 'prepare.json')])
    rep = json.loads((out / 'prepare.json').read_text()) if (out / 'prepare.json').exists() else {}
    if rep and rep.get('applied') is False:
        row['error'] = 'prepare refused: ' + str(rep.get('reason') or rep)[:400]
        (out / 'row.json').write_text(json.dumps(row, indent=1)); print(json.dumps(row)); sys.exit(1)
    # longest side from the prepared file's POSITION bounds
    sys.path.insert(0, str(R / 'prepare'))
    doc = json.loads(_json_chunk(prep))
    acc = [doc['accessors'][p['attributes']['POSITION']] for m in doc['meshes'] for p in m['primitives']]
    span = max(max(x['max'][i] for x in acc) - min(x['min'][i] for x in acc) for i in range(3))
    edge = round(span / 300, 4); row.update(span=round(span, 2), edge=edge)
    segments = a.segments if span >= 60 else 0   # small craft fill a full view well; close-ups cost ~12 pictures
    row['segments'] = segments
    # the painter refuses more than 400k faces: start at longest side / 300 and coarsen until the split copy fits
    # (16 Oct 2026: the 135k-triangle mining ship split at 22 cm gave >400k faces). Dense Blender meshes need little splitting.
    for attempt in range(6):
        if dense.exists() and faces(dense) <= 380000: break
        if dense.exists(): dense.unlink()
        if attempt == 5 or faces(prep) > 300000:
            dense.write_bytes(prep.read_bytes()); row['edge'] = None; break
        step('split', [PY, str(R / 'prepare/subdivide.py'), 'split', str(prep), str(dense), '--edge', str(edge)])
        if faces(dense) <= 380000: row['edge'] = edge; break
        edge = round(edge * 2, 4)
    row['faces'] = faces(dense)
    env = dict(os.environ, REPAINT_TAG='SPACE-' + k)
    cmd = [PY, str(R / 'core/repaint.py'), 'run', str(dense), '--out', str(out), '--guide', str(d / 'in' / f'{k}.guide.json'),
           '--views', a.views, '--tex-px', str(a.tex_px), '--no-spots', '--max-pictures', str(a.max_pictures), '--timeout', '2700',
           '--coverage-views', '2', '--segments', str(segments)]
    if a.editor == 'gpt':
        env['REPAINT_HELPER'] = str(R / 'bin/spool-edit'); env['SPOOL_WAIT'] = '900'; cmd += ['--image-model', 'gpt-image-2']
        env['REPAINT_SPOOL'] = str(R / 'spool-space')   # this batch's own spool, answered by its own local worker (6 slots)
    run = subprocess.run(cmd, env=env, capture_output=True, text=True)
    report = json.loads((out / 'report.json').read_text()) if (out / 'report.json').exists() else {}
    views = [v for v in report.get('views', []) if v.get('view') != 'source']
    row.update(applied=report.get('applied'), reason=report.get('reason'), kept=sum(1 for v in views if v.get('kept')), tried=len(views),
               outside=report.get('outside_painted'), pictures=report.get('pictures_used'))
    if run.returncode and not report:
        row['error'] = 'repaint: ' + (run.stderr or run.stdout)[-600:]
    if report.get('applied'):
        step('transfer', [PY, str(R / 'prepare/subdivide.py'), 'transfer', str(out / 'out.glb'), str(prep), str(out / 'painted.glb')])
    row['seconds'] = round(time.time() - t0)
    (out / 'row.json').write_text(json.dumps(row, indent=1)); print(json.dumps(row))


def faces(p):
    doc = json.loads(_json_chunk(p))
    return sum(doc['accessors'][pr['indices']]['count'] // 3 if 'indices' in pr else doc['accessors'][pr['attributes']['POSITION']]['count'] // 3
               for m in doc['meshes'] for pr in m['primitives'])


def _json_chunk(p):
    b = Path(p).read_bytes()
    n = int.from_bytes(b[12:16], 'little')
    return b[20:20 + n].decode()


if __name__ == '__main__':
    main()
