#!/usr/bin/env python3
"""Import an attested read-only OMG gen catalog snapshot, never the live Factory.

First run creates cards and hash-addressed blobs. After the GPU pass, rerun with
--measured to pin exact skinned bounds before enrichment. Existing differing v1
cards are refused unless they are the unfinished cards made by this import.
"""
import argparse, hashlib, json, re, shutil, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(ROOT/'lib'), str(ROOT/'tools')]
import card, glb
from import_existing import build_card

ANIMALS = {'wolf':'canine.wolf', 'fox':'canine.fox', 'dog':'canine.dog',
           'deer':'deer.deer', 'stag':'deer.stag', 'bull':'livestock.bull',
           'cow':'livestock.cow', 'horse':'livestock.horse', 'donkey':'livestock.donkey',
           'alpaca':'livestock.alpaca', 'scorpion':'arthropod.scorpion'}
MONSTERS = {'orc':'humanoid.orc', 'yeti':'humanoid.yeti', 'alien':'creature.alien',
            'bird-brute':'creature.bird', 'blue-demon':'creature.demon',
            'cactus-monster':'creature.cactus', 'dinosaur':'creature.dinosaur',
            'fish-raider':'creature.fishman', 'frog-brute':'creature.frog',
            'monkroose':'creature.chimera', 'mushroom-king':'creature.mushroom',
            'rabbit-brute':'creature.rabbit', 'tribal-monster':'creature.tribal'}
PACK = 'https://quaternius.com/packs/ultimateanimatedanimals.html'

def classification(entry):
    name = entry['id'].removeprefix('enemy-').removesuffix('-realistic').removesuffix('-real')
    if name in ANIMALS: return 'animal.'+ANIMALS[name]
    if name in MONSTERS: return 'monster.'+MONSTERS[name]
    raise ValueError('Unmapped creature: '+entry['id'])

