"""P0 import: every model we already own -> blob by hash + one card (version 1, gates "legacy").

usage: python3 tools/import_existing.py      (idempotent: same file = same release, cards rewritten)
Sources: FGE assets (garage, store kit, village kit, worldgen props), Cast dist files, IKEA catalog, FPS weapon kit.
Spatial numbers are MEASURED from the file (lib/glb.py; skinned meshes get their size from the browser pass);
kit notes (colliders, footprints, roles) are AUTHORED by the kit's maker; seats and similar are GUESSED.
"""
import glob, hashlib, json, os, re, shutil, sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, 'lib'))
import glb, card as cardlib  # noqa: E402

FB = os.path.expanduser('~/forge-build')
FGE = f'{FB}/forge-game-engine'
BANDS = json.load(open(f'{ROOT}/taxonomy/bands.json'))


def sha(path):
    return hashlib.sha256(open(path, 'rb').read()).hexdigest()


def kit_pieces(kit_json):
    try:
        d = json.load(open(kit_json))
    except FileNotFoundError:
        return {}, {}
    ps = d.get('pieces') or d.get('assets') or []
    return {os.path.basename(p['file']): p for p in ps}, d


# ---- classification: (kind.group.class, tags) per source -------------------------------------------------------------
GARAGE = {'kart': 'vehicle.kart.kart', 'race': 'vehicle.car.race', 'race-future': 'vehicle.car.race',
          'tractor-police': 'vehicle.utility.tractor', 'tractor': 'vehicle.utility.tractor', 'tractor-shovel': 'vehicle.utility.loader',
          'delivery': 'vehicle.utility.delivery-truck', 'delivery-flat': 'vehicle.utility.delivery-truck', 'van': 'vehicle.utility.van',
          'truck': 'vehicle.utility.pickup', 'truck-flat': 'vehicle.utility.pickup', 'ambulance': 'vehicle.service.ambulance',
          'firetruck': 'vehicle.service.fire-truck', 'garbage-truck': 'vehicle.service.garbage-truck', 'police': 'vehicle.service.police',
          'taxi': 'vehicle.service.taxi', 'sedan': 'vehicle.car.sedan', 'sedan-sports': 'vehicle.car.sports',
          'hatchback-sports': 'vehicle.car.hatchback', 'suv': 'vehicle.car.suv', 'suv-luxury': 'vehicle.car.suv'}

FURN = [(r'bed|bunk|mattress|crib', 'prop.furniture.bed'), (r'sofa', 'prop.furniture.sofa'), (r'armchair', 'prop.furniture.armchair'),
        (r'office_chair|dining_chair|chair_stack', 'prop.furniture.chair'), (r'table|desk|tv_bench', 'prop.furniture.table'),
        (r'bookcase|shelf|kubb', 'prop.furniture.shelf'), (r'wardrobe', 'prop.furniture.wardrobe'), (r'chest_drawers', 'prop.furniture.drawers'),
        (r'lamp|pendant', 'prop.furniture.lamp'), (r'kitchen', 'prop.furniture.kitchen'), (r'vanity|toilet', 'prop.furniture.bath'),
        (r'mirror', 'prop.furniture.mirror'), (r'^rug$', 'prop.furniture.rug'),
        (r'trolley|shopping_cart|stroller', 'prop.retail.cart'), (r'pallet|flatpack', 'prop.container.pallet'),
        (r'gondola|rack|price_stand|sign|stair_market|candle_bin|wire_bin|rug_rack|bin_', 'prop.retail.stand'),
        (r'plant', 'prop.decor.plant'), (r'cushion', 'prop.decor.cushion'), (r'vase', 'prop.decor.vase'), (r'frame', 'prop.decor.frame'),
        (r'book', 'prop.decor.book'), (r'plush', 'prop.decor.plush'), (r'meatball', 'prop.decor.food'), (r'bag|pot_stack', 'prop.decor.goods')]

STRUCT = [(r'entrance_doors', 'part.opening.door'), (r'partition', 'part.wall.partition'), (r'column', 'part.structure.column'),
          (r'stair|escalator', 'part.stair.stair'), (r'balustrade', 'part.stair.balustrade'), (r'slab', 'part.floor.slab'),
          (r'tray_rail', 'part.structure.rail'), (r'checkout', 'prop.retail.checkout'), (r'food_counter', 'prop.retail.counter'),
          (r'ball_pit', 'prop.decor.goods'), (r'sign', 'prop.retail.sign'), (r'gondola|rack|bin_', 'prop.retail.rack')]

