"""Take in the SOUS LES PAVES (Paris riot game) 3D assets as library cards.

usage: python3 tools/import_paves.py [--src <paves-root>/genres/riot/assets]

For each asset: write a job dir jobs/<slug>-v1/ (job.json + NOTES.md, the GLB beside it is git-ignored), run
tools/import_job.py on it (copies the GLB into store/blobs by hash, writes cards/.../v1.json), then patch the card with
what import_job cannot know: honest licence evidence (ours, no textures), the Cast attach socket from the game's
cast/manifest.json (bone + offset + rotation), placement notes, tags, and the right frame contract. The release hash is
recomputed after the patch. Re-runnable: it rewrites the same v1 cards.
"""
import json, math, os, shutil, subprocess, sys

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

SRC = '/home/claude-agent/forge-build/paves-root/genres/riot/assets'
GAME = 'SOUS LES PAVES (Paris riot game, genres/riot)'
GEAR_CONTRACT = 'cast/gear-grip-origin-+Z'   # NOT the FPS weapon contract (muzzle -Z): these are third-person Cast attachments

# The 3 launchers are rotated 180 deg about Y in the LIBRARY copy (muzzle -Z = the FPS weapon contract); the game's
# source GLBs are not touched. The attach rotation is composed with the same turn so the pose in the hand is unchanged.
ROT180 = {'riot-lbd-launcher', 'riot-gas-launcher', 'riot-firework-mortar'}
ROTATE_JS = os.path.join(ROOT, 'tools', 'rotate_y180.cjs')   # gltf-transform (~/tools-gltf) script: bakes x,z -> -x,-z into the vertices

