from __future__ import annotations

import hashlib
import json
import os
import pathlib
import threading
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Final

from crypto_utils import now_ts, canonical_json, b64e, b64d

if TYPE_CHECKING:
    from collections.abc import Iterator

_SQLCIPHER_AVAILABLE: bool = False
_sqlite_module: Any = None

try:
    import sqlcipher3 as _sqlite_module
    _SQLCIPHER_AVAILABLE = True
except ImportError:
    import sqlite3 as _sqlite_module
    _SQLCIPHER_AVAILABLE = False

def sqlcipher_available() -> bool:
    return _SQLCIPHER_AVAILABLE

_KDF_ITER: Final[int] = 500000

_PAGE_SIZE: Final[int] = 4096

_HMAC_ALGORITHM: Final[int] = 2

_KDF_ALGORITHM: Final[int] = 2

def _derive_raw_key(passphrase: str, salt: bytes) -> bytes:
    return hashlib.pbkdf2_hmac(
        "sha512",
        passphrase.encode("utf-8"),
        salt,
        iterations=_KDF_ITER,
        dklen=32,
    )

_ENC_MARKER_SUFFIX: Final[str] = ".enc"

def _marker_path(db_path: pathlib.Path) -> pathlib.Path:
    return db_path.with_suffix(db_path.suffix + _ENC_MARKER_SUFFIX)

def _write_marker(db_path: pathlib.Path, salt: bytes) -> None:
    _marker_path(db_path).write_bytes(salt)

def _read_marker(db_path: pathlib.Path) -> bytes | None:
    marker = _marker_path(db_path)
    if marker.exists():
        data = marker.read_bytes()
        if len(data) >= 16:
            return data[:16]
    return None

def _delete_marker(db_path: pathlib.Path) -> None:
    try:
        _marker_path(db_path).unlink(missing_ok=True)
    except OSError:
        pass