VILLAGE_B = [(r'floor|terrace', 'part.floor.floor'), (r'roof_slope', 'part.roof.slope'), (r'gable_end', 'part.roof.gable-end'),
             (r'chimney', 'part.roof.chimney'),
             (r'stair', 'part.stair.stair'), (r'balcony', 'part.stair.balcony'), (r'corner', 'part.wall.corner'), (r'parapet', 'part.wall.parapet'),
             (r'garden_wall', 'part.wall.garden-wall'), (r'retaining', 'part.wall.retaining-wall'), (r'wall', 'part.wall.module')]

VILLAGE_LM = {'lm_trawler': 'vehicle.sea.trawler', 'lm_watch_fort': 'building.fort.tower', 'lm_lighthouse': 'building.landmark.lighthouse',
              'lm_crane': 'building.landmark.crane', 'lm_chapel': 'building.medieval.chapel', 'lm_fish_hall': 'building.civic.market'}

VILLAGE_P = [(r'lamp_post', 'prop.street.lamp'), (r'bench', 'prop.street.bench'), (r'bollard|mooring', 'prop.harbour.mooring'),
             (r'anchor', 'prop.harbour.anchor'), (r'buoy', 'prop.harbour.buoy'), (r'net_rack', 'prop.harbour.net'),
             (r'rope', 'prop.harbour.rope'), (r'lobster', 'prop.harbour.lobster-pot'), (r'fish_boxes', 'prop.harbour.fish-box'),
             (r'rowboat', 'vehicle.sea.rowboat'), (r'boulder|rock', 'prop.nature.rock'), (r'quay_wall', 'part.wall.quay-wall'),
             (r'dock_deck', 'part.floor.deck'), (r'dock_stairs|stone_steps', 'part.stair.steps'), (r'barrel', 'prop.container.barrel'),
             (r'crate', 'prop.container.crate'), (r'fence', 'prop.farm.fence'), (r'cart', 'prop.farm.cart')]

WORLD = [(r'tree', 'prop.nature.tree'), (r'rock|basalt', 'prop.nature.rock'), (r'column', 'building.ruin.column'), (r'arch', 'building.ruin.arch')]
ENEMY = {'haj': 'monster.creature.shark', 'brute_billig': 'monster.construct.brute', 'imp_flatpack': 'monster.construct.imp',
         'lamp_sentry': 'monster.construct.sentry'}
WEAPON = {'kr7': ('weapon.firearm.rifle', 'KR-7 assault rifle'), 'sm9': ('weapon.firearm.smg', 'SM-9 submachine gun'),
          'ps12': ('weapon.firearm.shotgun', 'PS-12 pump shotgun')}


def rule(table, name):
    for pat, path in table:
        if re.search(pat, name):
            return path
    return None


def humanize(name):
    return re.sub(r'[_-]+', ' ', name).strip()


