"""Card for a finished spacecraft/v1 or hover/v1 job: tools/vehicles/import_vehicle.py (blob, card, Paint recolour field, sockets
of the shared roles) then the space fields a game reads:
  frame.contract  library/spacecraft-+Z-nose-+X-left-origin-centre | library/hover-+Z-front-+X-left-origin-ground
  nodes           body, turrets, guns, moving {name: {kind, axis}}, thrusters, lifts (+ doors/rings/fans lists for old readers)
  sockets         thruster / lift / rcs / dock / hangar / camera empties (import_vehicle only knows the vehicle roles)
  gameplay        length, beam, height, scale (fighter-size, ship, capital), hover height for speeders
usage: python3 tools/space/import_space.py <job_dir>"""
import json, math, 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.join(ROOT, 'tools', 'vehicles'))
import glb, card as cardlib  # noqa: E402
import import_vehicle  # noqa: E402
from check_vehicle import world_nodes  # noqa: E402
from check_space import MOVING  # noqa: E402

KIND = [(r'Turret_\d+', 'turret'), (r'Gun_\d+', 'gun'), (r'Ring(_\d+)?|Core|Drill|Fan_\w+', 'spinner'), (r'Radar', 'dish'),
        (r'Wing_[LR]', 'swing-wing'), (r'Radiator_[LR]|BayDoor_[LR]|Ramp|Hatch|Canopy', 'hinge'), (r'Door_[LR]', 'slide'),
        (r'Arm_\w+', 'arm'), (r'Pod_\w+', 'tilt'), (r'Vane_[LR]|Yoke', 'steer')]
DEFAULT_AXIS = {'turret': [0, 1, 0], 'gun': [-1, 0, 0], 'spinner': [0, 1, 0], 'dish': [0, 1, 0], 'swing-wing': [0, 1, 0],
                'hinge': [1, 0, 0], 'slide': [0, 0, -1], 'arm': [1, 0, 0], 'tilt': [1, 0, 0], 'steer': [0, 1, 0]}
SOCKET = {'thruster': 'thruster', 'lift': 'lift', 'rcs': 'rcs', 'dock': 'dock', 'hangar': 'hangar', 'camera': 'camera'}


def unit(v):
    try:
        v = [float(x) for x in v][:3]
        n = math.sqrt(sum(x * x for x in v))
        return [round(x / n, 4) for x in v] if n > 1e-6 and len(v) == 3 else None
    except Exception:  # noqa: BLE001
        return None


def main(jd):
    jd = os.path.abspath(jd); j = json.load(open(f'{jd}/job.json'))
    import_vehicle.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()}
    mesh = lambda n: doc['nodes'][wn[n][1]].get('mesh') is not None or bool(doc['nodes'][wn[n][1]].get('children'))
    hover = j.get('contract') == 'hover/v1'
    info = {}
    if os.path.exists(f'{jd}/nodes.json'):
        try:
            info = json.load(open(f'{jd}/nodes.json'))
        except Exception:  # noqa: BLE001
            info = {}
    axes_in = info.get('axes') if isinstance(info, dict) and isinstance(info.get('axes'), dict) else {}
    moving = {}
    for n in sorted(pos):
        if not re.fullmatch(MOVING, n) or not mesh(n):
            continue
        kind = next(k for rx, k in KIND if re.fullmatch(rx, n))
        ax = axes_in.get(n)
        if isinstance(ax, dict):  # some authors write {"axis": [...]} or {"yaw": [...], "pitch": [...]}
            ax = ax.get('axis') or ax.get('yaw') or ax.get('pitch') or next(iter(ax.values()), None)
        axis = unit(ax) if ax is not None else None
        moving[n] = {'kind': kind, 'axis': axis or DEFAULT_AXIS[kind], 'axis_source': 'author' if axis else 'default'}
    # signed axes measured from the geometry (17 Oct 2026: 105 of 109 guns elevated DOWN with the authors' Blender +X axes)
    from axes_space import measured_axes
    for n, (ax, _how) in measured_axes(f, moving).items():
        if ax: moving[n].update(axis=list(ax), axis_source='measured')
    pick = lambda rx: sorted((n for n in pos if re.fullmatch(rx, n)), key=lambda s: [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', s)])
    nodes = c.get('nodes') or {}
    nodes['body'] = [n for n in ('Hull',) if n in pos]
    for key, rx in (('turrets', r'Turret_\d+'), ('guns', r'Gun_\d+'), ('thrusters', r'Thruster_\d+'), ('lifts', r'Lift_\d+'),
                    ('rings', r'Ring(_\d+)?|Core'), ('fans', r'Fan_\w+'), ('arms', r'Arm_\w+'), ('pods', r'Pod_\w+'),
                    ('muzzles', r'Muzzle_\d+'), ('hardpoints', r'Hardpoint_\d+'), ('hangars', r'Hangar_\d+'), ('docks', r'Dock_\d+')):
        got = pick(rx)
        if got: nodes[key] = got
        else: nodes.pop(key, None)
    doors = pick(r'(Door_[LR]|BayDoor_[LR]|Ramp|Hatch|Canopy|Radiator_[LR])')
    doors = [d for d in doors if mesh(d)]
    if doors: nodes['doors'] = doors
    if moving: nodes['moving'] = moving
    nodes['axes'] = {n: v['axis'] for n, v in moving.items()}
    nodes['limits'] = {n: ({'yaw': [-3.1416, 3.1416]} if v['kind'] == 'turret' else [-0.2, 0.9] if v['kind'] == 'gun' else
                           [0, 1.4] if v['kind'] in ('hinge', 'arm') else [-0.6, 0.6] if v['kind'] in ('swing-wing', 'tilt', 'steer') else
                           [0, 2.0] if v['kind'] == 'slide' else None) for n, v in moving.items()}
    nodes['limits'] = {k: v for k, v in nodes['limits'].items() if v is not None}
    for k in ('wheels', 'steer'):
        if not nodes.get(k): nodes.pop(k, None)
    c['nodes'] = nodes
    have = {s['name'] for s in c.get('sockets', [])}
    for n, v in pos.items():
        head = n.lower().split('_')[0]
        if head in SOCKET and not mesh(n) and n.lower() not in have:
            c.setdefault('sockets', []).append(dict(name=n.lower(), role=SOCKET[head], parent=None, pos=v, rot=None, source='authored'))
    m = c['size']['measured_m']
    L = m['l']
    gp = {'source': 'measured', 'length': L, 'beam': m['w'], 'height': m['h'],
          'scale': 'vehicle' if hover else 'small-craft' if L <= 40 else 'ship' if L <= 250 else 'capital'}
    if hover:
        b = glb.measure(f)['bounds']
        gp['hover_height'] = round(b['min'][1], 3)
    c['gameplay'] = gp
    c['frame']['contract'] = ('library/hover-+Z-front-+X-left-origin-ground' if hover else
                              'library/spacecraft-+Z-nose-+X-left-origin-centre')
    c['tags'] = sorted(set(c.get('tags', [])) | {'space' if not hover else 'hover', 'sci-fi', j['family']})
    c['techniques'] = sorted(set(c.get('techniques', [])) | {'paint-factor', 'baked-pbr'})
    c['release'] = cardlib.release_hash(c)
    print(cardlib.write(ROOT, c), len(moving), 'moving parts,', len(nodes.get('thrusters', [])), 'thrusters,', len(c['sockets']), 'sockets')


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