def import_one(entry, source, root, measured=None):
    if not entry.get('gen') or entry['role'] not in ('enemy', 'boss'): raise ValueError('Not a generated creature')
    if entry['licence'] not in ('CC0-1.0', 'Forge-owned (generated)'): raise ValueError('Unattested licence')
    h = hashlib.sha256(source.read_bytes()).hexdigest()
    dst = root/'store/blobs'/(h+'.glb'); dst.parent.mkdir(parents=True,exist_ok=True)
    if not dst.exists(): shutil.copyfile(source,dst)
    m = glb.measure(dst); doc = glb.read(dst)[0]
    if not m['rig']: raise ValueError('Creature is not skinned')
    if set(entry['clips'].values()) - set(m['animations']): raise ValueError('Missing clip')
    if set(('idle','walk','run','attack','hit','die')) - set(entry['clips']): raise ValueError('Missing role')
    real = entry['style'] == 'realistic'
    owned = entry['licence'] == 'Forge-owned (generated)'
    urls = re.findall(r'https?://[^\s;]+', entry['credit'])
    if real and not owned and PACK not in urls: urls.append(PACK)
    if owned: urls.append('https://dashboard.le-borgne.fr/spikes/creature-frame-20261015/')
    evidence = 'W-LIBCARDS-evidence/source/catalog.json#'+entry['id']
    reports = ['W-LIBCARDS-evidence/source/REPORT.md']
    if real and not owned: reports += ['W-LIBCARDS-evidence/source/REPORT_REALISTIC.md', 'W-LIBCARDS-evidence/source/PIPELINE-REPORT.md',
                                       'W-REALISTIC-ROUND2-REPORT.md']
    author = entry['credit'] if owned else ('Forge; Quaternius' if real else 'Quaternius (Tomas Laulhe)')
    spec = dict(path=classification(entry), slug='omg-'+entry['id'].removeprefix('enemy-'),
                name=('Forge realistic ' if real else 'Quaternius toon ')+entry['id'].removeprefix('enemy-').removesuffix('-realistic').removesuffix('-real').replace('-',' '),
                summary=entry['about'], style=entry['style'], frame='creature/+Z-front', file=str(source),
                licence=dict(kind='owned' if owned else 'cc0', original=entry['licence'],
                    credit=entry['credit'], author=author, source_urls=urls,
                    game_use=True, raw_redistribute=True, evidence=evidence, reports=reports),
                provenance=dict(author=author, source=urls[0] if urls else evidence,
                    source_urls=urls, lane='forge-realistic-animal' if real else 'quaternius-cc0',
                    kit='omg-creatures', catalog=evidence, reports=reports,
                    method='Forge skeleton-first creature generator; no Quaternius donor' if owned else 'Generated mesh on Quaternius rig' if real else 'Quaternius CC0 import'))
    c = build_card(spec, m, h)
    c['provenance'].update(imported='2026-10-18', file_origin='gex44:/home/claude-agent/forge-monorepo/apps/forge-corp-factory/forge_corp/api/routes/games_assets/chars/'+entry['file'])
    c.update(rigged=True, omg_char_id=entry['id'], game_use=True, reuse={'omg':True,'omu':True})
    c['render']['source']='measured'
    joints = [[doc['nodes'][j].get('name',f'node-{j}') for j in s['joints']] for s in doc['skins']]
    c['rig'].update(skeleton='forge-scorpion' if owned else 'quaternius-animals' if c['id'].startswith('animal.') else 'quaternius-monsters',
                    joint_names=joints, clip_map=entry['clips'], source='measured', clip_map_source='authored')
    c['gameplay'] = {'omg_char_id':entry['id'],'role':entry['role'],
                     'speeds_mps':entry['speeds'], 'speeds_source':'authored' if owned else 'measured',
                     'speeds_evidence':[evidence] if owned else [r for r in reports if not r.endswith('PIPELINE-REPORT.md')],'speed_playback_rate':1,'speed_playback_rate_source':'authored',
                     'catalog_height_m':entry['height_m'],'catalog_height_source':'authored' if owned else 'measured',
                     'mood':entry['mood'],'tint':entry['tint'], 'source':'authored'}
    c['tags'] = sorted(set(c['tags']+entry['names']+[entry['style']]))
    c['thumbnail'] = f"media/{c['id'].replace('/','__')}/thumbnail.jpg"
    if measured:
        b = measured[h]; size = b['size']; lo,hi=b['min'],b['max']
        c['size'].update(measured_m=dict(zip(('w','h','l'),size)), source='measured',
                         method='Three.js Box3 precise skinned vertices on gex44 GPU; bind pose',
                         measurement_evidence='W-LIBCARDS-evidence/measured.json', scale_source='authored')
        c['bounds'] = {'geometry':{'min':lo,'max':hi,'source':'measured'},
                      'footprint':dict(kind='rect',minX=lo[0],maxX=hi[0],minZ=lo[2],maxZ=hi[2],source='measured'),
                      'collider':dict(parts=[dict(type='box',x=(lo[0]+hi[0])/2,y=(lo[1]+hi[1])/2,z=(lo[2]+hi[2])/2,
                         hx=size[0]/2,hy=size[1]/2,hz=size[2]/2)],source='guessed')}
        c['quality']={'gates':'GPU load, exact skinned bounds, thumbnail; source clip evidence in reports', 'judge':None}
    c['release']=card.release_hash(c)
    target=Path(card.path_of(root,c))
    if target.exists():
        old=json.loads(target.read_text())
        if old.get('omg_char_id') != entry['id']: raise ValueError('Existing card belongs to another importer')
        if old['release'] != c['release'] and (old.get('visual') or old['size']['source']=='measured'):
            raise ValueError('Pinned v1 differs; create a new version instead')
        if old.get('visual'): c['visual']=old['visual']
    card.write(root,c)
    return c

def main():
    p=argparse.ArgumentParser(description=__doc__);p.add_argument('--catalog',type=Path,required=True)
    p.add_argument('--source-dir',type=Path,required=True);p.add_argument('--measured',type=Path);p.add_argument('--root',type=Path,default=ROOT)
    a=p.parse_args();entries=json.loads(a.catalog.read_text());measure=json.loads(a.measured.read_text()) if a.measured else None
    if len(entries)!=30 or len({x['id'] for x in entries})!=30:raise ValueError('Expected exactly 30 unique gen creatures')
    cards=[import_one(e,a.source_dir/e['file'],a.root,measure) for e in entries]
    print(json.dumps({'count':len(cards),'by_kind':{k:sum(c['id'].startswith(k+'.') for c in cards) for k in ('animal','monster')},
                      'realistic':sum(c['style']=='realistic' for c in cards),'ids':[c['id'] for c in cards]},indent=1))
if __name__=='__main__':main()