# ---- sources ----------------------------------------------------------------------------------------------------------
def sources():
    """Yield dicts: file, slug, path, name, summary, style, frame, licence, provenance, kit piece (or None), extra."""
    cat = json.load(open(f'{FGE}/assets/garage/catalog.json'))
    for fam, items in cat['families'].items():
        for e in items:
            base = e['id']; key = 'kart' if base.startswith('kart') else base
            yield dict(file=f"{FGE}/assets/garage/{e['file']}", slug=f'{fam}-{base}', path=GARAGE[key],
                       name=f"{'Kenney' if fam == 'kenney' else 'Forge'} {humanize(base)}", summary=e.get('about', ''),
                       style='toy' if fam == 'kenney' else 'stylized', frame='garage/v2', garage=e,
                       licence=dict(kind='cc0' if fam == 'kenney' else 'owned', game_use=True, raw_redistribute=fam == 'kenney',
                                    evidence='FGE assets/garage/SOURCES.json (Kenney car kit, CC0 1.0)' if fam == 'kenney'
                                    else 'FGE tools/garage/forge/kart_standard.py, GPT-6 Astra worker 30 Sept'),
                       provenance=dict(author='Kenney' if fam == 'kenney' else 'gpt-6-astra', source=e.get('source', ''),
                                       kit='garage'))
    for kit, table, base_path in (('furniture', FURN, None), ('structure', STRUCT, None), ('enemies', None, None)):
        pieces, d = kit_pieces(f'{FGE}/assets/store-kit/{kit}/kit.json')
        for f in sorted(glob.glob(f'{FGE}/assets/store-kit/{kit}/*.glb')):
            n = os.path.basename(f)[:-4]
            path = ENEMY.get(n) if kit == 'enemies' else rule(table, n)
            if not path:
                print('UNMAPPED', kit, n); continue
            p = pieces.get(os.path.basename(f))
            yield dict(file=f, slug=f'store-{n.replace("_", "-")}', path=path, name=f'Flat-pack store {humanize(n)}',
                       summary=(p or {}).get('notes') or f'{humanize(n)} from the MÖBELKRIG flat-pack store kit',
                       style='stylized', frame='kit/+Z-front', piece=p,
                       licence=dict(kind='owned', game_use=True, raw_redistribute=False,
                                    evidence=f'FGE assets/store-kit/{kit}: original procedural Blender art by Forge workers (MÖBELKRIG, 25-27 Sept)'),
                       provenance=dict(author='forge-worker (Blender)', source=f'assets/store-kit/{kit}', kit='store-kit'))
    for kit, table in (('buildings', VILLAGE_B), ('landmarks', None), ('props', VILLAGE_P)):
        pieces, d = kit_pieces(f'{FGE}/assets/village-kit/{kit}/kit.json')
        for f in sorted(glob.glob(f'{FGE}/assets/village-kit/{kit}/*.glb')):
            n = os.path.basename(f)[:-4]
            path = VILLAGE_LM.get(n) if kit == 'landmarks' else rule(table, n)
            if not path:
                print('UNMAPPED', kit, n); continue
            p = pieces.get(os.path.basename(f))
            yield dict(file=f, slug=f'harbour-{n.replace("lm_", "").replace("_", "-")}', path=path,
                       name=f'Harbour village {humanize(n.replace("lm_", ""))}',
                       summary=(p or {}).get('notes') or f'{humanize(n)} from the harbour village kit',
                       style='stylized', frame='kit/+Z-front', piece=p,
                       licence=dict(kind='owned', game_use=True, raw_redistribute=False,
                                    evidence=f'FGE assets/village-kit/{kit}: Blender kit authored by Forge workers (village genre, Sept)'),
                       provenance=dict(author='forge-worker (Blender)', source=f'assets/village-kit/{kit}', kit='village-kit'))
    pieces, d = kit_pieces(f'{FGE}/assets/props/kit.json')
    for f in sorted(glob.glob(f'{FGE}/assets/props/*.glb')):
        n = os.path.basename(f)[:-4]; p = pieces.get(os.path.basename(f))
        yield dict(file=f, slug=f'tideglass-{n.replace("_", "-")}', path=rule(WORLD, n), name=f'Tideglass {humanize(n)}',
                   summary=(p or {}).get('provenance', '') or humanize(n), style='stylized', frame='kit/+Z-front', piece=p,
                   licence=dict(kind='owned', game_use=True, raw_redistribute=False,
                                evidence='FGE assets/props/kit.json: Tideglass biome art by Astra Blender workers, 14-15 Sept'),
                   provenance=dict(author='gpt-6-astra (Blender)', source=(p or {}).get('source', ''), kit='tideglass'))
    cast = f'{FB}/omg-cast-20260929/dist/cast-files'
    for f in sorted(glob.glob(f'{cast}/*/*.glb')):
        grp = f.split('/')[-2]; n = os.path.basename(f)[:-4]; low = n.lower()
        if grp == 'clips':
            path, nm = 'motion.clips.core', 'UAL core animation clips'
        elif low.startswith('hair'):
            path, nm = 'part.character.hair', humanize(n)
        elif low in ('goggles', 'helmet', 'football-helmet'):
            path, nm = 'part.character.gear', humanize(n)
        elif low in ('football-ball', 'racket', 'snowboard'):
            path, nm = 'prop.sport.' + {'football-ball': 'ball', 'racket': 'racket', 'snowboard': 'board'}[low], humanize(n)
        elif grp in ('fantasy', 'modern', 'scifi'):
            path, nm = 'part.character.outfit', f'Cast {grp} outfit {humanize(n)}'  # headless outfit worn on a base body
        elif low in ('ubc-male', 'ubc-female'):
            path, nm = 'character.base.' + low.split('-')[1], f'Cast base {low.split("-")[1]}'
        else:
            path, nm = 'character.people.' + {'crowd': 'crowd', 'modern': 'modern', 'fantasy': 'fantasy', 'scifi': 'scifi',
                                               'football': 'sport', 'tennis': 'sport', 'real': 'sport', 'outfit': 'sport',
                                               'paddle': 'water'}.get(grp, 'crowd'), f'Cast {grp} {humanize(n)}'
        yield dict(file=f, slug=f'cast-{grp}-{low.replace("_", "-")}', path=path, name=nm,
                   summary=f'Cast {grp} set: {humanize(n)} on the shared UAL skeleton', style='stylized', frame='cast/ual',
                   licence=dict(kind='cc0', game_use=True, raw_redistribute=True,
                                evidence='omg-cast-20260929/assets/cast/SOURCES.json (Quaternius UBC/UAL CC0 1.0; MPFB/MakeHuman CC0 output; '
                                         'Astra outfits owned)'),
                   provenance=dict(author='Quaternius + Forge Cast workers', source=f'omg-cast dist/cast-files/{grp}', kit='cast'))
    for f in sorted(glob.glob(f'{FB}/ikea-apartment-20260925/assets/*/*.glb')):
        n = os.path.basename(f)[:-4]
        yield dict(file=f, slug=f'ikea-{n}', path='prop.furniture.' + ({'kivik': 'sofa', 'klippan': 'sofa', 'landskrona': 'sofa',
                   'lisabo': 'chair', 'poang': 'armchair', 'strandmon': 'armchair', 'underverk': 'kitchen', 'rakall': 'kitchen',
                   'matmassig': 'kitchen', 'mattradition': 'kitchen'}.get(n, 'chair')), name=f'IKEA {n.upper()} reconstruction',
                   summary=f'Reconstruction of the IKEA {n.upper()} from official photos and sizes', style='realistic',
                   frame='kit/+Z-front',
                   licence=dict(kind='owned-recreation', game_use=False, raw_redistribute=False,
                                evidence='ikea-apartment-20260925 (Forge Blender rebuild). Real product design + trademark name: '
                                         'not cleared for games'),
                   provenance=dict(author='forge-worker (Blender)', source='ikea-apartment-20260925', kit='ikea'))
    for k, (path, nm) in WEAPON.items():
        f = f'{FB}/fps-engine-20261005/kit/astra/{k}/{k}.glb'
        if os.path.exists(f):
            yield dict(file=f, slug=f'fps-{k}', path=path, name=nm, summary=f'{nm}, realistic, named parts + sight/muzzle empties',
                       style='realistic', frame='weapon/grip-origin-muzzle-minus-z',
                       licence=dict(kind='owned', game_use=True, raw_redistribute=False,
                                    evidence='fps-engine-20261005 kit: Nano Banana Pro sheet + GPT-6 Astra Blender, 5 Oct'),
                       provenance=dict(author='gpt-6-astra (Blender)', source=f'fps-engine-20261005/kit/astra/{k}', kit='fps-kit'))


