#!/usr/bin/env python3
"""Reference builder: pack several dictionaries into ONE importable file.

A bundle is a plain ZIP of `.slovarypack` files and, optionally, the matching
`<pack_id>.vectors.zip` beside each of them (PACK_FORMAT.md section 6a). One
file, one link, one import step — pasting forty URLs is not an instruction a
user or a reviewer will follow.

Members are STORED, not deflated: a `.slovarypack` is a SQLite file that gzips
well, but a `.vectors.zip` is already compressed, and re-compressing the lot
costs minutes of CPU on both ends to save a few per cent. Storing also lets the
app read the central directory instantly.

    python scripts/db/pack_tools/make_bundle.py \
        --in data/packs --out slovary-corpus.slovarybundle

Standard library only, like the rest of pack_tools — this file is published as
the reference implementation.
"""
from __future__ import annotations

import argparse
import json
import sqlite3
import sys
import zipfile
from pathlib import Path


def read_meta(pack: Path) -> dict:
    con = sqlite3.connect(f"file:{pack.as_posix()}?mode=ro", uri=True)
    try:
        return dict(con.execute("SELECT key, value FROM pack_meta"))
    finally:
        con.close()


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("--in", dest="src", required=True,
                    help="directory holding .slovarypack (+ .vectors.zip)")
    ap.add_argument("--out", required=True, help="output bundle file")
    ap.add_argument("--no-vectors", action="store_true",
                    help="dictionaries only, leave the vectors out")
    args = ap.parse_args()

    src = Path(args.src)
    packs = sorted(src.glob("*.slovarypack"))
    if not packs:
        raise SystemExit(f"no .slovarypack files in {src}")

    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)

    # Pass 1: describe everything BEFORE writing a byte of it. The listing has
    # to be the first member of the archive (see below), and it cannot be
    # written first unless it is known first.
    listing = []
    for pack in packs:
        meta = read_meta(pack)
        pack_id = meta.get("pack_id", "")
        if not pack_id:
            print(f"  SKIP {pack.name}: no pack_id in pack_meta",
                  file=sys.stderr)
            continue
        entries = int(meta.get("entries_count", 0))
        item = {
            "pack_id": pack_id,
            "name": meta.get("name", pack_id),
            "lang": meta.get("lang", ""),
            "entries_count": entries,
            "size": pack.stat().st_size,
            "pack_version": meta.get("pack_version", ""),
            "license": meta.get("license", ""),
        }
        vec = src / f"{pack_id}.vectors.zip"
        if vec.exists() and not args.no_vectors:
            with zipfile.ZipFile(vec) as vz:
                vmeta = json.loads(vz.read("vectors_meta.json"))
            if int(vmeta.get("entries_count", -1)) != entries:
                print(f"  SKIP vectors for {pack_id}: they claim "
                      f"{vmeta.get('entries_count')} entries, the "
                      f"dictionary has {entries}", file=sys.stderr)
            else:
                item["vectors_size"] = vec.stat().st_size
        listing.append((item, pack, vec if "vectors_size" in item else None))

    total_entries = sum(i["entries_count"] for i, _, _ in listing)
    manifest = {
        "format": "slovary-bundle",
        "format_version": 2,
        "entries_count": total_entries,
        "dictionaries": [i for i, _, _ in listing],
    }

    with zipfile.ZipFile(out, "w", zipfile.ZIP_STORED, allowZip64=True) as z:
        # FIRST member, deliberately. An importer can then read the listing out
        # of the first few kilobytes of the file — one ranged request — and ask
        # the user which dictionaries they want before fetching hundreds of
        # megabytes. Written STORED like everything else, so those bytes are
        # the JSON itself with no decompression needed.
        z.writestr("bundle.json", json.dumps(manifest, indent=1))
        for item, pack, vec in listing:
            pack_id = item["pack_id"]
            # The name is the link between a dictionary and its vectors, so it
            # is rewritten to the pack_id rather than trusted as found.
            z.write(pack, f"{pack_id}.slovarypack")
            line = f"  {pack_id:34} {item['entries_count']:>8} entries"
            if vec is not None:
                z.write(vec, f"{pack_id}.vectors.zip")
                line += "  + vectors"
            print(line)

    size = out.stat().st_size
    print(f"\n{out}: {len(listing)} dictionaries, {total_entries} entries, "
          f"{size / 1048576:.0f} MB")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
