"""Compile cards/ into index.json (tree with counts, aliases, one compact row per asset's latest version).

usage: python3 tools/build_index.py [--measured browse/measured.json]
The browser pass (tools/gpu/measure_probe.py) writes exact bounds for every file, skinned ones included; when present
they replace the static numbers for size (source 'browser').
"""
import json, os, sys, time, hashlib

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


def row(c):
    m = c['size']['measured_m']; k = c['size'].get('scale_to_class', 1)
    glb = c['files']['glb'].split(':')[1]
    return {
        'id': c['id'], 'version': c['version'], 'release': c['release'], 'name': c['name'], 'summary': c['summary'],
        'tags': c['tags'], 'style': c['style'], 'status': c['status'],
        'size': {'w': round(m['w'] * k, 3), 'l': round(m['l'] * k, 3), 'h': round(m['h'] * k, 3)},
        'scale': k, 'size_source': c['size']['source'], 'in_band': c['size'].get('in_band'),
        'front_suspect': bool(c['size'].get('front_suspect')),
        'tris': c['render']['tris'], 'materials': c['render']['materials'], 'draw_calls': c['render']['draw_calls'],
        'gpu_mb': c['render']['gpu_mb_decoded'], 'bytes': c['render']['bytes'], 'codec': c['render']['codec'],
        'rig': (c.get('rig') or {}).get('skeleton'), 'clips': len(c.get('animations') or []),
        'sockets': [s['name'] for s in c.get('sockets') or []],
        'reuse': c.get('reuse', {'omg': bool(c['licence']['game_use']), 'omu': bool(c['licence']['game_use']), 'reason': '' if c['licence']['game_use'] else 'licence not cleared'}),
        'thumbnail': c.get('thumbnail', f"site/out/measure/thumbs/{glb}.jpg"), 'source_photos': c.get('source_photos', []),
        'provenance': c['provenance'],
        'game_use': c['licence']['game_use'], 'licence': c['licence']['kind'], 'author': c['provenance'].get('author'),
        'kit': c['provenance'].get('kit'), 'glb': glb, 'card': os.path.relpath(cardlib.path_of(ROOT, c), ROOT),
        'ktx2': any(t.get('fmt') == 'ktx2' for t in c['render'].get('textures') or []),
        # destructible tanks (tank-damage/v1): the second file games load for hit marks, dents, thrown tracks and the wreck
        **({'damage_glb': c['files']['damage_glb'].split(':')[1],
            'damage_bytes': ((c.get('nodes') or {}).get('damage') or {}).get('file', {}).get('bytes', 0)} if (c.get('files') or {}).get('damage_glb') else {}),
    }


def link_variants(rows):
    """parent / variants on the index rows. A variant keeps its own description, thumbnail and flags; its reuse can only be
    stricter than the parent's (a variant of something not cleared for OMG is not cleared either).
    A newer version of the same id (a painted copy, a re-run) has `variant` but no `parent` row: it is the same asset."""
    byid = {r['id']: r for r in rows}
    kids = {}
    for r in rows:
        pv = r['provenance']; par, var = pv.get('parent'), pv.get('variant')
        if var:
            r['variant'] = var
        if not par or par['id'] == r['id']:
            continue
        pr = byid.get(par['id'])
        r['parent'] = dict(par, name=pr['name'] if pr else None, in_index=bool(pr))
        if not pr:
            continue
        kids.setdefault(par['id'], []).append(r['id'])
        for t in ('omg', 'omu'):
            if r['reuse'].get(t) and not pr['reuse'].get(t):
                r['reuse'] = dict(r['reuse'], **{t: False}, reason=(r['reuse'].get('reason') or f"parent {pr['id']} not cleared for {t}"))
    for r in rows:
        r['variants'] = sorted(kids.get(r['id'], []))


def main():
    measured = {}
    if '--measured' in sys.argv:
        measured = json.load(open(sys.argv[sys.argv.index('--measured') + 1]))
    allc = cardlib.load_all(ROOT)
    ep = os.path.join(ROOT, 'embeddings.json')
    descriptions = json.load(open(ep)).get('assets', {}) if os.path.exists(ep) else {}
    tree = json.load(open(f'{ROOT}/taxonomy/tree.json'))
    rows = []
    up = os.path.join(ROOT, 'usage', 'by_asset.json')  # who uses what (tools/usage_index.py); engine-kit copies are not counted
    usage = json.load(open(up)) if os.path.exists(up) else {}
    for cid, versions in sorted(allc.items()):
        c = cardlib.active(versions)
        errs = cardlib.validate(c)
        if errs:
            print('INVALID', cid, errs); continue
        r = row(c)
        kdesc = hashlib.sha256((c['id']+'@'+c['release']).encode()).hexdigest()
        visual = descriptions.get(kdesc) or c.get('visual') or {}
        sp = os.path.join(ROOT,'safety',hashlib.sha256(visual.get('description','').encode()).hexdigest()+'.json')
        if os.path.exists(sp): visual = dict(visual,safety=json.load(open(sp)))
        if visual.get('description'):
            r['visual_description'] = visual['description']
            r['summary'] = visual['description']
        if visual.get('safety',{}).get('blocked') and 'reuse' not in c:
            r['reuse'] = {'omg':False,'omu':False,'reason':'thumbnail shows third-party artwork or a person'}
        b = measured.get(r['glb'])
        if b and r['size_source'] != 'measured':
            k = r['scale']; s = b['size']
            r['size'] = {'w': round(s[0] * k, 3), 'h': round(s[1] * k, 3), 'l': round(s[2] * k, 3)}
            r['size_source'] = 'browser'
            c_size = c['size']
            if c_size.get('band'):
                lo, hi = c_size['band']; v = r['size']['l'] if c_size['band_axis'] == 'length' else r['size']['h']
                r['in_band'] = lo <= v <= hi
        r['versions'] = len(versions)
        u = [x for x in usage.get(cid, {}).get('users', []) if not x.get('bulk')]
        if u:
            r['used_by'] = {'count': len(u), 'slugs': [f"{x['platform']}:{x['user']}" for x in u[:10]]}
        if len(versions) > 1:  # the viewer's version switch (lighter copies are drafts until 'Use in games')
            r['vers'] = [{'v': v['version'], 'glb': v['files']['glb'].split(':')[1], 'tris': v['render']['tris'],
                          'bytes': v['render']['bytes'], 'status': v['status'], 'active': v is c,
                          'card': os.path.relpath(cardlib.path_of(ROOT, v), ROOT), **({'lighter': v['lighter']} if v.get('lighter') else {})}
                         for v in versions if not cardlib.validate(v)]
        rows.append(r)
        kind, grp, cls = cid.split('/')[0].split('.')
        node = tree.setdefault(kind, {'summary': '', 'groups': {}})
        node['count'] = node.get('count', 0) + 1
        g = node['groups'].setdefault(grp, {'summary': '', 'classes': []})
        g['count'] = g.get('count', 0) + 1
        g.setdefault('class_counts', {})[cls] = g.get('class_counts', {}).get(cls, 0) + 1
        if cls not in g['classes']:
            g['classes'].append(cls)
    link_variants(rows)
    idx = {'version': 1, 'built': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'count': len(rows),
           'tree': tree, 'aliases': json.load(open(f'{ROOT}/taxonomy/aliases.json')), 'assets': rows}
    out = os.path.join(ROOT, 'index.json')
    open(out, 'w').write(json.dumps(idx, separators=(',', ':'), ensure_ascii=False))
    print(f'index.json: {len(rows)} assets, {os.path.getsize(out) // 1024} KB')


if __name__ == '__main__':
    main()
