#!/usr/bin/env python3
"""THE KILL LIBRARY HUB's data (KILLPAGES-A): writes 58k_library.js for 58_kill_library.html.

  python3 _tools/make_library.py            ledger + the cached live registry (_tools/sheets/lib_live.json)
  python3 _tools/make_library.py --live     also re-probes every live weapon page (headless, ~10 s a page) first
  python3 _tools/make_library.py --live-if-stale 3600   re-probes only when the cache is over an hour old (pages_autosync.sh)

Every kill in _tools/58k_LEDGER.md becomes a search row (name, weapon, rarity, MID / SKIN / REALM / DEEP / MULTI / MAP ELEMENT
/ VICTIM ROLE / ORGAN tags, the one-line CORE); the live pages' own registries (lib_probe.mjs) say which kills are playable
and give THE ROTATION: the order the game plays them in (K58.list: mythic first, then down the list), the skins and the
mid-round list. Builder lanes come from the ledger's section headers."""
import json, os, re, subprocess, sys, time

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LEDGER = os.path.join(ROOT, '_tools', '58k_LEDGER.md')
CACHE = os.path.join(ROOT, '_tools', 'sheets', 'lib_live.json')
OUT = os.path.join(ROOT, '58k_library.js')
RAR = ['MYTHIC', 'LEGENDARY', 'EPIC', 'RARE', 'COMMON']
SKINS = ['classic', 'taped', 'camo', 'cartoon', 'ice', 'neon', 'gold']

# the weapon list, in the index's order (shared with ix_status.py)
sys.path.insert(0, os.path.join(ROOT, '_tools'))
try:
    import ix_status
    WEAPONS = [w for w, _ in ix_status.WEAPONS]
    ALIASES = {w: a for w, a in ix_status.WEAPONS}
except Exception:  # pragma: no cover
    WEAPONS, ALIASES = [], {}


def read(p):
    with open(p, encoding='utf-8', errors='replace') as f:
        return f.read()


def lane_of(header):
    m = re.search(r'\((?:by )?([^)]*)\)', header)
    inner = (m.group(1) if m else '').strip()
    letter = re.match(r'(?:by )?([A-Z])\b', inner) or re.search(r'KILLPAGES-([A-Z])', inner) or re.search(r'by ([A-Z])\b', header)
    if letter:
        return letter.group(1)
    return 'STD'


def parse_ledger():
    rows, lanes, lane = [], {}, 'STD'
    for line in read(LEDGER).split('\n'):
        if line.startswith('## '):
            lane = lane_of(line)
            continue
        m = re.match(r'^- ([a-z0-9]+)\.([a-z0-9][a-z0-9-]*)\b(.*)$', line)
        if not m:
            continue
        w, slug, rest = m.group(1), m.group(2), m.group(3)
        key = w + '.' + slug
        nm = re.match(r'\s*\(([^)]*)\)', rest)
        name = nm.group(1).strip() if nm else slug.replace('-', ' ').upper()
        if name.lower().startswith('renamed') or len(name) > 60:
            name = slug.replace('-', ' ').upper()
        rar = next((r for r in RAR if re.search(r'·\s*' + r + r'\b', rest)), None)
        U = rest.upper()
        tags = []
        if '.mid-' in key or slug.startswith('mid-') or 'MID-ROUND' in U[:80]:
            tags.append('MID')
        if slug.startswith('skin-'):
            tags.append('SKIN')
        if 'DEATH REALM' in U or re.search(r'\bREALM\b', U[:200]):
            tags.append('REALM')
        if 'UNDERWATER' in U or 'DEEP ONLY' in U or "HOME DEEP" in U:
            tags.append('DEEP')
        if 'MULTI-VICTIM' in U or 'MULTI-KILL' in U or 'MULTI (' in U:
            tags.append('MULTI')
        if 'MAP ELEMENT' in U:
            tags.append('MAP ELEMENT')
        if 'VICTIM ROLE' in U:
            tags.append('VICTIM ROLE')
        if 'ORGAN MOMENT' in U:
            tags.append('ORGAN')
        status = 'ledgered'
        if re.search(r'·\s*PLAN\b', rest):
            status = 'plan'
        if re.search(r'·\s*SLATE\b', rest):
            status = 'slate'
        if re.search(r'\b(WITHDRAWN|DROPPED)\b', U[:160]):
            status = 'dropped'
        core = ''
        c = re.search(r'CORE:\s*(.*?)(?:\s·\s|$)', rest)
        if c:
            core = c.group(1).strip()
        if not core:
            s = re.search(r'(?:MOTION|BEATS):\s*(.*?)(?:\s·\s|$)', rest)
            core = s.group(1).strip() if s else ''
        core = re.sub(r'\s+', ' ', core)[:240]
        skin = slug[5:] if slug.startswith('skin-') and slug[5:] in SKINS else None
        rows.append({'k': key, 'w': w, 'n': name, 'r': rar, 'tags': tags, 'core': core, 'st': status, 'skin': skin, 'lane': lane})
        lanes.setdefault(w, lane)
    return rows, lanes


def realm_keys():
    """THE REALM REGISTRY (58k_REALMS.md) is the authority on which kills are death realms."""
    p = os.path.join(ROOT, '_tools', '58k_REALMS.md')
    keys = set()
    if not os.path.isfile(p):
        return keys
    for line in read(p).split('\n'):
        if not line.startswith('|'):
            continue
        cols = [x.strip() for x in line.split('|')]
        if len(cols) > 3:
            keys.update(re.findall(r'\b([a-z]+\.[a-z0-9][a-z0-9-]*)\b', cols[2]))
    return keys


