# Cache only derived read-only data. A DB replacement or WAL commit invalidates
# its entry; no SQLite connection or file handle remains open between requests.
_CACHE_LOCK = threading.RLock()
_CACHES: dict[str, OrderedDict] = {}
_CACHE_LIMITS = {"index": 4, "names": 16, "files": 16, "messages": 32}


def clear_browser_cache() -> None:
    with _CACHE_LOCK:
        _CACHES.clear()


def _database_revision(path: Path) -> tuple:
    values = [str(path.resolve()).casefold()]
    for candidate in (path, Path(str(path) + "-wal")):
        try:
            info = candidate.stat()
            values.append((info.st_size, info.st_mtime_ns, info.st_ctime_ns, info.st_ino))
        except OSError:
            values.append(None)
    return tuple(values)


def _cached(kind: str, key: Any, revision: tuple, build):
    # Building under the lock coalesces simultaneous navigation requests. The
    # GUI already loads in a worker; repeated searches cannot multiply scans.
    with _CACHE_LOCK:
        cache = _CACHES.setdefault(kind, OrderedDict())
        found = cache.get(key)
        if found is not None and found[0] == revision:
            cache.move_to_end(key)
            return found[1]
        value = build()
        cache[key] = (revision, value)
        cache.move_to_end(key)
        while len(cache) > _CACHE_LIMITS.get(kind, 16):
            cache.popitem(last=False)
        return value


def _message_index(message_path: Path) -> dict:
    revision = _database_revision(message_path)
    def build():
        connection = connect_sqlite(str(message_path))
        try:
            columns = _columns(connection, "message_table")
            result = {"columns": columns, "total_messages": 0, "conversations": {},
                      "ordered": [], "previews": {}, "revision": revision}
            if not {"conversation_id", "send_time"}.issubset(columns):
                return result
            groups: dict[str, list] = {}
            cursor = connection.execute("SELECT rowid,conversation_id,send_time FROM message_table")
            for row_id, conversation_id, timestamp in cursor:
                result["total_messages"] += 1
                conversation_id = str(conversation_id or "")
                if not conversation_id or conversation_id.startswith("Y:"):
                    continue
                try:
                    timestamp = float(timestamp or 0)
                    if not math.isfinite(timestamp):
                        timestamp = 0.0
                except (ValueError, TypeError, OverflowError):
                    timestamp = 0.0
                groups.setdefault(conversation_id, []).append((timestamp, row_id))
            for conversation_id, rows in groups.items():
                rows.sort()
                result["conversations"][conversation_id] = {
                    "rows": array("q", (row_id for _timestamp, row_id in rows)),
                    "lastTimestamp": rows[-1][0], "lastRowId": rows[-1][1],
                }
            result["ordered"] = sorted(result["conversations"], key=lambda key: (
                result["conversations"][key]["lastTimestamp"],
                result["conversations"][key]["lastRowId"], key), reverse=True)
            return result
        finally:
            connection.close()
    return _cached("index", revision[0], revision, build)


def _read_message_rows(message_path: Path, index: dict, row_ids: list[int]) -> dict[int, dict]:
    if not row_ids:
        return {}
    columns = index["columns"]
    wanted = [name for name in ("sender_id", "send_time", "content_type", "content", "server_id", "client_id") if name in columns]
    if not wanted:
        return {}
    connection = connect_sqlite(str(message_path))
    try:
        result = {}
        for offset in range(0, len(row_ids), 500):
            batch = row_ids[offset:offset + 500]
            marks = ",".join("?" for _ in batch)
            for row in connection.execute(f"SELECT rowid,{','.join(wanted)} FROM message_table WHERE rowid IN ({marks})", batch):
                result[int(row[0])] = dict(zip(wanted, row[1:]))
        return result
    finally:
        connection.close()


def _ensure_previews(message_path: Path, index: dict, conversation_ids: list[str]) -> None:
    with _CACHE_LOCK:
        missing = [key for key in conversation_ids if key not in index["previews"]]
        rows = _read_message_rows(message_path, index, [index["conversations"][key]["lastRowId"] for key in missing])
        for key in missing:
            item = rows.get(index["conversations"][key]["lastRowId"], {})
            index["previews"][key] = {
                "text": _content_label(item.get("content"), item.get("content_type")).replace("\n", " ")[:2000],
                "sender": str(item.get("sender_id") or ""),
            }


def _load_names(databases: dict[str, Path], account: str):
    revision = tuple((name, _database_revision(databases[name])) for name in ("user.db", "session.db") if name in databases)
    key = (account, tuple((name, str(path.resolve()).casefold()) for name, path in databases.items() if name in {"user.db", "session.db"}))
    return _cached("names", key, revision, lambda: _load_names_uncached(databases, account))


def _database_files(databases: dict[str, Path]):
    revision = tuple((name, _database_revision(path)) for name, path in sorted(databases.items()))
    key = tuple((name, str(path.resolve()).casefold()) for name, path in sorted(databases.items()))
    files, size = _cached("files", key, revision, lambda: _database_files_uncached(databases))
    return [dict(item) for item in files], size