def band_for(path):
    for k in (path, '.'.join(path.split('.')[:2])):
        if k in BANDS:
            return k, BANDS[k]
    return None, None


def build_card(s, m, h):
    w, ht, ln = m['size_xyz']
    skinned = bool(m['rig'])
    size = {'measured_m': {'w': w, 'l': ln, 'h': ht}, 'source': 'pending-browser' if skinned else 'measured'}
    if s['path'].startswith('vehicle.') and not skinned and w > ln * 1.2:
        size['front_suspect'] = 'wider than long: the front is probably not +Z'  # e.g. the harbour trawler (along X)
    key, band = band_for(s['path'])
    scale = 1.0
    if band:
        axis = 'length' if 'length' in band else 'height'
        val = ln if axis == 'length' else ht
        lo, hi = band[axis]
        # Only toy-unit kits are rescaled. Kits built in metres keep their size and get an honest out-of-band flag.
        if s.get('garage') and s['provenance']['author'] == 'Kenney' and not s['path'].startswith('vehicle.kart'):
            scale = 1.9  # garage fit.mjs CLASS_SCALE: Kenney car-kit units, one factor keeps a truck bigger than a sedan
        elif s['style'] == 'toy' and val and not (lo <= val <= hi):
            scale = round(band['typical'] / val, 3)
        size.update(class_band=key, band_axis=axis, band=[lo, hi])
        if not skinned and val:
            size['in_band'] = lo <= val * scale <= hi
    size['scale_to_class'] = scale
    p = s.get('piece') or {}
    bounds = {'geometry': m['bounds']}
    if p.get('footprint'):
        bounds['footprint'] = dict(kind='rect', **p['footprint'], source='authored')
    else:
        b = m['bounds']
        bounds['footprint'] = dict(kind='rect', minX=b['min'][0], maxX=b['max'][0], minZ=b['min'][2], maxZ=b['max'][2], source='measured')
    if p.get('collision'):
        bounds['collider'] = dict(parts=p['collision'], source='authored')
    else:
        b = m['bounds']
        bounds['collider'] = dict(parts=[{'type': 'box', 'x': round((b['min'][0] + b['max'][0]) / 2, 3),
                                          'y': round((b['min'][1] + b['max'][1]) / 2, 3), 'z': round((b['min'][2] + b['max'][2]) / 2, 3),
                                          'hx': round(w / 2, 3), 'hy': round(ht / 2, 3), 'hz': round(ln / 2, 3)}], source='guessed')
    sockets, nodes, gameplay = [], {}, {}
    g = s.get('garage')
    if g:
        wn = g['nodes'].get('wheels', [])
        nodes = {'body': g['nodes'].get('body', []), 'wheels': wn}
        for i, nm in enumerate(wn):
            pos = m['node_pos'].get(nm)
            r = g['size'].get('frontR' if i < 2 else 'rearR')
            sockets.append(dict(name=['wheel_fl', 'wheel_fr', 'wheel_rl', 'wheel_rr'][i] if len(wn) == 4 else nm, role='wheel',
                                parent=nm, pos=pos, rot=[0, 0, 0, 1], spin_axis='X', radius=r, steer=i < 2, source='measured'))
        if g.get('seat'):
            st = g['seat']
            sockets.append(dict(name='seat_driver', role='seat', parent=None, pos=[st['x'], st['y'], st['z']], rot=[0, 0, 0, 1],
                                source='guessed' if 'estimated' in g['nodes'].get('seatFrom', '') else 'authored'))
        gameplay = {'wheelbase': g['size'].get('wheelbase'), 'track': g['size'].get('track'), 'source': 'measured'}
    else:
        for nm, pos in m['node_pos'].items():
            low = nm.lower()
            if low.startswith(('muzzle', 'sight', 'grip', 'ejection', 'socket', 'seat', 'hand_', 'exhaust')):
                sockets.append(dict(name=nm, role=low.split('_')[0].rstrip('0123456789'), parent=None, pos=pos, rot=None,
                                    source='authored'))
    tags = sorted(set(re.split(r'[^a-z0-9]+', (s['slug'] + ' ' + s['path'].replace('.', ' ') + ' ' + s['name']).lower())) - {''})
    status = s.get('status') or ('approved' if s['licence']['game_use'] else 'candidate')
    card = {
        'id': f"{s['path']}/{s['slug']}", 'version': s.get('version', 1), 'name': s['name'], 'summary': s['summary'][:300],
        'tags': tags, 'style': s['style'], 'status': status,
        'frame': {'contract': s['frame'], 'up': '+Y', 'unit': 'm',
                  'front': '-Z (muzzle)' if s['frame'].startswith('weapon') else '+Z'},
        'size': size, 'bounds': bounds, 'placement': {'role': p.get('role'), 'notes': p.get('notes'), 'source': 'authored' if p else None},
        'sockets': sockets, 'nodes': nodes or {'all': m['nodes'][:60]},
        'paint': {'encoding': 'COLOR_0 region ids (garage)'} if g else None,
        'render': {'tris': m['tris'], 'verts': m['verts'], 'materials': m['materials'], 'draw_calls': m['draw_calls'],
                   'textures': m['textures'], 'gpu_mb_decoded': m['gpu_mb_decoded'], 'bytes': m['bytes'], 'codec': m['codec'],
                   'extensions': m['extensions']},
        'rig': {'skeleton': 'ual' if s['frame'] == 'cast/ual' else 'own', **m['rig']} if m['rig'] else None,
        'animations': m['animations'], 'gameplay': gameplay or None,
        'techniques': sorted({m['codec']} - {'none'} | ({'skinned'} if m['rig'] else set()) | ({'textured'} if m['textures'] else {'flat-colour'})),
        'provenance': {**s['provenance'], 'imported': '2026-10-07', 'file_origin': s['file'].replace(os.path.expanduser('~'), '~')},
        'licence': s['licence'],
        'quality': {'gates': s.get('gates', 'legacy'), 'judge': None},
        'files': {'glb': f'sha256:{h}'}, 'deps': [],
    }
    card['release'] = cardlib.release_hash(card)
    return card


def main():
    os.makedirs(f'{ROOT}/store/blobs', exist_ok=True)
    seen, cards, dupes = {}, [], []
    for s in sources():
        if not s.get('path'):
            print('NO PATH', s['file']); continue
        h = sha(s['file'])
        if h in seen:
            dupes.append((s['slug'], seen[h])); continue
        dst = f'{ROOT}/store/blobs/{h}.glb'
        if not os.path.exists(dst):
            shutil.copyfile(s['file'], dst)
        m = glb.measure(s['file'])
        c = build_card(s, m, h)
        seen[h] = c['id']
        cardlib.write(ROOT, c)
        cards.append(c)
    print(f'{len(cards)} cards, {len(dupes)} duplicate files skipped')
    for a, b in dupes[:10]:
        print('  dup', a, '=', b)


if __name__ == '__main__':
    main()