# slug, class path, title, file (relative to SRC), manifest key (gear attach) or None, summary, long axis / use note, extra tags
A = [
    # ---- hand-held melee + throwables (grip at origin, +Z along the item, +Y up) ----
    ('riot-tonfa', 'weapon.melee.baton', 'Police tonfa baton', 'cast/gear/tonfa.glb', 'tonfa',
     'Police baton (tonfa), 0.58 m long, held in the right hand.',
     'Grip at origin, baton runs along +Z (z -0.11 to 0.47 m).', ['police', 'baton', 'tonfa', 'club', 'melee']),
    ('riot-crowbar', 'weapon.melee.crowbar', 'Steel crowbar', 'cast/gear/crowbar.glb', 'crowbar',
     'Crowbar, 0.71 m long, held in the right hand.',
     'Grip at origin, bar runs along +Z (z -0.14 to 0.57 m).', ['crowbar', 'pry bar', 'tool', 'melee']),
    ('riot-shield', 'weapon.melee.shield', 'Transparent riot shield', 'cast/gear/shield.glb', 'shield',
     'Transparent riot shield with a POLICE band, 0.57 m wide and 1.04 m tall, carried on the left arm.',
     'Origin at the carry point; flat panel in the XY plane (0.04 m thick along Z), y -0.52 to 0.52 m.',
     ['police', 'riot', 'shield', 'transparent', 'defence']),
    ('riot-paving-stone', 'weapon.thrown.paving-stone', 'Paris paving stone', 'cast/gear/pave.glb', 'pave',
     'Paris paving stone (pave), about 15 cm across, thrown or carried in the right hand.',
     'Origin near the palm; stone about 0.15 x 0.14 x 0.17 m.', ['pave', 'cobblestone', 'paving', 'stone', 'throw', 'paris']),
    ('riot-molotov', 'weapon.thrown.molotov', 'Molotov cocktail', 'cast/gear/molotov.glb', 'molotov',
     'Molotov cocktail, 0.36 m long, thrown from the right hand.',
     'Grip at origin, item runs along +Z (z -0.04 to 0.32 m).', ['molotov', 'bottle', 'fire', 'throw', 'incendiary']),
    ('riot-sting-grenade', 'weapon.thrown.grenade', 'Sting grenade', 'cast/gear/sting.glb', 'sting',
     'Police sting grenade, about 12 cm tall, thrown from the right hand.',
     'Origin near the palm; grenade about 0.08 x 0.13 x 0.09 m.', ['police', 'grenade', 'sting', 'less-lethal', 'throw']),
    # ---- launchers (grip at origin, muzzle +Z) ----
    ('riot-lbd-launcher', 'weapon.firearm.launcher', '40 mm LBD less-lethal launcher', 'cast/gear/lbd.glb', 'lbd',
     '40 mm LBD less-lethal launcher, 0.72 m long, held in the right hand.',
     'Grip at origin, muzzle toward -Z (z down to -0.50 m); library copy turned 180 deg about Y from the game file.', ['police', 'lbd', '40mm', 'less-lethal', 'launcher']),
    ('riot-gas-launcher', 'weapon.firearm.launcher', 'Tear-gas launcher', 'cast/gear/gas.glb', 'gas',
     'Tear-gas launcher, 0.74 m long, held in the right hand.',
     'Grip at origin, muzzle toward -Z (z down to -0.51 m); library copy turned 180 deg about Y from the game file.', ['police', 'tear gas', 'gas', 'launcher', 'less-lethal']),
    ('riot-firework-mortar', 'weapon.firearm.launcher', 'Firework mortar tube', 'cast/gear/mortar.glb', 'mortar',
     'Firework mortar tube, 0.69 m long, held in the right hand.',
     'Grip at origin, tube mouth toward -Z (z down to -0.58 m); library copy turned 180 deg about Y from the game file.', ['firework', 'mortar', 'tube', 'launcher', 'improvised']),
    # ---- character gear worn on the UAL skeleton ----
    ('riot-crs-helmet', 'part.character.gear', 'CRS riot helmet with visor', 'cast/gear/helmet.glb', 'helmet',
     'CRS riot helmet with visor, worn on the Head bone of the UAL skeleton.',
     'Origin at the Head bone attach point; 0.27 x 0.28 x 0.30 m.', ['police', 'crs', 'helmet', 'visor', 'riot']),
    ('riot-gasmask', 'part.character.gear', 'Gas mask', 'cast/gear/gasmask.glb', 'gasmask',
     'Gas mask, 0.18 m wide, hung at the belt (pelvis bone) in the game.',
     'Origin at the mask centre; the game hangs it at the hip (pelvis offset 0.17, 0.02, -0.02), a face fit needs a new offset.',
     ['gas mask', 'respirator', 'mask', 'protest']),
    ('riot-balaclava', 'part.character.gear', 'Balaclava', 'cast/gear/balaclava.glb', 'balaclava',
     'Balaclava worn on the Head bone of the UAL skeleton.',
     'Origin at the Head bone attach point; 0.23 x 0.29 x 0.26 m.', ['balaclava', 'mask', 'black bloc', 'protest']),
    ('riot-backpack', 'part.character.gear', 'Protester backpack', 'cast/gear/backpack.glb', 'backpack',
     'Backpack, 0.28 m wide, worn on the back at the spine_03 bone.',
     'Origin at the spine_03 attach point; the pack sits behind the torso (z -0.29 to 0.05 m).', ['backpack', 'bag', 'protest']),
    # ---- protest props carried / worn ----
    ('riot-protest-sign', 'prop.protest.sign', 'Cardboard protest sign', 'cast/gear/sign.glb', 'sign',
     'Cardboard protest sign board, 0.68 m wide and 1.4 m tall, carried in the left hand.',
     'Origin at the hand attach point; the board spans y -0.28 to 1.11 m, 0.04 m thick along Z.',
     ['protest', 'sign', 'cardboard', 'placard']),
    ('riot-banner-pole', 'prop.protest.banner', 'Protest banner pole', 'cast/gear/banner-pole.glb', 'banner_pole',
     'Protest banner pole, 2.3 m tall, carried upright in the left hand.',
     'Origin at the hand attach point; the pole runs along +Y (y -0.68 to 1.61 m).', ['protest', 'banner', 'pole']),
    ('riot-protest-flag', 'prop.protest.flag', 'Protest flag on a pole', 'cast/gear/flag.glb', 'flag',
     'Protest flag on a 2.2 m pole, carried in the left hand.',
     'Origin at the hand attach point; the pole runs along +Y (y -0.65 to 1.55 m), the cloth extends toward -Z (to z -0.88 m).',
     ['protest', 'flag', 'pole']),
    ('riot-megaphone', 'prop.protest.megaphone', 'Megaphone', 'cast/gear/mega.glb', 'mega',
     'Megaphone, 0.39 m long, held in the right hand.',
     'Grip at origin; the megaphone spans z -0.03 to 0.36 m.', ['megaphone', 'bullhorn', 'protest', 'loudspeaker']),
    ('riot-protest-drum', 'prop.protest.drum', 'Protest drum', 'cast/gear/drum.glb', 'drum',
     'Drum, 0.46 m across, worn at the belly (spine_02 bone).',
     'Origin at the drum centre (0.46 x 0.31 x 0.46 m); the game wears it at the spine_02 bone.', ['drum', 'percussion', 'protest']),
    # ---- outfits skinned to the UAL rig ----
    ('riot-crs-armor', 'part.character.outfit', 'CRS riot police body armour', 'cast/outfits/crs-armor.glb', None,
     'CRS riot police body armour skinned to the Quaternius UAL rig.',
     'Skinned to the UAL bone names (65 joints), T-pose; the game layers it over a Cast base outfit.',
     ['police', 'crs', 'armour', 'riot', 'cast', 'ual']),
    ('riot-black-bloc', 'part.character.outfit', 'Black-bloc hoodie outfit', 'cast/outfits/black-bloc.glb', None,
     'Black-bloc hoodie outfit skinned to the Quaternius UAL rig.',
     'Skinned to the UAL bone names (65 joints), T-pose, upper body (y 1.11 to 1.86 m); the game layers it over a Cast base outfit.',
     ['black bloc', 'hoodie', 'protest', 'cast', 'ual']),
    # ---- architecture ----
    ('riot-haussmann-facade-kit', 'part.wall.module', 'Haussmann facade module kit', 'map/haussmann-kit.glb', None,
     'Paris Haussmann facade module kit: window bay with balcony, shop bay, corner rotunda, mansard dormer and chimney stack.',
     'Five separate top-level module nodes (WindowBayBalcony, ShopBay, CornerRotunda, MansardDormer, ChimneyStack), metres, Y up; '
     'the game instances them along a street. Materials by name (HaussmannStone, ParisZinc, ParisGlass, WroughtIron, ChimneyBrick, TerracottaPot).',
     ['haussmann', 'paris', 'facade', 'balcony', 'mansard', 'chimney', 'shopfront', 'kit']),
    ('riot-monument-republique', 'building.landmark.statue', 'Monument a la Republique, Paris', 'map/monument.glb', None,
     'Monument a la Republique, Paris: stylised landmark statue (Marianne, three seated allegories, bronze reliefs, lion and ballot urn) on its drum, 26k tris.',
     'Origin on the ground at the monument centre, 19 m x 19 m footprint, 25.6 m tall.',
     ['monument', 'republique', 'paris', 'statue', 'landmark', 'square']),
]


