"""Card for a finished Astra vehicle job: tools/import_job.py (blob + card) then the vehicle runtime fields a game reads:
nodes (wheels, steering, rider points, doors), sockets with measured positions, paint (Paint material factor + the
six paints from nodes.json) and gameplay (wheelbase, track). usage: python3 tools/vehicles/import_vehicle.py <job_dir>"""
import json, os, re, sys
ROOT = os.path.dirname(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')); sys.path.insert(0, os.path.dirname(__file__))
import glb, card as cardlib, import_job  # noqa: E402
from check_vehicle import world_nodes  # noqa: E402


def main(jd):
    jd = os.path.abspath(jd); j = json.load(open(f'{jd}/job.json'))
    # a card version is never reused by another job (11 Oct 2026: the tank retrofit jobs, version 2, overwrote the v2 cards of the
    # 10 Oct repacks of their v1 jobs): pick the next free version in job.json instead
    p0 = cardlib.path_of(ROOT, {'id': j['id'], 'version': j.get('version', 1)})
    if os.path.exists(p0):
        was = (json.load(open(p0)).get('provenance') or {}).get('job')
        if was and was != os.path.basename(jd):
            sys.exit(f"{p0} belongs to job {was}: set job.json version to a free card version (see {os.path.dirname(p0)})")
    import_job.main(jd)
    p = cardlib.path_of(ROOT, {'id': j['id'], 'version': j.get('version', 1)})
    c = json.load(open(p))
    f = f"{jd}/{j['slug']}.glb"
    doc = glb.read(f)[0]
    wn = world_nodes(doc)
    pos = {n: v[0] for n, v in wn.items()}
    fam = j.get('family'); two = fam in ('motos', 'bikes')
    WRE = r'Wheel_(F|R|FL|FR|RL|RR|RL2|RR2|L\d+|R\d+)'
    def worder(n):
        base = ['Wheel_F', 'Wheel_FL', 'Wheel_FR', 'Wheel_R', 'Wheel_RL', 'Wheel_RR', 'Wheel_RL2', 'Wheel_RR2']
        return (0, base.index(n), 0) if n in base else (1, n[6], int(n[7:]))
    wheels = sorted([n for n in pos if re.fullmatch(WRE, n)], key=worder)
    solid = lambda n: doc['nodes'][wn[n][1]].get('mesh') is not None or bool(doc['nodes'][wn[n][1]].get('children'))
    doors = sorted(n for n in pos if re.fullmatch(r'(Door_\w+|BayDoor_[LR]|Ramp|Canopy|Hatch_\w+|Hook)', n) and solid(n))  # empties = entry points
    steer = ['Fork'] if two else [n for n in wheels if n in ('Wheel_FL', 'Wheel_FR')]
    body = [n for n in ('Body', 'Frame', 'Hull', 'Fuselage') if n in pos][:1]
    nodes = {'body': body, 'wheels': wheels, 'steer': steer}
    pick = lambda rx: sorted(n for n in pos if re.fullmatch(rx, n))
    for key, rx in (('handlebar', r'Handlebar'), ('steering_wheel', r'SteeringWheel'), ('kickstand', r'Kickstand'), ('turret', r'Turret'),
                    ('guns', r'(Gun|Gun_[LR]|Gun_Hull|MG|MG_\d+)'), ('barrels', r'Barrel'), ('tracks', r'Track_[LR]'), ('rotors', r'(MainRotor|TailRotor|Rotor_Front|Rotor_Rear|Rotodome)'),
                    ('gear', r'Gear_\w+'), ('surfaces', r'(Aileron|Elevon|Elevator|Stabilator|Rudder|Canard|Flap|DragRudder)(_\w+)?')):
        if pick(rx): nodes[key] = pick(rx)
    if 'Crank' in pos: nodes['crank'] = ['Crank'] + [n for n in ('Pedal_L', 'Pedal_R') if n in pos]
    if doors: nodes['doors'] = doors
    sockets = []
    for n in wheels:
        sockets.append(dict(name=n.lower(), role='wheel', parent=n, pos=pos[n], rot=[0, 0, 0, 1], spin_axis='X',
                            radius=round(pos[n][1], 3), steer=(n in steer) or (two and n == 'Wheel_F'), source='measured'))
    roles = {'seat': 'seat', 'hand': 'hand', 'foot': 'foot', 'door': 'door', 'light': 'light', 'exhaust': 'exhaust', 'muzzle': 'muzzle',
             'hardpoint': 'hardpoint', 'troop': 'seat', 'fx': 'fx', 'nozzle': 'nozzle', 'engine': 'nozzle', 'center': 'center_of_mass'}
    for n, v in pos.items():
        low = n.lower(); head = low.split('_')[0]
        if head in roles and n not in doors and doc['nodes'][wn[n][1]].get('mesh') is None:
            sockets.append(dict(name=low, role=roles[head], parent=None, pos=v, rot=None, source='authored'))
    paints = []
    if os.path.exists(f'{jd}/nodes.json'):
        try:
            paints = json.load(open(f'{jd}/nodes.json')).get('paints') or []
        except Exception:  # noqa: BLE001
            paints = []
    def rgb(p):
        if isinstance(p, dict):
            p = next((v for k, v in p.items() if isinstance(v, list) and len(v) >= 3 and 'rgb' in k.lower() or k in ('linear', 'color', 'colour')), None)
        return [round(float(x), 4) for x in p[:3]] if isinstance(p, list) and len(p) >= 3 else None
    names = [p.get('name') if isinstance(p, dict) else None for p in paints]
    pairs = [(rgb(p), n) for p, n in zip(paints, names) if rgb(p)]
    paints, names = [a for a, _ in pairs], [n or f'paint {i + 1}' for i, (_, n) in enumerate(pairs)]
    mats = {m.get('name'): m for m in doc.get('materials', [])}
    fac = (mats.get('Paint', {}).get('pbrMetallicRoughness', {}) or {}).get('baseColorFactor', [1, 1, 1, 1])[:3]
    c['paint'] = {'encoding': 'material factor', 'material': 'Paint', 'default': [round(x, 4) for x in fac],
                  'paints': paints or [[round(x, 4) for x in fac]], 'names': names if paints else ['default'], 'colour_space': 'linear',
                  'how': "set the Paint material's color (linear RGB) to recolour; nothing else uses Paint"}
    c['nodes'] = nodes
    c['sockets'] = sockets
    fz = [pos[n][2] for n in wheels if n in ('Wheel_F', 'Wheel_FL', 'Wheel_FR')]
    rz = [pos[n][2] for n in wheels if n not in ('Wheel_F', 'Wheel_FL', 'Wheel_FR')]
    gp = {'source': 'measured'}
    if fz and rz:
        gp['wheelbase'] = round(sum(fz) / len(fz) - min(rz), 3)
    if 'Wheel_FL' in pos and 'Wheel_FR' in pos:
        gp['track'] = round(pos['Wheel_FL'][0] - pos['Wheel_FR'][0], 3)
    c['gameplay'] = gp
    c['frame']['contract'] = {'motos': 'library/two-wheel-+Z-front-+X-left', 'bikes': 'library/two-wheel-+Z-front-+X-left',
                              'tanks': 'library/tracked-+Z-front-+X-left-origin-ground-centre',
                              'rotor': 'library/rotorcraft-+Z-nose-+X-left-origin-under-mast',
                              'jets': 'library/aircraft-+Z-nose-+X-left-origin-under-main-gear'}.get(fam, 'garage/v2')
    if j.get('damage'):
        damage_fields(jd, j, c, doc, wn, pos)
    if j.get('tags'):
        c['tags'] = sorted(set(c.get('tags', [])) | set(j['tags']))
    c['techniques'] = sorted(set(c.get('techniques', [])) | {'paint-factor', 'baked-pbr'})
    c['release'] = cardlib.release_hash(c)
    print(cardlib.write(ROOT, c), len(sockets), 'sockets,', len(c['paint']['paints']), 'paints')


def damage_fields(jd, j, c, doc, wn, pos):
    """tank-damage/v1 (11 Oct 2026): c.files.damage_glb + c.nodes.damage = what a game shows and hides for each state,
    read from the node NAMES of the two files (never from the author's own JSON), + standard tank axes and aim limits."""
    import hashlib, shutil
    from check_vehicle import check_damage
    rep = check_damage(jd, j, doc, wn, None)
    kids = {n: {doc['nodes'][k].get('name') for k in _all(doc, i)} for n, (_, i) in wn.items() if n in ('Turret',)}
    on_turret = kids.get('Turret', set())
    b = c['bounds'] if isinstance(c.get('bounds'), dict) else {}
    m = c['size']['measured_m']; hw, hl = m['w'] / 2, m['l'] / 2
    def zone(n):
        if n in on_turret or n.startswith(('Part_Turret', 'Dmg_Turret')): return 'turret'
        x, y, z = pos[n]; nx, nz = x / hw, z / hl
        if abs(nx) < 0.45 and abs(nz) < 0.55: return 'top'
        return ('left' if x > 0 else 'right') if abs(nx) >= abs(nz) else ('front' if z > 0 else 'rear')
    parts = sorted(n for n in pos if n.startswith('Part_') and doc['nodes'][wn[n][1]].get('mesh') is not None)
    d = {'contract': 'tank-damage/v1', 'states': rep['states'], 'parts': [], 'scars': {}, 'broken': {}, 'wreck': {}, 'attach': {}}
    f = f"{jd}/{j['slug']}-damage.glb"
    dn = {}
    if os.path.exists(f):
        h = hashlib.sha256(open(f, 'rb').read()).hexdigest()
        shutil.copyfile(f, f'{ROOT}/store/blobs/{h}.glb')
        c['files']['damage_glb'] = f'sha256:{h}'
        dd = glb.read(f)[0]; dn = world_nodes(dd)
        dmm = glb.measure(f)
        d['file'] = {'key': 'damage_glb', 'tris': dmm['tris'], 'bytes': dmm['bytes'], 'gpu_mb_decoded': round(dmm['gpu_mb_decoded'], 1)}
    for n in parts:
        t = 'Dmg_' + n[5:]
        d['parts'].append({'node': n, 'twin': t if t in dn else None, 'zone': zone(n), 'pos': pos[n]})
    for n in sorted(dn):
        if n.startswith('Scar_'):
            d['scars'][n[5:].lower().replace('_', '-')] = n
    base_gun = 'Gun' if 'Gun' in pos else 'Gun_L'
    for a, bb in (('Track_L', 'Track_L_Broken'), ('Track_R', 'Track_R_Broken'), (base_gun, 'Gun_Broken')):
        if bb in dn and a in pos: d['broken'][a] = bb
    for k, n in (('hull', 'Wreck_Hull'), ('turret', 'Wreck_Turret')):
        if n in dn: d['wreck'][k] = n
    if 'Hull_Damage' in dn: d['attach']['Hull_Damage'] = 'Hull'
    if 'Turret_Damage' in dn: d['attach']['Turret_Damage'] = 'Turret' if 'Turret' in pos else 'Hull'
    d['fx'] = {n[3:].lower(): pos[n] for n in sorted(pos) if n.startswith('Fx_')}
    d['how'] = ('load files.damage_glb once, add it at the tank origin, hide all of it; put Turret_Damage under the node in attach '
                '(same pivot); hit = show a Scar_ and swap a Part_ for its twin; stripped = hide Part_ nodes (or throw them); '
                'broken = swap the node for its _Broken twin; wreck = hide the whole healthy file, show Wreck_Hull (+ Wreck_Turret, '
                'which may be thrown off). Paint in both files takes the same colour.')
    c['nodes']['damage'] = d
    if rep['errors'] or rep['warnings']:
        c['quality']['damage'] = {'errors': rep['errors'], 'warnings': rep['warnings']}
    axes, limits = dict(c['nodes'].get('axes') or {}), dict(c['nodes'].get('limits') or {})
    for n in c['nodes'].get('guns', []):
        axes[n] = {'yaw': [0, 1, 0], 'pitch': [-1, 0, 0]}
        limits[n] = ({'yaw': [0, 0], 'pitch': [-0.12, 0.35]} if n == 'Gun' and 'Turret' in pos else
                     {'yaw': [-0.44, 0.44], 'pitch': [-0.25, 0.3]} if n == 'Gun' else
                     {'yaw': [-0.9, 0.9], 'pitch': [-0.25, 0.35]} if n in ('Gun_L', 'Gun_R') else
                     {'yaw': [0, 0], 'pitch': [-0.25, 0.44]} if n == 'Gun_Hull' else {'yaw': [-1.0, 1.0], 'pitch': [-0.15, 0.6]})
    if 'Turret' in pos: axes['Turret'] = [0, 1, 0]
    for n in c['nodes'].get('barrels', []):
        axes[n] = [0, 0, -1]; limits[n] = [0, 0.35]  # metres of recoil along the bore
    c['nodes']['axes'], c['nodes']['limits'] = axes, limits


def _all(doc, i):
    out, stack = [], list(doc['nodes'][i].get('children', []))
    while stack:
        k = stack.pop(); out.append(k); stack += doc['nodes'][k].get('children', [])
    return out


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