# Copyright 2026, Jiri Eischmann <jiri@eischmann.cz> and the meshy contributors
# SPDX-License-Identifier: GPL-3.0-or-later

"""SQLite storage for contacts, messages, and channels."""

import contextlib
import os
import sqlite3
from datetime import datetime

from meshy.models import Channel, ChannelMessage, Contact, Message, MessageStatus, NotificationLevel


class Storage:
    @staticmethod
    def db_path_for_device(device_address: str) -> str:
        data_dir = os.path.join(
            os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "meshy"
        )
        safe_addr = device_address.replace(":", "_").replace("/", "_")
        return os.path.join(data_dir, f"device_{safe_addr}.db")

    @staticmethod
    def delete_device_db(device_address: str):
        path = Storage.db_path_for_device(device_address)
        for suffix in ("", "-wal", "-shm"):
            with contextlib.suppress(FileNotFoundError):
                os.remove(path + suffix)

    def __init__(self, db_path: str | None = None, device_address: str | None = None):
        if db_path is None:
            if device_address:
                db_path = self.db_path_for_device(device_address)
                os.makedirs(os.path.dirname(db_path), exist_ok=True)
            else:
                data_dir = os.path.join(
                    os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "meshy"
                )
                os.makedirs(data_dir, exist_ok=True)
                db_path = os.path.join(data_dir, "meshy.db")
        self._db = sqlite3.connect(db_path)
        self._db.row_factory = sqlite3.Row
        self._db.execute("PRAGMA journal_mode=WAL")
        self._batch_depth = 0
        self._create_tables()

    def _create_tables(self):
        self._db.executescript("""
            CREATE TABLE IF NOT EXISTS contacts (
                public_key_hex TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                type INTEGER NOT NULL DEFAULT 1,
                flags INTEGER NOT NULL DEFAULT 0,
                path_length INTEGER NOT NULL DEFAULT -1,
                path BLOB NOT NULL DEFAULT X'',
                latitude REAL,
                longitude REAL,
                last_seen REAL,
                last_message_at REAL,
                is_active INTEGER NOT NULL DEFAULT 1
            );

            CREATE TABLE IF NOT EXISTS messages (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                contact_key_hex TEXT NOT NULL,
                sender_key_hex TEXT NOT NULL,
                text TEXT NOT NULL,
                timestamp REAL NOT NULL,
                is_outgoing INTEGER NOT NULL,
                is_cli INTEGER NOT NULL DEFAULT 0,
                status INTEGER NOT NULL DEFAULT 0,
                message_id TEXT,
                FOREIGN KEY (contact_key_hex) REFERENCES contacts(public_key_hex)
            );

            CREATE TABLE IF NOT EXISTS channels (
                idx INTEGER PRIMARY KEY,
                name TEXT NOT NULL DEFAULT '',
                psk BLOB NOT NULL DEFAULT X'00000000000000000000000000000000',
                unread_count INTEGER NOT NULL DEFAULT 0,
                notification_level INTEGER NOT NULL DEFAULT 0
            );

            CREATE TABLE IF NOT EXISTS channel_messages (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                channel_index INTEGER NOT NULL,
                sender_name TEXT NOT NULL DEFAULT '',
                text TEXT NOT NULL,
                timestamp REAL NOT NULL,
                is_outgoing INTEGER NOT NULL DEFAULT 0,
                FOREIGN KEY (channel_index) REFERENCES channels(idx)
            );

            CREATE TABLE IF NOT EXISTS channel_message_paths (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                channel_index INTEGER NOT NULL,
                sender_timestamp INTEGER NOT NULL,
                msg_text_hash TEXT NOT NULL,
                snr REAL,
                path_len INTEGER NOT NULL DEFAULT -1,
                path_bytes TEXT NOT NULL DEFAULT ''
            );

            CREATE TABLE IF NOT EXISTS regions (
                name TEXT PRIMARY KEY
            );

            CREATE TABLE IF NOT EXISTS discovered_contacts (
                public_key_hex TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                type INTEGER NOT NULL DEFAULT 1,
                flags INTEGER NOT NULL DEFAULT 0,
                path_length INTEGER NOT NULL DEFAULT -1,
                path BLOB NOT NULL DEFAULT X'',
                path_hash_size INTEGER NOT NULL DEFAULT 1,
                latitude REAL,
                longitude REAL,
                last_seen REAL
            );

            CREATE INDEX IF NOT EXISTS idx_messages_contact
                ON messages(contact_key_hex, timestamp);
            CREATE INDEX IF NOT EXISTS idx_channel_messages_channel
                ON channel_messages(channel_index, timestamp);
            CREATE INDEX IF NOT EXISTS idx_channel_message_paths
                ON channel_message_paths(channel_index, sender_timestamp, msg_text_hash);
        """)
        self._db.commit()
        self._migrate()

    def _migrate(self):
        """Add columns that may not exist in older databases."""
        cursor = self._db.execute("PRAGMA table_info(channels)")
        ch_columns = {row[1] for row in cursor.fetchall()}
        if "notification_level" not in ch_columns:
            self._db.execute(
                "ALTER TABLE channels ADD COLUMN notification_level INTEGER NOT NULL DEFAULT 0"
            )
        if "last_read_at" not in ch_columns:
            self._db.execute("ALTER TABLE channels ADD COLUMN last_read_at REAL")
        if "region" not in ch_columns:
            self._db.execute("ALTER TABLE channels ADD COLUMN region TEXT NOT NULL DEFAULT ''")

        cursor = self._db.execute("PRAGMA table_info(contacts)")
        ct_columns = {row[1] for row in cursor.fetchall()}
        if "last_read_at" not in ct_columns:
            self._db.execute("ALTER TABLE contacts ADD COLUMN last_read_at REAL")
        if "path_hash_size" not in ct_columns:
            self._db.execute(
                "ALTER TABLE contacts ADD COLUMN path_hash_size INTEGER NOT NULL DEFAULT 1"
            )
        if "device_lastmod" not in ct_columns:
            self._db.execute("ALTER TABLE contacts ADD COLUMN device_lastmod REAL")

        cursor = self._db.execute("PRAGMA table_info(messages)")
        msg_columns = {row[1] for row in cursor.fetchall()}
        if "room_sender_name" not in msg_columns:
            self._db.execute(
                "ALTER TABLE messages ADD COLUMN room_sender_name TEXT NOT NULL DEFAULT ''"
            )

        self._db.execute("""
            CREATE TABLE IF NOT EXISTS room_passwords (
                room_key_hex TEXT PRIMARY KEY,
                password TEXT NOT NULL
            )
        """)
        self._db.execute("""
            CREATE TABLE IF NOT EXISTS settings (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL DEFAULT ''
            )
        """)

        self._db.execute("""
            CREATE TABLE IF NOT EXISTS discovered_contacts (
                public_key_hex TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                type INTEGER NOT NULL DEFAULT 1,
                flags INTEGER NOT NULL DEFAULT 0,
                path_length INTEGER NOT NULL DEFAULT -1,
                path BLOB NOT NULL DEFAULT X'',
                path_hash_size INTEGER NOT NULL DEFAULT 1,
                latitude REAL,
                longitude REAL,
                last_seen REAL
            )
        """)

        self._db.commit()
        self._cleanup_old_data()

    def _cleanup_old_data(self):
        """Remove stale data that is no longer relevant."""
        import time

        week_ago = time.time() - 7 * 86400
        self._db.execute(
            "DELETE FROM channel_message_paths WHERE sender_timestamp < ?", (int(week_ago),)
        )
        ninety_days_ago = time.time() - 90 * 86400
        self._db.execute("DELETE FROM discovered_contacts WHERE last_seen < ?", (ninety_days_ago,))
        self._db.commit()

    def _commit(self):
        if self._batch_depth == 0:
            self._db.commit()

    @contextlib.contextmanager
    def batch(self):
        self._batch_depth += 1
        try:
            yield
        finally:
            self._batch_depth -= 1
            if self._batch_depth == 0:
                self._commit()

    def close(self):
        self._db.close()

    # ─── Contacts ──────────────────────────────────────────────

    def save_contact(self, contact: Contact):
        self._db.execute(
            """
            INSERT INTO contacts
            (public_key_hex, name, type, flags, path_length, path, path_hash_size,
             latitude, longitude, last_seen, device_lastmod, last_message_at, is_active)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(public_key_hex) DO UPDATE SET
                name = excluded.name,
                type = excluded.type,
                flags = excluded.flags,
                path_length = excluded.path_length,
                path = excluded.path,
                path_hash_size = excluded.path_hash_size,
                latitude = excluded.latitude,
                longitude = excluded.longitude,
                last_seen = excluded.last_seen,
                device_lastmod = excluded.device_lastmod,
                last_message_at = excluded.last_message_at,
                is_active = excluded.is_active
        """,
            (
                contact.public_key_hex,
                contact.name,
                contact.type,
                contact.flags,
                contact.path_length,
                contact.path,
                contact.path_hash_size,
                contact.latitude,
                contact.longitude,
                contact.last_seen.timestamp() if contact.last_seen else None,
                contact.device_lastmod.timestamp() if contact.device_lastmod else None,
                contact.last_message_at.timestamp() if contact.last_message_at else None,
                int(contact.is_active),
            ),
        )
        self._commit()

    def deactivate_contact(self, public_key_hex: str):
        """Mark a contact as inactive (no longer on the device)."""
        self._db.execute(
            "UPDATE contacts SET is_active = 0 WHERE public_key_hex = ?", (public_key_hex,)
        )
        self._commit()

    def get_contacts(self) -> list[Contact]:
        rows = self._db.execute(
            "SELECT * FROM contacts WHERE is_active = 1 ORDER BY last_seen DESC"
        ).fetchall()
        return [self._row_to_contact(r) for r in rows]

    def get_max_device_lastmod(self) -> int | None:
        """Return the highest device_lastmod timestamp among active contacts."""
        row = self._db.execute(
            "SELECT MAX(device_lastmod) FROM contacts WHERE is_active = 1"
        ).fetchone()
        val = row[0] if row else None
        return int(val) if val else None

    def get_contact(self, public_key_hex: str) -> Contact | None:
        row = self._db.execute(
            "SELECT * FROM contacts WHERE public_key_hex = ?", (public_key_hex,)
        ).fetchone()
        return self._row_to_contact(row) if row else None

    def remove_contact(self, public_key_hex: str):
        self._db.execute("DELETE FROM contacts WHERE public_key_hex = ?", (public_key_hex,))
        self._db.execute("DELETE FROM messages WHERE contact_key_hex = ?", (public_key_hex,))
        self._commit()

    def find_contact_by_prefix(self, prefix: bytes) -> Contact | None:
        """Find a contact by the first 6 bytes of its public key."""
        prefix_hex = prefix.hex()
        upper = prefix_hex[:-1] + chr(ord(prefix_hex[-1]) + 1)
        row = self._db.execute(
            "SELECT * FROM contacts WHERE public_key_hex >= ? AND public_key_hex < ?",
            (prefix_hex, upper),
        ).fetchone()
        return self._row_to_contact(row) if row else None

    def _row_to_contact(self, row) -> Contact:
        return Contact(
            public_key=bytes.fromhex(row["public_key_hex"]),
            name=row["name"],
            type=row["type"],
            flags=row["flags"],
            path_length=row["path_length"],
            path=bytes(row["path"]),
            path_hash_size=row["path_hash_size"],
            latitude=row["latitude"],
            longitude=row["longitude"],
            last_seen=datetime.fromtimestamp(row["last_seen"]) if row["last_seen"] else None,
            device_lastmod=datetime.fromtimestamp(row["device_lastmod"])
            if row["device_lastmod"]
            else None,
            last_message_at=datetime.fromtimestamp(row["last_message_at"])
            if row["last_message_at"]
            else None,
            is_active=bool(row["is_active"]),
        )

    def get_oldest_unread_contact_timestamp(self, contact_key_hex: str) -> float | None:
        """Return the timestamp of the oldest unread incoming message for a contact."""
        last_read = self.get_contact_last_read_at(contact_key_hex)
        if last_read is None:
            row = self._db.execute(
                """
                SELECT MIN(timestamp) as ts FROM messages
                WHERE contact_key_hex = ? AND is_outgoing = 0
            """,
                (contact_key_hex,),
            ).fetchone()
        else:
            row = self._db.execute(
                """
                SELECT MIN(timestamp) as ts FROM messages
                WHERE contact_key_hex = ? AND timestamp > ? AND is_outgoing = 0
            """,
                (contact_key_hex, last_read),
            ).fetchone()
        return row["ts"] if row and row["ts"] else None

    def get_contact_last_read_at(self, public_key_hex: str) -> float | None:
        row = self._db.execute(
            "SELECT last_read_at FROM contacts WHERE public_key_hex = ?", (public_key_hex,)
        ).fetchone()
        return row["last_read_at"] if row and row["last_read_at"] else None

    def set_contact_last_read_at(self, public_key_hex: str, timestamp: float):
        self._db.execute(
            "UPDATE contacts SET last_read_at = ? WHERE public_key_hex = ?",
            (timestamp, public_key_hex),
        )
        self._commit()

    # ─── Discovered Contacts ─────────────────────────────────

    def save_discovered_contact(self, contact: Contact):
        self._db.execute(
            """
            INSERT INTO discovered_contacts
            (public_key_hex, name, type, flags, path_length, path,
             path_hash_size, latitude, longitude, last_seen)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(public_key_hex) DO UPDATE SET
                name = excluded.name,
                type = excluded.type,
                flags = excluded.flags,
                path_length = excluded.path_length,
                path = excluded.path,
                path_hash_size = excluded.path_hash_size,
                latitude = excluded.latitude,
                longitude = excluded.longitude,
                last_seen = excluded.last_seen
        """,
            (
                contact.public_key_hex,
                contact.name,
                contact.type,
                contact.flags,
                contact.path_length,
                contact.path,
                contact.path_hash_size,
                contact.latitude,
                contact.longitude,
                contact.last_seen.timestamp() if contact.last_seen else None,
            ),
        )
        self._commit()

    def get_discovered_contacts(self) -> list[Contact]:
        rows = self._db.execute(
            "SELECT * FROM discovered_contacts ORDER BY last_seen DESC"
        ).fetchall()
        return [self._row_to_discovered_contact(r) for r in rows]

    def _row_to_discovered_contact(self, row) -> Contact:
        return Contact(
            public_key=bytes.fromhex(row["public_key_hex"]),
            name=row["name"],
            type=row["type"],
            flags=row["flags"],
            path_length=row["path_length"],
            path=bytes(row["path"]),
            path_hash_size=row["path_hash_size"],
            latitude=row["latitude"],
            longitude=row["longitude"],
            last_seen=datetime.fromtimestamp(row["last_seen"]) if row["last_seen"] else None,
            is_active=True,
        )

    def count_unread_messages(self, contact_key_hex: str) -> int:
        """Count incoming messages newer than last_read_at for a contact."""
        last_read = self.get_contact_last_read_at(contact_key_hex)
        if last_read is None:
            row = self._db.execute(
                """
                SELECT COUNT(*) as cnt FROM messages
                WHERE contact_key_hex = ? AND is_outgoing = 0
            """,
                (contact_key_hex,),
            ).fetchone()
        else:
            row = self._db.execute(
                """
                SELECT COUNT(*) as cnt FROM messages
                WHERE contact_key_hex = ? AND timestamp > ? AND is_outgoing = 0
            """,
                (contact_key_hex, last_read),
            ).fetchone()
        return row["cnt"] if row else 0

    def count_unread_channel_messages(self, channel_index: int) -> int:
        """Count channel messages newer than last_read_at."""
        last_read = self.get_channel_last_read_at(channel_index)
        if last_read is None:
            row = self._db.execute(
                """
                SELECT COUNT(*) as cnt FROM channel_messages
                WHERE channel_index = ? AND is_outgoing = 0
            """,
                (channel_index,),
            ).fetchone()
        else:
            row = self._db.execute(
                """
                SELECT COUNT(*) as cnt FROM channel_messages
                WHERE channel_index = ? AND timestamp > ? AND is_outgoing = 0
            """,
                (channel_index, last_read),
            ).fetchone()
        return row["cnt"] if row else 0

    # ─── Messages ──────────────────────────────────────────────

    def save_message(self, contact_key_hex: str, message: Message):
        self._db.execute(
            """
            INSERT INTO messages
            (contact_key_hex, sender_key_hex, text, timestamp,
             is_outgoing, is_cli, status, message_id, room_sender_name)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """,
            (
                contact_key_hex,
                message.sender_key_hex,
                message.text,
                message.timestamp.timestamp(),
                int(message.is_outgoing),
                int(message.is_cli),
                int(message.status),
                message.message_id,
                message.room_sender_name,
            ),
        )
        # Update contact's last_message_at
        self._db.execute(
            """
            UPDATE contacts SET last_message_at = ?
            WHERE public_key_hex = ?
        """,
            (message.timestamp.timestamp(), contact_key_hex),
        )
        self._commit()

    def get_messages(self, contact_key_hex: str, limit: int = 30, offset: int = 0) -> list[Message]:
        rows = self._db.execute(
            """
            SELECT * FROM messages
            WHERE contact_key_hex = ?
            ORDER BY timestamp DESC LIMIT ? OFFSET ?
        """,
            (contact_key_hex, limit, offset),
        ).fetchall()
        return [self._row_to_message(r) for r in reversed(rows)]

    def has_duplicate_message(self, contact_key_hex: str, timestamp: float, text: str) -> bool:
        """Check if an incoming message is a duplicate.

        Matches same sender + text within a 5-second window around the
        timestamp to catch retries where the timestamp may differ slightly.
        """
        row = self._db.execute(
            """
            SELECT 1 FROM messages
            WHERE contact_key_hex = ? AND text = ?
              AND is_outgoing = 0
              AND abs(timestamp - ?) <= 5
            LIMIT 1
        """,
            (contact_key_hex, text, timestamp),
        ).fetchone()
        return row is not None

    def delete_message(self, message_id: str):
        self._db.execute("DELETE FROM messages WHERE message_id = ?", (message_id,))
        self._commit()

    def update_message_status(self, message_id: str, status: MessageStatus):
        self._db.execute(
            "UPDATE messages SET status = ? WHERE message_id = ?", (int(status), message_id)
        )
        self._commit()

    def fail_orphaned_messages(self):
        """Mark any PENDING or SENT outgoing messages as FAILED.

        Called on disconnect to clean up messages that were in flight
        when the connection dropped.
        """
        self._db.execute(
            "UPDATE messages SET status = ? WHERE is_outgoing = 1 AND status IN (?, ?)",
            (int(MessageStatus.FAILED), int(MessageStatus.PENDING), int(MessageStatus.SENT)),
        )
        self._commit()

    def _row_to_message(self, row) -> Message:
        return Message(
            sender_key=bytes.fromhex(row["sender_key_hex"]),
            text=row["text"],
            timestamp=datetime.fromtimestamp(row["timestamp"]),
            is_outgoing=bool(row["is_outgoing"]),
            is_cli=bool(row["is_cli"]),
            status=MessageStatus(row["status"]),
            message_id=row["message_id"],
            room_sender_name=row["room_sender_name"],
        )

    # ─── Room Passwords ───────────────────────────────────────

    def save_room_password(self, room_key_hex: str, password: str):
        self._db.execute(
            "INSERT OR REPLACE INTO room_passwords (room_key_hex, password) VALUES (?, ?)",
            (room_key_hex, password),
        )
        self._commit()

    def get_room_password(self, room_key_hex: str) -> str | None:
        row = self._db.execute(
            "SELECT password FROM room_passwords WHERE room_key_hex = ?", (room_key_hex,)
        ).fetchone()
        return row["password"] if row else None

    def remove_room_password(self, room_key_hex: str):
        self._db.execute("DELETE FROM room_passwords WHERE room_key_hex = ?", (room_key_hex,))
        self._commit()

    # ─── Channels ──────────────────────────────────────────────

    def save_channel(self, channel: Channel):
        self._db.execute(
            """
            INSERT INTO channels (idx, name, psk, unread_count, notification_level, region)
            VALUES (?, ?, ?, ?, ?, ?)
            ON CONFLICT(idx) DO UPDATE SET
                name = excluded.name,
                psk = excluded.psk,
                unread_count = excluded.unread_count,
                notification_level = excluded.notification_level,
                region = excluded.region
        """,
            (
                channel.index,
                channel.name,
                channel.psk,
                channel.unread_count,
                int(channel.notification_level),
                channel.region,
            ),
        )
        self._commit()

    def get_channels(self) -> list[Channel]:
        rows = self._db.execute("SELECT * FROM channels ORDER BY idx").fetchall()
        return [self._row_to_channel(r) for r in rows]

    def get_channel(self, index: int) -> Channel | None:
        row = self._db.execute("SELECT * FROM channels WHERE idx = ?", (index,)).fetchone()
        return self._row_to_channel(row) if row else None

    def _row_to_channel(self, row) -> Channel:
        return Channel(
            index=row["idx"],
            name=row["name"],
            psk=bytes(row["psk"]),
            unread_count=row["unread_count"],
            notification_level=NotificationLevel(row["notification_level"]),
            region=row["region"].split(",")[0].strip(),
        )

    def remove_channel(self, index: int):
        self._db.execute("DELETE FROM channels WHERE idx = ?", (index,))
        self._db.execute("DELETE FROM channel_messages WHERE channel_index = ?", (index,))
        self._commit()

    def set_channel_notification_level(self, index: int, level: NotificationLevel):
        self._db.execute(
            "UPDATE channels SET notification_level = ? WHERE idx = ?", (int(level), index)
        )
        self._commit()

    def set_channel_region(self, index: int, region: str):
        self._db.execute("UPDATE channels SET region = ? WHERE idx = ?", (region, index))
        self._commit()

    # ─── Regions ──────────────────────────────────────────────

    def get_regions(self) -> list[str]:
        rows = self._db.execute("SELECT name FROM regions ORDER BY name").fetchall()
        return [r["name"] for r in rows]

    def add_region(self, name: str):
        self._db.execute("INSERT OR IGNORE INTO regions (name) VALUES (?)", (name,))
        self._commit()

    def remove_region(self, name: str):
        self._db.execute("DELETE FROM regions WHERE name = ?", (name,))
        # Clear region from channels that used it
        self._db.execute("UPDATE channels SET region = '' WHERE region = ?", (name,))
        # Clear default scope if it was this region
        row = self._db.execute("SELECT value FROM settings WHERE key = 'default_scope'").fetchone()
        if row and row["value"] == name:
            self._db.execute(
                "INSERT OR REPLACE INTO settings (key, value) VALUES ('default_scope', '')"
            )
        self._commit()

    def get_default_scope(self) -> str:
        row = self._db.execute("SELECT value FROM settings WHERE key = 'default_scope'").fetchone()
        return row["value"] if row else ""

    def set_default_scope(self, scope: str):
        self._db.execute(
            "INSERT OR REPLACE INTO settings (key, value) VALUES ('default_scope', ?)", (scope,)
        )
        self._commit()

    def get_oldest_unread_channel_timestamp(self, channel_index: int) -> float | None:
        """Return the timestamp of the oldest unread incoming channel message."""
        last_read = self.get_channel_last_read_at(channel_index)
        if last_read is None:
            row = self._db.execute(
                """
                SELECT MIN(timestamp) as ts FROM channel_messages
                WHERE channel_index = ? AND is_outgoing = 0
            """,
                (channel_index,),
            ).fetchone()
        else:
            row = self._db.execute(
                """
                SELECT MIN(timestamp) as ts FROM channel_messages
                WHERE channel_index = ? AND timestamp > ? AND is_outgoing = 0
            """,
                (channel_index, last_read),
            ).fetchone()
        return row["ts"] if row and row["ts"] else None

    def get_channel_last_read_at(self, index: int) -> float | None:
        row = self._db.execute(
            "SELECT last_read_at FROM channels WHERE idx = ?", (index,)
        ).fetchone()
        return row["last_read_at"] if row and row["last_read_at"] else None

    def get_max_channel_message_timestamp(self, channel_index: int) -> float | None:
        """Return the newest message timestamp for a channel."""
        row = self._db.execute(
            "SELECT MAX(timestamp) as ts FROM channel_messages WHERE channel_index = ?",
            (channel_index,),
        ).fetchone()
        return row["ts"] if row and row["ts"] else None

    def set_channel_last_read_at(self, index: int, timestamp: float):
        self._db.execute("UPDATE channels SET last_read_at = ? WHERE idx = ?", (timestamp, index))
        self._commit()

    # ─── Channel Messages ──────────────────────────────────────

    def save_channel_message(self, msg: ChannelMessage):
        self._db.execute(
            """
            INSERT INTO channel_messages
            (channel_index, sender_name, text, timestamp, is_outgoing)
            VALUES (?, ?, ?, ?, ?)
        """,
            (
                msg.channel_index,
                msg.sender_name,
                msg.text,
                msg.timestamp.timestamp(),
                int(msg.is_outgoing),
            ),
        )
        self._commit()

    def delete_channel_message(self, channel_index: int, timestamp: float, text: str):
        self._db.execute(
            """
            DELETE FROM channel_messages
            WHERE channel_index = ? AND abs(timestamp - ?) < 1 AND text = ?
        """,
            (channel_index, timestamp, text),
        )
        self._commit()

    def get_channel_messages(
        self, channel_index: int, limit: int = 30, offset: int = 0
    ) -> list[ChannelMessage]:
        rows = self._db.execute(
            """
            SELECT * FROM channel_messages
            WHERE channel_index = ?
            ORDER BY timestamp DESC LIMIT ? OFFSET ?
        """,
            (channel_index, limit, offset),
        ).fetchall()
        return [
            ChannelMessage(
                channel_index=r["channel_index"],
                sender_name=r["sender_name"],
                text=r["text"],
                timestamp=datetime.fromtimestamp(r["timestamp"]),
                is_outgoing=bool(r["is_outgoing"]),
            )
            for r in reversed(rows)
        ]

    # ─── Channel Message Paths ────────────────────────────────

    @staticmethod
    def _msg_text_hash(text: str) -> str:
        import hashlib

        return hashlib.md5(text.encode("utf-8")).hexdigest()[:16]

    def save_channel_message_path(
        self, channel_index: int, sender_timestamp: int, text: str, path_info: dict
    ):
        text_hash = self._msg_text_hash(text)
        self._db.execute(
            """
            INSERT INTO channel_message_paths
            (channel_index, sender_timestamp, msg_text_hash, snr, path_len, path_bytes)
            VALUES (?, ?, ?, ?, ?, ?)
        """,
            (
                channel_index,
                sender_timestamp,
                text_hash,
                path_info.get("snr"),
                path_info.get("path_len", -1),
                path_info.get("path_bytes", ""),
            ),
        )
        self._commit()

    def get_channel_message_paths(
        self, channel_index: int, sender_timestamp: int, text: str
    ) -> list[dict]:
        text_hash = self._msg_text_hash(text)
        rows = self._db.execute(
            """
            SELECT snr, path_len, path_bytes FROM channel_message_paths
            WHERE channel_index = ? AND sender_timestamp = ? AND msg_text_hash = ?
        """,
            (channel_index, sender_timestamp, text_hash),
        ).fetchall()
        return [
            {"snr": r["snr"], "path_len": r["path_len"], "path_bytes": r["path_bytes"]}
            for r in rows
        ]
