"""A byte-identical, faster `state_hash_lines` — prototype and proof.

Question (2026-09-12, session 41): `hesper doctor` reports a turn at 14.75 s
with two citizens, while H71 measured 2.60 ms an action. Where do the seconds
go?  `python -m cProfile` on a copy of the dev world answered: 12.3 s of a
12.4 s turn is `db.state_hash`, and inside it 262,200 calls of `json.dumps`
(one per map tile, 512 x 512) plus per-row `sqlite3.Row` name lookups.

The hash is *defined* as those bytes, and versions 1-4 are frozen, so a faster
path is only admissible if it produces the identical byte string for every
version on every world. This file has such a path and the proof.

Rules of `json.dumps(obj, sort_keys=True, separators=(",", ":"),
ensure_ascii=False)` for what SQLite can hand back (None, int, float, str;
bytes would raise in the original too):
  None  -> null
  int   -> int.__repr__            (bool never comes out of sqlite3)
  float -> float.__repr__, with nan/inf as NaN/Infinity/-Infinity
  str   -> json.encoder.encode_basestring (the C one, ensure_ascii=False)
Keys are sorted once per table, so the SELECT asks for the columns in that
order and each line is a join.

Run:  python research/hesper/turn-cost/fastlines.py <db>   (a copy, never live)
Prints, per hash version: old bytes == new bytes, and both timings.
"""
import json
import sys
import time
from json.encoder import encode_basestring as _enc_str, INFINITY as _INF

sys.path.insert(0, ".")
from hesper import db  # noqa: E402


def _enc(v):
    if v is None:
        return "null"
    if isinstance(v, str):
        return _enc_str(v)
    if isinstance(v, int):
        return int.__repr__(v)
    if isinstance(v, float):
        if v != v:
            return "NaN"
        if v == _INF:
            return "Infinity"
        if v == -_INF:
            return "-Infinity"
        return float.__repr__(v)
    raise TypeError(f"Object of type {type(v).__name__} is not JSON serializable")


def fast_state_hash_lines(conn, module_tables=(), version=db.HASH_VERSION):
    parts = []
    for table in db._hash_tables(conn, module_tables, version):
        order, columns = db._order_by(conn, table)
        drop = db._excluded_columns(table, version)
        keep = sorted(c for c in columns if c not in drop)
        if not keep:
            continue
        parts.append(table + "\n")
        # The exclusion rules read one column by name; find its index once.
        excl_col, excl_values = db.HASH_EXCLUDED_ROWS.get(table, (None, frozenset()))
        pre_col, prefixes = (None, ())
        if version >= 2:
            pre_col, prefixes = db.HASH_EXCLUDED_ROW_PREFIXES_V2.get(table, (None, ()))
        prefixes = tuple(prefixes)
        ei = keep.index(excl_col) if excl_col in keep else None
        pi = keep.index(pre_col) if pre_col in keep else None
        # An excluded column that is not in `keep` still decides exclusion in
        # the original (it reads the full Row); select it too, after `keep`.
        extra = []
        if excl_col is not None and ei is None:
            extra.append(excl_col); ei = len(keep) + len(extra) - 1
        if pre_col is not None and pi is None and pre_col not in extra:
            extra.append(pre_col); pi = len(keep) + len(extra) - 1
        elif pre_col is not None and pi is None:
            pi = len(keep) + extra.index(pre_col)
        select = ", ".join(f'"{c}"' for c in keep + extra)
        heads = ['"' + k + '":' for k in keep]   # keys are plain identifiers
        n = len(keep)
        cur = conn.cursor()
        cur.row_factory = None
        for row in cur.execute(f'SELECT {select} FROM "{table}" ORDER BY {order}'):
            if ei is not None and row[ei] in excl_values:
                continue
            if pi is not None and str(row[pi]).startswith(prefixes):
                continue
            parts.append("{" + ",".join([heads[i] + _enc(row[i]) for i in range(n)]) + "}\n")
    return "".join(parts).encode("utf-8")


def main(path):
    conn = db.connect(path) if hasattr(db, "connect") else None
    if conn is None:
        import sqlite3
        conn = sqlite3.connect(path); conn.row_factory = sqlite3.Row
    ok = True
    for version in range(1, db.HASH_VERSION + 1):
        t0 = time.perf_counter(); old = db.state_hash_lines(conn, version=version)
        t1 = time.perf_counter(); new = fast_state_hash_lines(conn, version=version)
        t2 = time.perf_counter()
        same = old == new
        ok &= same
        print(f"version {version}: identical={same} bytes={len(old)} "
              f"old={t1 - t0:.2f}s new={t2 - t1:.2f}s")
        if not same:
            for i, (a, b) in enumerate(zip(old.splitlines(), new.splitlines())):
                if a != b:
                    print("  first difference at line", i); print("  old:", a[:200]); print("  new:", b[:200]); break
    print("ALL IDENTICAL" if ok else "DIFFERENCE FOUND")
    return 0 if ok else 1


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