"""Backfill usage events from what exists today. Writes usage/backfill/<source>.jsonl (replaced each run).

  --briefs <file.json>   Factory brief rows (tools/usage_db_query.sh output): OMG games built with a library pack
  --scan <dir> [...]     hand-built games: GLB files whose sha256 is a library blob, text files naming a library id or glb sha
Times are UTC. The user of a scanned file is the first directory under the scan root, minus worktree wrappers.
"""
import glob, hashlib, json, os, re, sys, time

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

TEXT_EXT = {'.json', '.mjs', '.js', '.ts', '.html', '.md', '.py', '.jsonl', '.txt', '.yml', '.yaml'}
SKIP_DIRS = {'node_modules', '.git', '.scratch', 'dist', '__pycache__'}
ID_RE = re.compile(r'\b(?:building|character|monster|motion|part|prop|vehicle|weapon)\.[a-z0-9-]+\.[a-z0-9-]+/[a-z0-9][a-z0-9-]*')
HEX_RE = re.compile(r'\b[0-9a-f]{64}\b')


def catalogue(root):
    """release -> (id, version); glb sha (glb + damage_glb) -> (id, version, release); all card versions."""
    rel, blob = {}, {}
    for cid, versions in cardlib.load_all(root).items():
        for c in versions:
            r = c.get('release') or cardlib.release_hash(c)
            rel[r] = (cid, c['version'])
            for k in ('glb', 'damage_glb'):
                f = (c.get('files') or {}).get(k)
                if f and ':' in f:
                    blob[f.split(':')[1]] = (cid, c['version'], r)
    return rel, blob


def now():
    return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())


def slug(s):
    return re.sub(r'[^a-z0-9]+', '-', (s or '').lower()).strip('-')[:48] or 'untitled'


def from_briefs(path, rel):
    ev, missing = [], 0
    for b in json.load(open(path)):
        runs = [r for r in (b.get('runs') or []) if r.get('slug')]
        user = runs[-1]['slug'] if runs else f"{slug(b.get('title'))}-{b['brief_id'][:8]}"
        for r in b.get('releases') or []:
            if r not in rel:
                missing += 1
                continue
            aid, ver = rel[r]
            ev.append({'ts': (b.get('ts') or '')[:19] + 'Z', 'asset_id': aid, 'version': ver, 'release': r, 'user': user,
                       'platform': 'omg', 'source': 'db-brief:' + b['brief_id'][:8]})
    return ev, missing


def project_of(scan_root, path):
    parts = os.path.relpath(path, scan_root).split(os.sep)
    return parts[0] if parts and parts[0] not in ('.',) else 'root'


def scan(scan_root, rel, blob, ids):
    ev, seen = [], set()
    for dp, dns, fns in os.walk(scan_root):
        dns[:] = [d for d in dns if d not in SKIP_DIRS and not (d.startswith('forge-assets') and dp == scan_root)]
        for fn in fns:
            p = os.path.join(dp, fn)
            ext = os.path.splitext(fn)[1].lower()
            try:
                st = os.stat(p)
                if os.path.islink(p):
                    continue
                if ext == '.glb' and st.st_size < 60_000_000:
                    sha, src = hashlib.sha256(open(p, 'rb').read()).hexdigest(), 'glb-hash'
                    hits = [blob[sha]] if sha in blob else []
                elif ext in TEXT_EXT and st.st_size < 3_000_000:
                    txt = open(p, encoding='utf-8', errors='ignore').read()
                    src = 'text-ref'
                    hits = [(i, None, None) for i in set(ID_RE.findall(txt)) if i in ids]
                    hits += [blob[h] for h in set(HEX_RE.findall(txt)) if h in blob]
                else:
                    continue
            except OSError:
                continue
            user = project_of(scan_root, p)
            for aid, ver, r in hits:
                k = (aid, ver, user, src)
                if k in seen:
                    continue
                seen.add(k)
                ev.append({'ts': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(st.st_mtime)), 'asset_id': aid, 'version': ver,
                           'release': r, 'user': user, 'platform': 'hand', 'source': src})
    return ev


def write(name, ev):
    d = os.path.join(ROOT, 'usage', 'backfill')
    os.makedirs(d, exist_ok=True)
    with open(os.path.join(d, name + '.jsonl'), 'w') as f:
        for e in ev:
            f.write(json.dumps(e, sort_keys=True) + '\n')
    print(f'{name}: {len(ev)} events')


def main(argv):
    rel, blob = catalogue(ROOT)
    ids = {c for c, _ in rel.values()}
    if '--briefs' in argv:
        ev, missing = from_briefs(argv[argv.index('--briefs') + 1], rel)
        write('omg-briefs', ev)
        print(f'  releases not in cards: {missing}')
    if '--scan' in argv:
        for d in argv[argv.index('--scan') + 1:]:
            if d.startswith('--'):
                break
            write('hand-' + slug(os.path.basename(d.rstrip('/'))), scan(d, rel, blob, ids))


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