def live_pages():
    out = []
    for w in WEAPONS:
        for n in [w] + ALIASES.get(w, []):
            p = f'58k_{n}.html'
            if os.path.isfile(os.path.join(ROOT, p)) and '58k_page.js' in read(os.path.join(ROOT, p)):
                out.append((w, p))
                break
    return out


def probe_all():
    cache = {}
    for w, p in live_pages():
        t0 = time.time()
        try:
            r = subprocess.run(['node', os.path.join(ROOT, '_tools', 'lib_probe.mjs'), p, w], capture_output=True, text=True, timeout=200, cwd=ROOT)
            d = json.loads(r.stdout.strip().splitlines()[-1])
            if d.get('lib'):
                cache[w] = {'page': p, 'lib': d['lib'], 'at': time.strftime('%d %b %H:%M')}
            print(f'  {w:12s} {p:24s} {"ok" if d.get("lib") else "no registry"}  {time.time() - t0:.0f}s', flush=True)
        except Exception as e:  # noqa
            print(f'  {w:12s} {p:24s} FAILED {e}', flush=True)
    os.makedirs(os.path.dirname(CACHE), exist_ok=True)
    old = json.loads(read(CACHE)) if os.path.isfile(CACHE) else {}
    old.update(cache)
    with open(CACHE, 'w', encoding='utf-8') as f:
        json.dump(old, f)
    return old


def main():
    rows, lanes = parse_ledger()
    stale = None
    if '--live-if-stale' in sys.argv:   # the autosync: plain every refresh, a full re-probe only when the cache is older than N s
        i = sys.argv.index('--live-if-stale')
        n = float(sys.argv[i + 1]) if len(sys.argv) > i + 1 else 3600
        stale = not os.path.isfile(CACHE) or time.time() - os.path.getmtime(CACHE) > n
    live = probe_all() if ('--live' in sys.argv or stale) else (json.loads(read(CACHE)) if os.path.isfile(CACHE) else {})
    by = {r['k']: r for r in rows}
    weapons, rotation, mids, skins, zones = {}, {}, {}, {}, []
    try:
        node = subprocess.run(['node', '-e', "global.window=global;require('./40_wdefs.js');const W=global.WDEFS;const o={};for(const k in W)o[k]={name:W[k].name,cat:W[k].cat};process.stdout.write(JSON.stringify(o))"], capture_output=True, text=True, cwd=ROOT, timeout=30)
        wdefs = json.loads(node.stdout)
    except Exception:
        wdefs = {}
    for w in WEAPONS:
        L = live.get(w)
        weapons[w] = {'name': (wdefs.get(w) or {}).get('name') or w.upper(), 'cat': (wdefs.get(w) or {}).get('cat') or '', 'lane': lanes.get(w, '?'), 'page': L['page'] if L else None}
        if not L:
            continue
        lib = L['lib']
        rotation[w] = [d['key'] for d in lib.get('list', [])]
        mids[w] = [d['key'] for d in lib.get('mids', [])]
        skins[w] = [d['key'] for d in lib.get('skins', [])]
        if lib.get('zones') and not zones:
            zones = lib['zones']
        for d in lib.get('list', []) + lib.get('skins', []) + lib.get('mids', []):
            r = by.get(d['key'])
            if not r:
                r = {'k': d['key'], 'w': w, 'n': d['name'], 'r': d['rarity'], 'tags': [], 'core': (d.get('sum') or '').split('. ')[0][:240], 'st': 'page', 'skin': d.get('skin'), 'lane': lanes.get(w, '?')}
                if d['kind'] == 'mid':
                    r['tags'].append('MID')
                if d['kind'] == 'skin':
                    r['tags'].append('SKIN')
                rows.append(r)
                by[r['k']] = r
            r['live'] = True
            r['ready'] = bool(d.get('ready'))
            r['n'] = d['name'] or r['n']
            r['r'] = d['rarity'] or r['r']
            r['arena'] = d.get('arena')
            r['len'] = d.get('len')
            if d.get('arena') == 'deep' and 'DEEP' not in r['tags']:
                r['tags'].append('DEEP')
            if (d.get('victims') or 1) >= 2 and 'MULTI' not in r['tags']:
                r['tags'].append('MULTI')
            if d['kind'] == 'mid' and 'MID' not in r['tags']:
                r['tags'].append('MID')
    RK = realm_keys()
    for r in rows:
        if r['k'] in RK and 'REALM' not in r['tags']:
            r['tags'].append('REALM')
        r.setdefault('live', False)
        r.setdefault('ready', False)
    rows.sort(key=lambda r: (WEAPONS.index(r['w']) if r['w'] in WEAPONS else 99, r['k']))
    lane_ids = []
    for w in WEAPONS:
        l = weapons[w]['lane']
        if l not in lane_ids:
            lane_ids.append(l)
    out = {'updated': time.strftime('%d %b %H:%M'), 'order': WEAPONS, 'weapons': weapons, 'lanes': lane_ids, 'kills': rows,
           'rotation': rotation, 'mids': mids, 'skins': skins, 'zones': zones, 'rarities': RAR}
    js = '/* generated by _tools/make_library.py for 58_kill_library.html; do not edit */\nwindow.LIB = ' + json.dumps(out, separators=(',', ':')) + ';\n'
    with open(OUT, 'w', encoding='utf-8') as f:
        f.write(js)
    print(f'58k_library.js: {len(rows)} kills ({sum(1 for r in rows if r["live"])} on live pages), {len(rotation)} live weapons, {len(zones)} zone looks')


if __name__ == '__main__':
    main()