def euler_to_quat(r):
    """three.js Euler order XYZ -> quaternion [x, y, z, w]."""
    c = [math.cos(a / 2) for a in r]; s = [math.sin(a / 2) for a in r]
    x = s[0] * c[1] * c[2] + c[0] * s[1] * s[2]
    y = c[0] * s[1] * c[2] - s[0] * c[1] * s[2]
    z = c[0] * c[1] * s[2] + s[0] * s[1] * c[2]
    w = c[0] * c[1] * c[2] - s[0] * s[1] * s[2]
    return [round(v, 6) for v in (x, y, z, w)]


def quat_mul(a, b):
    ax, ay, az, aw = a; bx, by, bz, bw = b
    return [round(v, 6) for v in (aw * bx + ax * bw + ay * bz - az * by, aw * by - ax * bz + ay * bw + az * bx,
                                   aw * bz + ax * by - ay * bx + az * bw, aw * bw - ax * bx - ay * by - az * bz)]


def main(src=SRC):
    manifest = json.load(open(f'{src}/cast/manifest.json'))['gear']
    jobs = os.path.join(ROOT, 'jobs')
    made = []
    for slug, cls, title, rel, gkey, summary, note, tags in A:
        jd = os.path.join(jobs, f'{slug}-v1')
        os.makedirs(jd, exist_ok=True)
        if slug in ROT180:
            subprocess.run(['node', ROTATE_JS, f'{src}/{rel}', f'{jd}/{slug}.glb'], check=True,
                           env=dict(os.environ, NODE_PATH=os.path.expanduser('~/tools-gltf/node_modules')))
        else:
            shutil.copyfile(f'{src}/{rel}', f'{jd}/{slug}.glb')
        json.dump({'id': f'{cls}/{slug}', 'version': 1, 'name': summary, 'title': title, 'slug': slug, 'pen': 'gemini4',
                   'family': 'sous-les-paves', 'tags': tags, 'status': 'approved',
                   'source': f'{GAME}: assets/{rel}', 'contract': GEAR_CONTRACT if (gkey and slug not in ROT180) else 'kit/+Z-front'},
                  open(f'{jd}/job.json', 'w'), indent=1, ensure_ascii=False)
        open(f'{jd}/NOTES.md', 'w').write(
            f'{summary}\n\nGame: {GAME}. Drawn by Gemini 4 Argon in Blender (Antigravity CLI) on 18 Oct 2026; licence: ours.\n'
            f'Frame: {note}\nNo image textures (flat materials); no third-party artwork.\n')
        import_job.main(jd)
        made.append((slug, cls, gkey, rel))

    for slug, cls, gkey, rel in made:
        a = next(x for x in A if x[0] == slug)
        p = os.path.join(ROOT, 'cards', *cls.split('.'), slug, 'v1.json')
        c = json.load(open(p))
        m = glb.measure(f'{jobs}/{slug}-v1/{slug}.glb')   # the library copy (rotated for the launchers)
        # --- licence: ours, honest evidence (import_job's default text talks about a Nano Banana sheet + Poly Haven)
        c['licence'] = dict(kind='owned', game_use=True, raw_redistribute=False,
                            evidence=f'Original model drawn by Gemini 4 Argon in Blender via the Antigravity CLI on 18 Oct 2026 for {GAME}; '
                                     f'owned by Forge. No image textures, no third-party artwork. Source: paves-root/genres/riot/assets/{rel}.')
        c['provenance'].update(source=f'jobs/{slug}-v1', origin=f'paves-root/genres/riot/assets/{rel}', game='sous-les-paves',
                               imported='2026-10-19', kit='riot-kit')
        c['tags'] = sorted(set(c['tags']) | {t for x in a[7] for t in x.lower().replace('-', ' ').split()} | {'riot', 'paves', 'sous-les-paves'})
        c['summary'] = a[5][:300]
        if slug in ROT180:
            c['frame'] = {'contract': 'weapon/grip-origin-muzzle-minus-z', 'up': '+Y', 'unit': 'm', 'front': '-Z (muzzle)'}
            c['provenance']['library_copy'] = 'rotated 180 deg about Y (gltf-transform) from the game file so the muzzle points -Z'
        elif gkey or cls.startswith('part.character.outfit'):
            c['frame'] = {'contract': 'cast/ual' if not gkey else GEAR_CONTRACT, 'up': '+Y', 'unit': 'm',
                          'front': '+Z' if not gkey else '+Z (along the item, grip at the origin)'}
        if cls.startswith('part.character.outfit'):
            c['rig']['skeleton'] = 'ual'
            c['techniques'] = sorted(set(c['techniques']) | {'skinned'})
        # --- placement + Cast attach socket
        c['placement'] = {'role': 'cast-attachment' if gkey else ('cast-outfit' if 'outfit' in cls else 'architecture'),
                          'notes': a[6], 'source': 'authored'}
        if gkey:
            g = manifest[gkey]
            q = euler_to_quat(g['rot']); note = f"{GAME} cast/manifest.json gear.{gkey}: bone {g['bone']}, offset and Euler XYZ rotation in bone space"
            if slug in ROT180:   # q_new = q_game * Ry(180): same pose in the hand with the turned model
                q = quat_mul(euler_to_quat(g['rot']), [0, 1, 0, 0])
                note += '; rot = game rot composed with the 180 deg Y turn of the library copy (pose in the hand unchanged)'
            c['sockets'] = [dict(name='attach', role='attach', parent=g['bone'], pos=g['offset'], rot=q,
                                 manifest_euler_xyz=g['rot'], skeleton='ual', source='authored', note=note)]
            if cls.endswith('launcher'):
                b = m['bounds']
                c['sockets'].append(dict(name='muzzle', role='muzzle', parent=None,
                                         pos=[round((b['min'][0] + b['max'][0]) / 2, 3), round((b['min'][1] + b['max'][1]) / 2, 3),
                                              round(b['min'][2], 3)], rot=None, source='measured',
                                         note='centre of the -Z end of the bounding box (no muzzle empty in the file)'))
        c['release'] = cardlib.release_hash(c)
        cardlib.write(ROOT, c)
        print('patched', c['id'], c['render']['tris'], 'tris', [s['name'] for s in c['sockets']])


if __name__ == '__main__':
    main(*(sys.argv[2:3] if len(sys.argv) > 2 and sys.argv[1] == '--src' else []))