class DB:

    __slots__ = ("path", "_passphrase", "_salt", "_lock", "conn")

    def __init__(self, path: pathlib.Path, passphrase: str | None = None) -> None:
        self.path = pathlib.Path(path)
        self._passphrase: str | None = None
        self._salt: bytes | None = None
        self._lock = threading.RLock()
        self.conn: Any = None

        self._recover_wal_if_needed(passphrase)

        existing_salt = _read_marker(self.path)

        if existing_salt is not None:

            if not passphrase:
                raise ValueError("Database is encrypted but no passphrase provided")
            if not _SQLCIPHER_AVAILABLE:
                raise RuntimeError(
                    "Database is encrypted but SQLCipher not installed. "
                    "Install: pip install sqlcipher3-binary"
                )
            self._salt = existing_salt
            self._passphrase = passphrase
            self._open_encrypted()
        elif passphrase:

            if not _SQLCIPHER_AVAILABLE:
                raise RuntimeError(
                    "Cannot encrypt: SQLCipher not installed. "
                    "Install: pip install sqlcipher3-binary"
                )
            self._salt = os.urandom(16)
            self._passphrase = passphrase
            self._open_encrypted()
            _write_marker(self.path, self._salt)
        else:

            self._open_plain()

        self._init_schema()

    def _open_encrypted(self) -> None:
        if self._salt is None or self._passphrase is None:
            raise ValueError("Salt and passphrase required for encrypted DB")

        raw_key = _derive_raw_key(self._passphrase, self._salt)
        hex_key = raw_key.hex()

        self.conn = _sqlite_module.connect(
            str(self.path),
            check_same_thread=False,
            timeout=30.0,
        )

        self.conn.execute(f"PRAGMA key = \"x'{hex_key}'\"")

        self.conn.execute(f"PRAGMA cipher_page_size = {_PAGE_SIZE}")
        self.conn.execute(f"PRAGMA kdf_iter = {_KDF_ITER}")
        self.conn.execute(f"PRAGMA cipher_hmac_algorithm = HMAC_SHA512")
        self.conn.execute(f"PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512")
        self.conn.execute("PRAGMA cipher_memory_security = ON")

        self.conn.execute("PRAGMA cipher_plaintext_header_size = 0")

        try:
            self.conn.execute("SELECT count(*) FROM sqlite_master").fetchone()
        except Exception as e:
            self.conn.close()
            self.conn = None
            raise ValueError("Invalid passphrase - decryption failed") from e

        self._configure_connection()

    def _open_plain(self) -> None:
        self.conn = _sqlite_module.connect(
            str(self.path),
            check_same_thread=False,
            timeout=30.0,
        )
        self._configure_connection()

    def _configure_connection(self) -> None:

        if self._salt is not None:
            self.conn.execute("PRAGMA journal_mode = DELETE")
        else:
            self.conn.execute("PRAGMA journal_mode = WAL")
        self.conn.execute("PRAGMA synchronous = NORMAL")
        self.conn.execute("PRAGMA busy_timeout = 5000")
        self.conn.execute("PRAGMA foreign_keys = ON")

        try:
            self.conn.execute("PRAGMA temp_store = FILE")
        except Exception:
            pass
        try:
            self.conn.execute("PRAGMA cache_size = -2000")
        except Exception:
            pass

    def _configure_connection_for_export(self, conn: Any) -> None:
        try:
            conn.execute("PRAGMA journal_mode = OFF")
        except Exception:
            pass
        try:
            conn.execute("PRAGMA synchronous = OFF")
        except Exception:
            pass
        try:
            conn.execute("PRAGMA temp_store = FILE")
        except Exception:
            pass
        try:
            conn.execute("PRAGMA cache_size = -1000")
        except Exception:
            pass
        try:
            conn.execute("PRAGMA foreign_keys = OFF")
        except Exception:
            pass

    def close(self) -> None:
        with self._lock:
            if self.conn is not None:
                try:
                    self.conn.commit()
                except Exception:
                    pass

                try:
                    self.conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
                except Exception:
                    pass
                try:
                    self.conn.close()
                except Exception:
                    pass
                self.conn = None

                self._cleanup_wal_files()

    def _cleanup_wal_files(self) -> None:
        try:
            wal_path = pathlib.Path(str(self.path) + "-wal")
            if wal_path.exists():
                wal_path.unlink()
        except Exception:
            pass
        try:
            shm_path = pathlib.Path(str(self.path) + "-shm")
            if shm_path.exists():
                shm_path.unlink()
        except Exception:
            pass

    def _recover_wal_if_needed(self, passphrase: str | None) -> None:
        wal_path = pathlib.Path(str(self.path) + "-wal")
        if not wal_path.exists():
            return

        if not self.path.exists():

            self._cleanup_wal_files()
            return

        print(f"[DB] Recovering WAL files for {self.path.name}...")

        try:
            existing_salt = _read_marker(self.path)
            recovery_conn = None

            if existing_salt is not None:

                if not passphrase:
                    print(f"[DB] WAL recovery skipped: encrypted DB needs passphrase")
                    return
                if not _SQLCIPHER_AVAILABLE:
                    print(f"[DB] WAL recovery skipped: SQLCipher not available")
                    return

                raw_key = _derive_raw_key(passphrase, existing_salt)
                hex_key = raw_key.hex()

                recovery_conn = _sqlite_module.connect(
                    str(self.path),
                    check_same_thread=False,
                    timeout=30.0,
                )
                recovery_conn.execute(f"PRAGMA key = \"x'{hex_key}'\"")
                recovery_conn.execute(f"PRAGMA cipher_page_size = {_PAGE_SIZE}")
                recovery_conn.execute(f"PRAGMA kdf_iter = {_KDF_ITER}")
                recovery_conn.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA512")
                recovery_conn.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512")
            else:

                recovery_conn = _sqlite_module.connect(
                    str(self.path),
                    check_same_thread=False,
                    timeout=30.0,
                )

            recovery_conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
            recovery_conn.commit()
            recovery_conn.close()

            self._cleanup_wal_files()
            print(f"[DB] WAL recovery complete for {self.path.name}")

        except Exception as e:
            print(f"[DB] WAL recovery failed: {e}")

    def __enter__(self) -> DB:
        return self

    def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
        self.close()

    def __del__(self) -> None:
        try:
            self.close()
        except Exception:
            pass

    def is_encrypted(self) -> bool:
        return self._salt is not None and self._passphrase is not None

    def is_unlocked(self) -> bool:
        return self.is_encrypted() and self.conn is not None

    @staticmethod
    def has_passphrase_file(path: pathlib.Path) -> bool:
        return _read_marker(pathlib.Path(path)) is not None

    def has_passphrase(self) -> bool:
        return self._salt is not None

    def set_passphrase(self, new_passphrase: str) -> None:
        if not new_passphrase:
            raise ValueError("Passphrase cannot be empty")
        if not _SQLCIPHER_AVAILABLE:
            raise RuntimeError(
                "Cannot encrypt: SQLCipher not installed. "
                "Install: pip install sqlcipher3-binary"
            )

        with self._lock:
            if self._salt is not None:

                new_salt = os.urandom(16)
                new_raw_key = _derive_raw_key(new_passphrase, new_salt)
                new_hex_key = new_raw_key.hex()

                self.conn.execute(f"PRAGMA rekey = \"x'{new_hex_key}'\"")

                self._salt = new_salt
                self._passphrase = new_passphrase
                _write_marker(self.path, self._salt)
            else:

                self._encrypt_existing_db(new_passphrase)

    def _encrypt_existing_db(self, passphrase: str) -> None:
        new_salt = os.urandom(16)
        raw_key = _derive_raw_key(passphrase, new_salt)
        hex_key = raw_key.hex()

        temp_path = self.path.with_suffix(".tmp_encrypted")

        try:

            if self.conn is not None:
                self.conn.commit()
                self.conn.close()
                self.conn = None

            src_conn = _sqlite_module.connect(str(self.path), check_same_thread=False)
            try:
                self._configure_connection_for_export(src_conn)

                src_conn.execute(
                    f"ATTACH DATABASE '{temp_path}' AS encrypted KEY \"x'{hex_key}'\""
                )

                try:
                    src_conn.execute(f"PRAGMA encrypted.cipher_page_size = {_PAGE_SIZE}")
                except Exception:
                    pass
                try:
                    src_conn.execute(f"PRAGMA encrypted.kdf_iter = {_KDF_ITER}")
                except Exception:
                    pass
                try:
                    src_conn.execute("PRAGMA encrypted.cipher_hmac_algorithm = HMAC_SHA512")
                except Exception:
                    pass
                try:
                    src_conn.execute("PRAGMA encrypted.cipher_kdf_algorithm = PBKDF2_HMAC_SHA512")
                except Exception:
                    pass
                try:
                    src_conn.execute("PRAGMA encrypted.cipher_plaintext_header_size = 0")
                except Exception:
                    pass
                src_conn.execute("SELECT sqlcipher_export('encrypted')")
                src_conn.execute("DETACH DATABASE encrypted")
                src_conn.commit()
            finally:
                try:
                    src_conn.close()
                except Exception:
                    pass

            self.path.unlink()
            temp_path.rename(self.path)

            self._salt = new_salt
            self._passphrase = passphrase
            _write_marker(self.path, self._salt)

            self._open_encrypted()

        except Exception:
            try:
                temp_path.unlink(missing_ok=True)
            except OSError:
                pass
            self._open_plain()
            raise

    def remove_passphrase(self) -> None:
        if self._salt is None:
            return

        with self._lock:
            temp_path = self.path.with_suffix(".tmp_decrypted")

            try:

                raw_key = _derive_raw_key(self._passphrase, self._salt)
                hex_key = raw_key.hex()

                src = None
                try:
                    src = _sqlite_module.connect(str(self.path), check_same_thread=False)
                    self._configure_connection_for_export(src)
                    src.execute(f"PRAGMA key = \"x'{hex_key}'\"")
                    try:
                        src.execute(f"PRAGMA kdf_iter = {_KDF_ITER}")
                    except Exception:
                        pass

                    try:
                        src.execute(f"ATTACH DATABASE '{temp_path}' AS plaintext KEY ''")
                    except Exception:

                        src.execute(f"ATTACH DATABASE '{temp_path}' AS plaintext")

                    src.execute("SELECT sqlcipher_export('plaintext')")
                    src.execute("DETACH DATABASE plaintext")
                    src.commit()
                finally:
                    try:
                        if src is not None:
                            src.close()
                    except Exception:
                        pass

                if self.conn is not None:
                    try:
                        self.conn.close()
                    except Exception:
                        pass
                    self.conn = None

                self.path.unlink()
                temp_path.rename(self.path)
                _delete_marker(self.path)

                self._salt = None
                self._passphrase = None

                self._open_plain()

            except Exception:
                try:
                    temp_path.unlink(missing_ok=True)
                except OSError:
                    pass
                raise

    def export_backup(self, dst_path: pathlib.Path, passphrase: str | None = None) -> None:
        dst_path = pathlib.Path(dst_path)

        with self._lock:
            if self.conn is None:
                raise RuntimeError("Database is not open")

            dst_encrypt = (self._salt is not None)
            dst_passphrase: str | None = None
            if dst_encrypt:
                dst_passphrase = self._passphrase
                if not dst_passphrase:
                    raise ValueError("Database is encrypted but no passphrase is loaded")
            else:
                dst_passphrase = passphrase
                dst_encrypt = bool(dst_passphrase)

            if dst_encrypt and not _SQLCIPHER_AVAILABLE:
                raise RuntimeError(
                    "Cannot create encrypted backup: SQLCipher not installed. "
                    "Install: pip install sqlcipher3-binary"
                )

            dst_conn = None
            dst_salt: bytes | None = None
            try:
                dst_conn = _sqlite_module.connect(
                    str(dst_path),
                    check_same_thread=False,
                    timeout=30.0,
                )
                self._configure_connection_for_export(dst_conn)

                if dst_encrypt:
                    dst_salt = os.urandom(16)
                    raw_key = _derive_raw_key(dst_passphrase or "", dst_salt)
                    hex_key = raw_key.hex()
                    dst_conn.execute(f"PRAGMA key = \"x'{hex_key}'\"")

                    try:
                        dst_conn.execute(f"PRAGMA cipher_page_size = {_PAGE_SIZE}")
                    except Exception:
                        pass
                    try:
                        dst_conn.execute(f"PRAGMA kdf_iter = {_KDF_ITER}")
                    except Exception:
                        pass
                    try:
                        dst_conn.execute("PRAGMA cipher_hmac_algorithm = HMAC_SHA512")
                    except Exception:
                        pass
                    try:
                        dst_conn.execute("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512")
                    except Exception:
                        pass
                    try:
                        dst_conn.execute("PRAGMA cipher_memory_security = ON")
                    except Exception:
                        pass
                    try:
                        dst_conn.execute("PRAGMA cipher_plaintext_header_size = 0")
                    except Exception:
                        pass

                try:
                    self.conn.backup(dst_conn)
                except TypeError:

                    self.conn.backup(dst_conn, pages=0)
                dst_conn.commit()
            except Exception:

                try:
                    if dst_conn is not None:
                        dst_conn.close()
                except Exception:
                    pass
                try:
                    dst_path.unlink(missing_ok=True)
                except OSError:
                    pass
                try:
                    _delete_marker(dst_path)
                except Exception:
                    pass
                raise
            finally:
                try:
                    if dst_conn is not None:
                        dst_conn.close()
                except Exception:
                    pass

            if dst_encrypt and dst_salt is not None:
                _write_marker(dst_path, dst_salt)
            else:
                _delete_marker(dst_path)

    @contextmanager
    def _cursor(self) -> Iterator[Any]:
        with self._lock:
            cur = self.conn.cursor()
            try:
                yield cur
            finally:
                pass

    def _commit(self) -> None:
        with self._lock:
            self.conn.commit()

    def _init_schema(self) -> None:
        with self._cursor() as cur:
            cur.executescript(
                """
                CREATE TABLE IF NOT EXISTS identity (
                    alias TEXT PRIMARY KEY,
                    sign_sk_b64 TEXT NOT NULL,
                    dh_sk_b64 TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );

                CREATE TABLE IF NOT EXISTS peers (
                    alias TEXT PRIMARY KEY,
                    fp TEXT NOT NULL,
                    sign_pub_b64 TEXT NOT NULL,
                    dh_pub_b64 TEXT NOT NULL,
                    pinned_fp TEXT,
                    updated_at INTEGER NOT NULL
                );

                CREATE TABLE IF NOT EXISTS chats (
                    peer_alias TEXT PRIMARY KEY,
                    created_at INTEGER NOT NULL,
                    last_ts INTEGER NOT NULL DEFAULT 0,
                    unread INTEGER NOT NULL DEFAULT 0
                );

                CREATE TABLE IF NOT EXISTS sessions (
                    peer_alias TEXT PRIMARY KEY,
                    peer_fp TEXT NOT NULL,
                    send_ck_b64 TEXT NOT NULL,
                    recv_ck_b64 TEXT NOT NULL,
                    send_n INTEGER NOT NULL,
                    recv_n INTEGER NOT NULL,
                    established_at INTEGER NOT NULL
                );

                CREATE TABLE IF NOT EXISTS requests_in (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    peer_alias TEXT NOT NULL,
                    peer_fp TEXT NOT NULL,
                    payload_json TEXT NOT NULL,
                    received_at INTEGER NOT NULL
                );

                CREATE TABLE IF NOT EXISTS requests_out (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    peer_alias TEXT NOT NULL,
                    peer_fp TEXT,
                    state TEXT NOT NULL,
                    payload_json TEXT NOT NULL,
                    created_at INTEGER NOT NULL,
                    updated_at INTEGER NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_reqout_peer ON requests_out(peer_alias, id);

                CREATE TABLE IF NOT EXISTS messages (
                    msg_id TEXT PRIMARY KEY,
                    peer_alias TEXT NOT NULL,
                    sender_alias TEXT NOT NULL,
                    ts INTEGER NOT NULL,
                    body TEXT NOT NULL,
                    reply_to TEXT,
                    edited_ts INTEGER,
                    local_state TEXT NOT NULL DEFAULT 'ok',
                    kind TEXT NOT NULL DEFAULT 'text',
                    meta_json TEXT
                );
                CREATE INDEX IF NOT EXISTS idx_messages_peer_ts ON messages(peer_alias, ts);
                CREATE INDEX IF NOT EXISTS idx_messages_peer_ts_msgid ON messages(peer_alias, ts DESC, msg_id DESC);

                CREATE TABLE IF NOT EXISTS outbox (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    peer_alias TEXT NOT NULL,
                    payload_json TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_outbox_peer ON outbox(peer_alias, id);

                CREATE TABLE IF NOT EXISTS settings (
                    key TEXT PRIMARY KEY,
                    value TEXT NOT NULL
                );

                CREATE TABLE IF NOT EXISTS groups (
                    group_id TEXT PRIMARY KEY,
                    name TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );

                CREATE TABLE IF NOT EXISTS group_members (
                    group_id TEXT NOT NULL,
                    member_alias TEXT NOT NULL,
                    added_at INTEGER NOT NULL,
                    PRIMARY KEY(group_id, member_alias)
                );
                CREATE INDEX IF NOT EXISTS idx_group_members_gid ON group_members(group_id);

                CREATE TABLE IF NOT EXISTS files (
                    file_id TEXT PRIMARY KEY,
                    name TEXT,
                    bytes_total INTEGER NOT NULL,
                    record_count INTEGER NOT NULL,
                    record_hashes_json TEXT NOT NULL,
                    local_path TEXT NOT NULL,
                    added_at INTEGER NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_files_added ON files(added_at DESC);

                CREATE TABLE IF NOT EXISTS file_offers (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    peer_alias TEXT NOT NULL,
                    sender_alias TEXT NOT NULL,
                    file_id TEXT NOT NULL,
                    name TEXT,
                    meta_json TEXT NOT NULL,
                    received_at INTEGER NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_file_offers_peer ON file_offers(peer_alias, received_at DESC);

                CREATE TABLE IF NOT EXISTS cloud_file_map (
                    file_id TEXT PRIMARY KEY,
                    blob_sha256 TEXT NOT NULL,
                    created_at INTEGER NOT NULL
                );

                CREATE TABLE IF NOT EXISTS plugin_settings (
                    plugin_id TEXT NOT NULL,
                    key TEXT NOT NULL,
                    value TEXT NOT NULL,
                    PRIMARY KEY (plugin_id, key)
                );
                CREATE INDEX IF NOT EXISTS idx_plugin_settings_plugin ON plugin_settings(plugin_id);

                -- Plugin data storage: isolated per-plugin tables within main DB
                -- Each plugin's data is stored with plugin_id prefix for full isolation
                CREATE TABLE IF NOT EXISTS plugin_data (
                    plugin_id TEXT NOT NULL,
                    table_name TEXT NOT NULL,
                    row_id TEXT NOT NULL,
                    data_json TEXT NOT NULL,
                    created_at REAL DEFAULT (strftime('%s', 'now')),
                    updated_at REAL DEFAULT (strftime('%s', 'now')),
                    PRIMARY KEY (plugin_id, table_name, row_id)
                );
                CREATE INDEX IF NOT EXISTS idx_plugin_data_plugin ON plugin_data(plugin_id);
                CREATE INDEX IF NOT EXISTS idx_plugin_data_table ON plugin_data(plugin_id, table_name);
                """
            )
        self._commit()
        self._ensure_messages_columns()

    def _ensure_messages_columns(self) -> None:
        with self._cursor() as cur:
            try:
                cur.execute("PRAGMA table_info(messages)")
                cols = {r[1] for r in cur.fetchall()}
            except Exception:
                return

            if "kind" not in cols:
                try:
                    cur.execute("ALTER TABLE messages ADD COLUMN kind TEXT NOT NULL DEFAULT 'text'")
                except Exception:
                    pass
            if "meta_json" not in cols:
                try:
                    cur.execute("ALTER TABLE messages ADD COLUMN meta_json TEXT")
                except Exception:
                    pass
        try:
            self._commit()
        except Exception:
            pass

    def load_identity(self, alias: str) -> tuple[str, str, str] | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT alias, sign_sk_b64, dh_sk_b64 FROM identity WHERE alias=?",
                (alias,),
            )
            row = cur.fetchone()
        return tuple(row) if row else None

    def save_identity(self, alias: str, sign_sk_b64: str, dh_sk_b64: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT OR REPLACE INTO identity(alias, sign_sk_b64, dh_sk_b64, created_at) VALUES(?,?,?,?)",
                (alias, sign_sk_b64, dh_sk_b64, now_ts()),
            )
        self._commit()

    def upsert_peer(self, alias: str, fp: str, sign_pub_b64: str, dh_pub_b64: str) -> None:
        with self._cursor() as cur:
            cur.execute("SELECT pinned_fp FROM peers WHERE alias=?", (alias,))
            row = cur.fetchone()
            pinned = row[0] if row else None
            cur.execute(
                "INSERT OR REPLACE INTO peers(alias, fp, sign_pub_b64, dh_pub_b64, pinned_fp, updated_at) VALUES(?,?,?,?,?,?)",
                (alias, fp, sign_pub_b64, dh_pub_b64, pinned, now_ts()),
            )
        self._commit()

    def get_peer(self, alias: str) -> dict | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT alias, fp, sign_pub_b64, dh_pub_b64, pinned_fp FROM peers WHERE alias=?",
                (alias,),
            )
            row = cur.fetchone()
        if not row:
            return None
        return {
            "alias": row[0],
            "fp": row[1],
            "sign_pub_b64": row[2],
            "dh_pub_b64": row[3],
            "pinned_fp": row[4],
        }

    def pin_peer(self, alias: str, fp: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "UPDATE peers SET pinned_fp=?, updated_at=? WHERE alias=?",
                (fp, now_ts(), alias),
            )
        self._commit()

    def list_peers(self) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT alias, fp, sign_pub_b64, dh_pub_b64, pinned_fp FROM peers ORDER BY alias ASC"
            )
            rows = cur.fetchall()
        return [
            {
                "alias": r[0],
                "fp": r[1],
                "sign_pub_b64": r[2],
                "dh_pub_b64": r[3],
                "pinned_fp": r[4],
            }
            for r in rows
        ]

    def ensure_chat(self, peer_alias: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT OR IGNORE INTO chats(peer_alias, created_at, last_ts, unread) VALUES(?,?,?,?)",
                (peer_alias, now_ts(), 0, 0),
            )
        self._commit()

    def list_chats(self) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT peer_alias, created_at, last_ts, unread FROM chats ORDER BY last_ts DESC, created_at DESC"
            )
            rows = cur.fetchall()
        return [
            {"peer_alias": r[0], "created_at": r[1], "last_ts": r[2], "unread": r[3]}
            for r in rows
        ]

    def set_chat_unread(self, peer_alias: str, unread: int) -> None:
        with self._cursor() as cur:
            cur.execute("UPDATE chats SET unread=? WHERE peer_alias=?", (unread, peer_alias))
        self._commit()

    def inc_chat_unread(self, peer_alias: str, inc: int = 1) -> None:
        with self._cursor() as cur:
            cur.execute("UPDATE chats SET unread=unread+? WHERE peer_alias=?", (inc, peer_alias))
        self._commit()

    def set_chat_last_ts(self, peer_alias: str, ts: int) -> None:
        with self._cursor() as cur:
            cur.execute("UPDATE chats SET last_ts=? WHERE peer_alias=?", (ts, peer_alias))
        self._commit()

    def delete_chat(self, peer_alias: str) -> None:
        with self._cursor() as cur:
            cur.execute("DELETE FROM messages WHERE peer_alias=?", (peer_alias,))
            cur.execute("DELETE FROM chats WHERE peer_alias=?", (peer_alias,))
            cur.execute("DELETE FROM sessions WHERE peer_alias=?", (peer_alias,))
            cur.execute("DELETE FROM outbox WHERE peer_alias=?", (peer_alias,))
            cur.execute("DELETE FROM requests_out WHERE peer_alias=?", (peer_alias,))
        self._commit()

    def save_session(
        self,
        peer_alias: str,
        peer_fp: str,
        send_ck: bytes,
        recv_ck: bytes,
        send_n: int,
        recv_n: int,
    ) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT OR REPLACE INTO sessions(peer_alias, peer_fp, send_ck_b64, recv_ck_b64, send_n, recv_n, established_at) VALUES(?,?,?,?,?,?,?)",
                (peer_alias, peer_fp, b64e(send_ck), b64e(recv_ck), send_n, recv_n, now_ts()),
            )
        self._commit()

    def load_session(self, peer_alias: str) -> dict | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT peer_alias, peer_fp, send_ck_b64, recv_ck_b64, send_n, recv_n FROM sessions WHERE peer_alias=?",
                (peer_alias,),
            )
            row = cur.fetchone()
        if not row:
            return None
        return {
            "peer_alias": row[0],
            "peer_fp": row[1],
            "send_ck": b64d(row[2]),
            "recv_ck": b64d(row[3]),
            "send_n": row[4],
            "recv_n": row[5],
        }

    def add_request_in(self, peer_alias: str, peer_fp: str, payload: dict) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT INTO requests_in(peer_alias, peer_fp, payload_json, received_at) VALUES(?,?,?,?)",
                (peer_alias, peer_fp, canonical_json(payload), now_ts()),
            )
        self._commit()

    def list_requests_in(self) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT id, peer_alias, peer_fp, payload_json, received_at FROM requests_in ORDER BY id DESC"
            )
            rows = cur.fetchall()
        out = []
        for r in rows:
            out.append({
                "id": r[0],
                "peer_alias": r[1],
                "peer_fp": r[2],
                "payload": json.loads(r[3]),
                "received_at": r[4],
            })
        return out

    def delete_request_in(self, req_id: int) -> None:
        with self._cursor() as cur:
            cur.execute("DELETE FROM requests_in WHERE id=?", (req_id,))
        self._commit()

    def count_requests_in(self) -> int:
        with self._cursor() as cur:
            cur.execute("SELECT COUNT(*) FROM requests_in")
            return int(cur.fetchone()[0])

    def add_request_out(
        self,
        peer_alias: str,
        peer_fp: str | None,
        payload: dict,
        state: str = "pending",
    ) -> int:
        with self._cursor() as cur:
            cur.execute(
                "INSERT INTO requests_out(peer_alias, peer_fp, state, payload_json, created_at, updated_at) VALUES(?,?,?,?,?,?)",
                (peer_alias, peer_fp, state, canonical_json(payload), now_ts(), now_ts()),
            )
            lastrowid = cur.lastrowid
        self._commit()
        return int(lastrowid)

    def set_request_out_state_by_peer(self, peer_alias: str, state: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "UPDATE requests_out SET state=?, updated_at=? WHERE peer_alias=? AND state='pending'",
                (state, now_ts(), peer_alias),
            )
        self._commit()

    def set_request_out_state(self, req_id: int, state: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "UPDATE requests_out SET state=?, updated_at=? WHERE id=?",
                (state, now_ts(), req_id),
            )
        self._commit()

    def list_requests_out(self) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT id, peer_alias, peer_fp, state, payload_json, created_at, updated_at FROM requests_out ORDER BY id DESC"
            )
            rows = cur.fetchall()
        out = []
        for r in rows:
            out.append({
                "id": r[0],
                "peer_alias": r[1],
                "peer_fp": r[2],
                "state": r[3],
                "payload": json.loads(r[4]),
                "created_at": r[5],
                "updated_at": r[6],
            })
        return out

    def find_pending_request_out(self, peer_alias: str) -> dict | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT id, peer_alias, peer_fp, state, payload_json, created_at, updated_at "
                "FROM requests_out WHERE peer_alias=? AND state='pending' ORDER BY id DESC LIMIT 1",
                (peer_alias,),
            )
            r = cur.fetchone()
        if not r:
            return None
        try:
            payload = json.loads(r[4])
        except Exception:
            payload = {}
        return {
            "id": r[0],
            "peer_alias": r[1],
            "peer_fp": r[2],
            "state": r[3],
            "payload": payload,
            "created_at": r[5],
            "updated_at": r[6],
        }

    def delete_request_out(self, req_id: int) -> None:
        with self._cursor() as cur:
            cur.execute("DELETE FROM requests_out WHERE id=?", (req_id,))
        self._commit()

    def count_requests_out(self) -> int:
        with self._cursor() as cur:
            cur.execute("SELECT COUNT(*) FROM requests_out")
            return int(cur.fetchone()[0])

    def _row_to_message(self, r: tuple) -> dict:
        return {
            "msg_id": r[0],
            "peer_alias": r[1],
            "sender_alias": r[2],
            "ts": r[3],
            "body": r[4],
            "reply_to": r[5],
            "edited_ts": r[6],
            "local_state": r[7],
            "kind": r[8] or "text",
            "meta": json.loads(r[9]) if r[9] else None,
        }

    def add_message(
        self,
        msg_id: str,
        peer_alias: str,
        sender_alias: str,
        ts: int,
        body: str,
        reply_to: str | None,
        edited_ts: int | None,
        local_state: str,
        kind: str = "text",
        meta: dict | None = None,
    ) -> None:
        meta_json = None
        if meta is not None:
            try:
                meta_json = canonical_json(meta)
            except Exception:
                pass

        with self._cursor() as cur:
            cur.execute(
                "INSERT OR REPLACE INTO messages(msg_id, peer_alias, sender_alias, ts, body, reply_to, edited_ts, local_state, kind, meta_json) "
                "VALUES(?,?,?,?,?,?,?,?,?,?)",
                (msg_id, peer_alias, sender_alias, ts, body, reply_to, edited_ts, local_state, kind or "text", meta_json),
            )
        self._commit()
        self.set_chat_last_ts(peer_alias, ts)

    def update_message_body(self, msg_id: str, new_body: str, edited_ts: int) -> None:
        with self._cursor() as cur:
            cur.execute(
                "UPDATE messages SET body=?, edited_ts=? WHERE msg_id=?",
                (new_body, edited_ts, msg_id),
            )
        self._commit()

    def update_message_meta(self, msg_id: str, meta_updates: dict) -> None:
        with self._cursor() as cur:
            cur.execute("SELECT meta_json FROM messages WHERE msg_id=?", (msg_id,))
            row = cur.fetchone()
            if row:
                existing_meta = json.loads(row[0]) if row[0] else {}
                existing_meta.update(meta_updates)
                cur.execute(
                    "UPDATE messages SET meta_json=? WHERE msg_id=?",
                    (json.dumps(existing_meta), msg_id),
                )
        self._commit()

    def delete_message(self, msg_id: str) -> None:
        with self._cursor() as cur:
            cur.execute("DELETE FROM messages WHERE msg_id=?", (msg_id,))
        self._commit()

    def get_message(self, msg_id: str) -> dict | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT msg_id, peer_alias, sender_alias, ts, body, reply_to, edited_ts, local_state, kind, meta_json "
                "FROM messages WHERE msg_id=?",
                (msg_id,),
            )
            r = cur.fetchone()
        if not r:
            return None
        return self._row_to_message(r)

    def list_messages(self, peer_alias: str, limit: int = 1000) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT msg_id, peer_alias, sender_alias, ts, body, reply_to, edited_ts, local_state, kind, meta_json "
                "FROM messages WHERE peer_alias=? ORDER BY ts ASC LIMIT ?",
                (peer_alias, limit),
            )
            rows = cur.fetchall()
        return [self._row_to_message(r) for r in rows]

    def list_messages_tail(self, peer_alias: str, limit: int = 200) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT msg_id, peer_alias, sender_alias, ts, body, reply_to, edited_ts, local_state, kind, meta_json "
                "FROM messages WHERE peer_alias=? ORDER BY ts DESC, msg_id DESC LIMIT ?",
                (peer_alias, int(limit)),
            )
            rows = cur.fetchall()
        out = [self._row_to_message(r) for r in rows]
        out.reverse()
        return out

    def list_messages_before(
        self,
        peer_alias: str,
        before_ts: int,
        before_msg_id: str,
        limit: int = 200,
    ) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT msg_id, peer_alias, sender_alias, ts, body, reply_to, edited_ts, local_state, kind, meta_json "
                "FROM messages WHERE peer_alias=? AND (ts < ? OR (ts = ? AND msg_id < ?)) "
                "ORDER BY ts DESC, msg_id DESC LIMIT ?",
                (peer_alias, int(before_ts), int(before_ts), str(before_msg_id), int(limit)),
            )
            rows = cur.fetchall()
        out = [self._row_to_message(r) for r in rows]
        out.reverse()
        return out

    def has_messages_before(self, peer_alias: str, before_ts: int, before_msg_id: str) -> bool:
        with self._cursor() as cur:
            cur.execute(
                "SELECT 1 FROM messages WHERE peer_alias=? AND (ts < ? OR (ts = ? AND msg_id < ?)) LIMIT 1",
                (peer_alias, int(before_ts), int(before_ts), str(before_msg_id)),
            )
            return cur.fetchone() is not None

    def list_messages_from(
        self,
        peer_alias: str,
        from_ts: int,
        from_msg_id: str,
        limit: int | None = None,
    ) -> list[dict]:
        with self._cursor() as cur:
            base = (
                "SELECT msg_id, peer_alias, sender_alias, ts, body, reply_to, edited_ts, local_state, kind, meta_json "
                "FROM messages WHERE peer_alias=? AND (ts > ? OR (ts = ? AND msg_id >= ?)) ORDER BY ts ASC, msg_id ASC"
            )
            params: list[Any] = [peer_alias, int(from_ts), int(from_ts), str(from_msg_id)]
            if isinstance(limit, int) and limit > 0:
                base += " LIMIT ?"
                params.append(int(limit))
            cur.execute(base, tuple(params))
            rows = cur.fetchall()
        return [self._row_to_message(r) for r in rows]

    def get_latest_file_id(self, peer_alias: str) -> str | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT meta_json FROM messages WHERE peer_alias=? AND kind='file' AND meta_json IS NOT NULL ORDER BY ts DESC LIMIT 1",
                (peer_alias,),
            )
            r = cur.fetchone()
        if not r or not r[0]:
            return None
        try:
            meta = json.loads(r[0])
        except Exception:
            return None
        if not isinstance(meta, dict):
            return None
        fid = meta.get("file_id")
        if isinstance(fid, str) and fid:
            return fid
        return None

    def mark_message_ok(self, msg_id: str) -> None:
        with self._cursor() as cur:
            cur.execute("UPDATE messages SET local_state='ok' WHERE msg_id=?", (msg_id,))
        self._commit()

    def add_outbox(self, peer_alias: str, payload: dict) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT INTO outbox(peer_alias, payload_json, created_at) VALUES(?,?,?)",
                (peer_alias, canonical_json(payload), now_ts()),
            )
        self._commit()

    def pop_outbox(self, peer_alias: str, limit: int = 200) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT id, payload_json FROM outbox WHERE peer_alias=? ORDER BY id ASC LIMIT ?",
                (peer_alias, limit),
            )
            rows = cur.fetchall()
            ids = [r[0] for r in rows]
            payloads = [json.loads(r[1]) for r in rows]
            if ids:
                placeholders = ",".join(["?"] * len(ids))
                cur.execute(f"DELETE FROM outbox WHERE id IN ({placeholders})", ids)
        self._commit()
        return payloads

    def get_setting(self, key: str, decrypt: bool = True) -> str | None:
        with self._cursor() as cur:
            cur.execute("SELECT value FROM settings WHERE key=?", (key,))
            row = cur.fetchone()
        return row[0] if row else None

    def set_setting(self, key: str, value: str, encrypt: bool = True) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
                (key, value),
            )
        self._commit()

    def get_plugin_setting(self, plugin_id: str, key: str) -> str | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT value FROM plugin_settings WHERE plugin_id=? AND key=?",
                (plugin_id, key),
            )
            row = cur.fetchone()
        return row[0] if row else None

    def set_plugin_setting(self, plugin_id: str, key: str, value: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT INTO plugin_settings(plugin_id, key, value) VALUES(?,?,?) "
                "ON CONFLICT(plugin_id, key) DO UPDATE SET value=excluded.value",
                (plugin_id, key, value),
            )
        self._commit()

    def get_all_plugin_settings(self, plugin_id: str) -> dict[str, str]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT key, value FROM plugin_settings WHERE plugin_id=?",
                (plugin_id,),
            )
            return {row[0]: row[1] for row in cur.fetchall()}

    def delete_plugin_setting(self, plugin_id: str, key: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "DELETE FROM plugin_settings WHERE plugin_id=? AND key=?",
                (plugin_id, key),
            )
        self._commit()

    def delete_all_plugin_settings(self, plugin_id: str) -> None:
        with self._cursor() as cur:
            cur.execute("DELETE FROM plugin_settings WHERE plugin_id=?", (plugin_id,))
        self._commit()

    def plugin_data_insert(self, plugin_id: str, table_name: str, row_id: str, data: dict) -> None:
        import time
        data_json = json.dumps(data, ensure_ascii=False)
        with self._cursor() as cur:
            cur.execute(
                """INSERT INTO plugin_data (plugin_id, table_name, row_id, data_json, updated_at)
                   VALUES (?, ?, ?, ?, ?)
                   ON CONFLICT(plugin_id, table_name, row_id)
                   DO UPDATE SET data_json=excluded.data_json, updated_at=excluded.updated_at""",
                (plugin_id, table_name, row_id, data_json, time.time()),
            )
        self._commit()

    def plugin_data_select(
        self, plugin_id: str, table_name: str,
        where_field: str | None = None, where_value: Any = None,
        order_by: str | None = None, desc: bool = False, limit: int = 0
    ) -> list[dict]:
        sql = "SELECT data_json FROM plugin_data WHERE plugin_id=? AND table_name=?"
        params: list = [plugin_id, table_name]

        if where_field and where_value is not None:
            sql += f" AND JSON_EXTRACT(data_json, '$.{where_field}') = ?"
            params.append(where_value)

        if order_by:
            direction = "DESC" if desc else "ASC"
            sql += f" ORDER BY JSON_EXTRACT(data_json, '$.{order_by}') {direction}"

        if limit > 0:
            sql += f" LIMIT {limit}"

        with self._cursor() as cur:
            cur.execute(sql, tuple(params))
            rows = cur.fetchall()

        result = []
        for row in rows:
            try:
                result.append(json.loads(row[0]))
            except Exception:
                pass
        return result

    def plugin_data_get(self, plugin_id: str, table_name: str, row_id: str) -> dict | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT data_json FROM plugin_data WHERE plugin_id=? AND table_name=? AND row_id=?",
                (plugin_id, table_name, row_id),
            )
            row = cur.fetchone()
        if row:
            try:
                return json.loads(row[0])
            except Exception:
                pass
        return None

    def plugin_data_delete(self, plugin_id: str, table_name: str, row_id: str) -> int:
        with self._cursor() as cur:
            cur.execute(
                "DELETE FROM plugin_data WHERE plugin_id=? AND table_name=? AND row_id=?",
                (plugin_id, table_name, row_id),
            )
            count = cur.rowcount
        self._commit()
        return count

    def plugin_data_delete_where(
        self, plugin_id: str, table_name: str,
        where_field: str, where_value: Any
    ) -> int:
        sql = f"""DELETE FROM plugin_data
                  WHERE plugin_id=? AND table_name=?
                  AND JSON_EXTRACT(data_json, '$.{where_field}') = ?"""
        with self._cursor() as cur:
            cur.execute(sql, (plugin_id, table_name, where_value))
            count = cur.rowcount
        self._commit()
        return count

    def plugin_data_count(
        self, plugin_id: str, table_name: str,
        where_field: str | None = None, where_value: Any = None
    ) -> int:
        sql = "SELECT COUNT(*) FROM plugin_data WHERE plugin_id=? AND table_name=?"
        params: list = [plugin_id, table_name]

        if where_field and where_value is not None:
            sql += f" AND JSON_EXTRACT(data_json, '$.{where_field}') = ?"
            params.append(where_value)

        with self._cursor() as cur:
            cur.execute(sql, tuple(params))
            return cur.fetchone()[0]

    def plugin_data_clear_table(self, plugin_id: str, table_name: str) -> int:
        with self._cursor() as cur:
            cur.execute(
                "DELETE FROM plugin_data WHERE plugin_id=? AND table_name=?",
                (plugin_id, table_name),
            )
            count = cur.rowcount
        self._commit()
        return count

    def plugin_data_clear_all(self, plugin_id: str) -> int:
        with self._cursor() as cur:
            cur.execute("DELETE FROM plugin_data WHERE plugin_id=?", (plugin_id,))
            count = cur.rowcount
        self._commit()
        return count

    def plugin_data_execute_json_query(
        self, plugin_id: str, table_name: str,
        json_where: str, params: tuple = ()
    ) -> list[dict]:
        sql = f"SELECT data_json FROM plugin_data WHERE plugin_id=? AND table_name=? AND {json_where}"
        with self._cursor() as cur:
            cur.execute(sql, (plugin_id, table_name) + params)
            rows = cur.fetchall()

        result = []
        for row in rows:
            try:
                result.append(json.loads(row[0]))
            except Exception:
                pass
        return result

    def create_group(self, group_id: str, name: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT OR IGNORE INTO groups(group_id, name, created_at) VALUES(?,?,?)",
                (group_id, name, now_ts()),
            )
        self._commit()

    def rename_group(self, group_id: str, name: str) -> None:
        with self._cursor() as cur:
            cur.execute("UPDATE groups SET name=? WHERE group_id=?", (name, group_id))
        self._commit()

    def get_group(self, group_id: str) -> dict | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT group_id, name, created_at FROM groups WHERE group_id=?",
                (group_id,),
            )
            r = cur.fetchone()
        if not r:
            return None
        return {"group_id": r[0], "name": r[1], "created_at": r[2]}

    def list_groups(self) -> list[dict]:
        with self._cursor() as cur:
            cur.execute("SELECT group_id, name, created_at FROM groups ORDER BY created_at DESC")
            rows = cur.fetchall()
        return [{"group_id": r[0], "name": r[1], "created_at": r[2]} for r in rows]

    def add_group_member(self, group_id: str, member_alias: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT OR IGNORE INTO group_members(group_id, member_alias, added_at) VALUES(?,?,?)",
                (group_id, member_alias, now_ts()),
            )
        self._commit()

    def remove_group_member(self, group_id: str, member_alias: str) -> None:
        with self._cursor() as cur:
            cur.execute(
                "DELETE FROM group_members WHERE group_id=? AND member_alias=?",
                (group_id, member_alias),
            )
        self._commit()

    def list_group_members(self, group_id: str) -> list[str]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT member_alias FROM group_members WHERE group_id=? ORDER BY member_alias ASC",
                (group_id,),
            )
            return [str(r[0]) for r in cur.fetchall() if r and r[0]]

    def is_group_member(self, group_id: str, member_alias: str) -> bool:
        with self._cursor() as cur:
            cur.execute(
                "SELECT 1 FROM group_members WHERE group_id=? AND member_alias=?",
                (group_id, member_alias),
            )
            return cur.fetchone() is not None

    def upsert_file(
        self,
        file_id: str,
        name: str | None,
        bytes_total: int,
        record_count: int,
        record_hashes: list[str],
        local_path: str,
    ) -> None:
        with self._cursor() as cur:
            cur.execute(
                "INSERT OR REPLACE INTO files(file_id, name, bytes_total, record_count, record_hashes_json, local_path, added_at) VALUES(?,?,?,?,?,?,?)",
                (
                    file_id,
                    name,
                    int(bytes_total),
                    int(record_count),
                    json.dumps(record_hashes, ensure_ascii=False),
                    str(local_path),
                    now_ts(),
                ),
            )
        self._commit()

    def get_file(self, file_id: str) -> dict | None:
        with self._cursor() as cur:
            cur.execute(
                "SELECT file_id, name, bytes_total, record_count, record_hashes_json, local_path, added_at FROM files WHERE file_id=?",
                (file_id,),
            )
            r = cur.fetchone()
        if not r:
            return None
        return {
            "file_id": r[0],
            "name": r[1],
            "bytes_total": int(r[2]),
            "record_count": int(r[3]),
            "record_hashes": json.loads(r[4]) if r[4] else [],
            "local_path": r[5],
            "added_at": int(r[6]),
        }

    def has_file(self, file_id: str) -> bool:
        with self._cursor() as cur:
            cur.execute("SELECT 1 FROM files WHERE file_id=?", (file_id,))
            return cur.fetchone() is not None

    def delete_file(self, file_id: str) -> None:
        with self._cursor() as cur:
            cur.execute("DELETE FROM files WHERE file_id=?", (file_id,))
        self._commit()

    def list_files(self, limit: int = 200) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT file_id, name, bytes_total, record_count, record_hashes_json, local_path, added_at "
                "FROM files ORDER BY added_at DESC LIMIT ?",
                (int(limit),),
            )
            rows = cur.fetchall()
        out: list[dict] = []
        for r in rows:
            out.append({
                "file_id": r[0],
                "name": r[1],
                "bytes_total": int(r[2]),
                "record_count": int(r[3]),
                "record_hashes": json.loads(r[4]) if r[4] else [],
                "local_path": r[5],
                "added_at": int(r[6]),
            })
        return out

    def add_file_offer(
        self,
        peer_alias: str,
        sender_alias: str,
        file_id: str,
        name: str | None,
        meta: dict,
    ) -> int:
        with self._cursor() as cur:
            cur.execute(
                "INSERT INTO file_offers(peer_alias, sender_alias, file_id, name, meta_json, received_at) VALUES(?,?,?,?,?,?)",
                (peer_alias, sender_alias, file_id, name, canonical_json(meta), now_ts()),
            )
            lastrowid = cur.lastrowid
        self._commit()
        return int(lastrowid)

    def list_file_offers(self, peer_alias: str, limit: int = 200) -> list[dict]:
        with self._cursor() as cur:
            cur.execute(
                "SELECT id, peer_alias, sender_alias, file_id, name, meta_json, received_at "
                "FROM file_offers WHERE peer_alias=? ORDER BY received_at DESC LIMIT ?",
                (peer_alias, int(limit)),
            )
            rows = cur.fetchall()
        out: list[dict] = []
        for r in rows:
            out.append({
                "id": int(r[0]),
                "peer_alias": r[1],
                "sender_alias": r[2],
                "file_id": r[3],
                "name": r[4],
                "meta": json.loads(r[5]) if r[5] else {},
                "received_at": int(r[6]),
            })
        return out

    def set_cloud_file_blob(self, file_id: str, blob_sha256: str, created_at: int) -> None:
        try:
            with self._cursor() as cur:
                cur.execute(
                    "INSERT OR REPLACE INTO cloud_file_map(file_id, blob_sha256, created_at) VALUES(?,?,?)",
                    (str(file_id), str(blob_sha256), int(created_at)),
                )
            self._commit()
        except Exception:
            pass

    def get_cloud_file_blob(self, file_id: str) -> dict | None:
        try:
            with self._cursor() as cur:
                cur.execute(
                    "SELECT file_id, blob_sha256, created_at FROM cloud_file_map WHERE file_id=?",
                    (str(file_id),),
                )
                r = cur.fetchone()
            if not r:
                return None
            return {"file_id": r[0], "blob_sha256": r[1], "created_at": r[2]}
        except Exception:
            return None

    def delete_cloud_file_blob(self, file_id: str) -> None:
        try:
            with self._cursor() as cur:
                cur.execute("DELETE FROM cloud_file_map WHERE file_id=?", (str(file_id),))
            self._commit()
        except Exception:
            pass
