import Foundation
import GRDB

/// Central database manager. Owns the GRDB DatabasePool for the current account.
/// Android parity: `RosettaDatabase.kt` — Room database with WAL mode.
@MainActor
final class DatabaseManager {

    static let shared = DatabaseManager()
    /// Internal migration identifiers exposed for migration harness tests.
    nonisolated static let migrationIdentifiers: [String] = [
        "v1_initial",
        "v2_sync_cursors",
        "v3_split_read_status",
        "v4_schema_parity_android_desktop",
        "v5_full_schema_superset_parity",
        "v6_bidirectional_alias_sync",
        "v7_sync_cursor_reconcile_and_perf_indexes",
        "v8_last_message_sender_key",
    ]
    nonisolated static let migrationV7SyncCursorReconcile = "v7_sync_cursor_reconcile_and_perf_indexes"
    nonisolated static let migrationV8LastMessageSenderKey = "v8_last_message_sender_key"

    private var dbPool: DatabasePool?
    private var currentAccount: String = ""

    private init() {}

    // MARK: - Lifecycle

    /// Open (or create) the SQLite database for the given account.
    /// Must be called during session start, before any repository access.
    func bootstrap(accountPublicKey: String) throws {
        let account = accountPublicKey.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !account.isEmpty else { return }

        // Already open for this account
        if currentAccount == account, dbPool != nil { return }

        // Close previous if switching accounts
        close()

        currentAccount = account
        let dbURL = Self.databaseURL(for: account)

        // Ensure directory exists
        let dir = dbURL.deletingLastPathComponent()
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)

        // Configure GRDB
        var config = Configuration()
        config.foreignKeysEnabled = true
        // iOS file protection — encrypted at rest when device is locked.
        config.prepareDatabase { db in
            // WAL mode for concurrent reads (Android parity: JournalMode.WRITE_AHEAD_LOGGING)
            try db.execute(sql: "PRAGMA journal_mode = WAL")
            // PERF: NORMAL sync = async fsync; reduces write latency ~2-3× vs FULL.
            try db.execute(sql: "PRAGMA synchronous = NORMAL")
            // PERF: Checkpoint after 1000 pages (~4MB) instead of default 1000 frames.
            try db.execute(sql: "PRAGMA wal_autocheckpoint = 1000")
        }

        let pool = try DatabasePool(path: dbURL.path, configuration: config)

        // Apply schema migrations
        var migrator = DatabaseMigrator()
        migrator.registerMigration("v1_initial") { db in
            // Messages table — matches Android MessageEntity
            try db.create(table: "messages") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("account", .text).notNull()
                t.column("from_public_key", .text).notNull()
                t.column("to_public_key", .text).notNull()
                // Desktop/Android parity: encrypted payload as received from network.
                t.column("content", .text).notNull().defaults(to: "")
                t.column("chacha_key", .text).notNull().defaults(to: "")
                t.column("text", .text).notNull().defaults(to: "")
                // Android/Desktop naming parity (alias of `text` in iOS pipeline).
                t.column("plain_message", .text).notNull().defaults(to: "")
                t.column("timestamp", .integer).notNull()
                t.column("is_read", .integer).notNull().defaults(to: 0)
                // Android/Desktop naming parity (alias of `is_read`).
                t.column("read", .integer).notNull().defaults(to: 0)
                t.column("from_me", .integer).notNull().defaults(to: 0)
                t.column("delivery_status", .integer).notNull().defaults(to: 0)
                // Android/Desktop naming parity (alias of `delivery_status`).
                t.column("delivered", .integer).notNull().defaults(to: 0)
                t.column("message_id", .text).notNull()
                t.column("reply_to_message_id", .text)
                t.column("dialog_key", .text).notNull()
                t.column("attachments", .text).notNull().defaults(to: "[]")
                t.column("attachment_password", .text)
            }

            // Android parity indexes
            try db.create(
                index: "idx_messages_account_message_id",
                on: "messages",
                columns: ["account", "message_id"],
                unique: true
            )
            try db.create(
                index: "idx_messages_account_dialog_key_timestamp",
                on: "messages",
                columns: ["account", "dialog_key", "timestamp"]
            )

            // Dialogs table — matches Android DialogEntity
            try db.create(table: "dialogs") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("account", .text).notNull()
                t.column("opponent_key", .text).notNull()
                t.column("opponent_title", .text).notNull().defaults(to: "")
                t.column("opponent_username", .text).notNull().defaults(to: "")
                t.column("last_message", .text).notNull().defaults(to: "")
                t.column("last_message_timestamp", .integer).notNull().defaults(to: 0)
                t.column("unread_count", .integer).notNull().defaults(to: 0)
                t.column("is_online", .integer).notNull().defaults(to: 0)
                t.column("last_seen", .integer).notNull().defaults(to: 0)
                t.column("verified", .integer).notNull().defaults(to: 0)
                t.column("i_have_sent", .integer).notNull().defaults(to: 0)
                t.column("is_pinned", .integer).notNull().defaults(to: 0)
                t.column("is_muted", .integer).notNull().defaults(to: 0)
                t.column("last_message_from_me", .integer).notNull().defaults(to: 0)
                t.column("last_message_delivered", .integer).notNull().defaults(to: 0)
            }

            try db.create(
                index: "idx_dialogs_account_opponent_key",
                on: "dialogs",
                columns: ["account", "opponent_key"],
                unique: true
            )
            try db.create(
                index: "idx_dialogs_account_timestamp",
                on: "dialogs",
                columns: ["account", "last_message_timestamp"]
            )
        }

        // v2: sync_cursors table (Android parity: AccountSyncTimeEntity in SQLite, not UserDefaults)
        migrator.registerMigration("v2_sync_cursors") { db in
            try db.create(table: "sync_cursors") { t in
                t.column("account", .text).notNull().primaryKey()
                t.column("timestamp", .integer).notNull().defaults(to: 0)
            }
        }

        // v3: Android/Desktop parity — split delivery_status into delivered (0/1/2) + read (0/1).
        // Previously iOS combined both into delivery_status (0=WAITING, 1=DELIVERED, 2=ERROR, 3=READ).
        // Android and Desktop use separate columns: delivered (0/1/2) + read (0/1).
        migrator.registerMigration("v3_split_read_status") { db in
            // Step 1: Outgoing non-READ messages had is_read=1 due to `|| fromMe` in insert.
            // Reset to 0 — is_read for outgoing now means "opponent has read" (Android parity).
            try db.execute(sql: "UPDATE messages SET is_read = 0 WHERE from_me = 1 AND delivery_status != 3")

            // Step 2: Convert READ (3) → DELIVERED (1). is_read stays 1 for these.
            try db.execute(sql: "UPDATE messages SET delivery_status = 1 WHERE delivery_status = 3")

            // Step 3: Add last_message_read to dialogs (Android: lastMessageRead column).
            let dialogColumns = Set(try db.columns(in: "dialogs").map(\.name))
            if !dialogColumns.contains("last_message_read") {
                try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_message_read INTEGER NOT NULL DEFAULT 0")
            }
        }

        // v4: align critical schema gaps with Android/Desktop without breaking existing iOS code.
        migrator.registerMigration("v4_schema_parity_android_desktop") { db in
            // Android parity index used by participant-pair queries and migration tooling.
            try db.execute(
                sql: """
                    CREATE INDEX IF NOT EXISTS idx_messages_account_from_to_timestamp
                    ON messages(account, from_public_key, to_public_key, timestamp)
                    """
            )

            // Android parity: dialogs cache attachments of the latest message.
            let dialogColumns = Set(try db.columns(in: "dialogs").map(\.name))
            if !dialogColumns.contains("last_message_attachments") {
                try db.execute(
                    sql: "ALTER TABLE dialogs ADD COLUMN last_message_attachments TEXT NOT NULL DEFAULT '[]'"
                )
            }

            // Backfill attachment cache for existing dialogs from current messages.
            try db.execute(
                sql: """
                    UPDATE dialogs
                    SET last_message_attachments = COALESCE((
                        SELECT m.attachments
                        FROM messages m
                        WHERE m.account = dialogs.account
                          AND m.dialog_key = CASE
                              WHEN dialogs.account = dialogs.opponent_key THEN dialogs.account
                              WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE '#group:%' THEN dialogs.opponent_key
                              WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE 'group:%' THEN dialogs.opponent_key
                              WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE 'conversation:%' THEN dialogs.opponent_key
                              WHEN dialogs.account < dialogs.opponent_key THEN dialogs.account || ':' || dialogs.opponent_key
                              ELSE dialogs.opponent_key || ':' || dialogs.account
                          END
                        ORDER BY m.timestamp DESC, m.message_id DESC
                        LIMIT 1
                    ), '[]')
                    WHERE last_message_attachments = '[]'
                    """
            )

            // Android/Desktop naming parity for sync cursor storage.
            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS accounts_sync_times (
                        account TEXT NOT NULL PRIMARY KEY,
                        last_sync INTEGER NOT NULL
                    )
                    """
            )

            if try db.tableExists("sync_cursors") {
                try db.execute(
                    sql: """
                        INSERT OR IGNORE INTO accounts_sync_times (account, last_sync)
                        SELECT account, timestamp FROM sync_cursors
                        """
                )
                try db.execute(
                    sql: """
                        UPDATE accounts_sync_times
                        SET last_sync = CASE
                            WHEN COALESCE(
                                (SELECT s.timestamp FROM sync_cursors s WHERE s.account = accounts_sync_times.account),
                                last_sync
                            ) > last_sync
                            THEN (SELECT s.timestamp FROM sync_cursors s WHERE s.account = accounts_sync_times.account)
                            ELSE last_sync
                        END
                        WHERE account IN (SELECT account FROM sync_cursors)
                        """
                )
            }

            // Android parity tables (prepared for shared features).
            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS pinned_messages (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        account TEXT NOT NULL,
                        dialog_key TEXT NOT NULL,
                        message_id TEXT NOT NULL,
                        pinned_at INTEGER NOT NULL
                    )
                    """
            )
            try db.execute(
                sql: """
                    CREATE UNIQUE INDEX IF NOT EXISTS index_pinned_messages_account_dialog_key_message_id
                    ON pinned_messages(account, dialog_key, message_id)
                    """
            )
            try db.execute(
                sql: """
                    CREATE INDEX IF NOT EXISTS index_pinned_messages_account_dialog_key_pinned_at
                    ON pinned_messages(account, dialog_key, pinned_at)
                    """
            )

            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS groups (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        account TEXT NOT NULL,
                        group_id TEXT NOT NULL,
                        title TEXT NOT NULL,
                        description TEXT NOT NULL,
                        "key" TEXT NOT NULL
                    )
                    """
            )
            try db.execute(
                sql: """
                    CREATE UNIQUE INDEX IF NOT EXISTS index_groups_account_group_id
                    ON groups(account, group_id)
                    """
            )

            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS avatar_cache (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        public_key TEXT NOT NULL,
                        avatar TEXT NOT NULL,
                        timestamp INTEGER NOT NULL
                    )
                    """
            )
            try db.execute(
                sql: """
                    CREATE INDEX IF NOT EXISTS index_avatar_cache_public_key_timestamp
                    ON avatar_cache(public_key, timestamp)
                    """
            )

            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS blacklist (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        public_key TEXT NOT NULL,
                        account TEXT NOT NULL
                    )
                    """
            )
            try db.execute(
                sql: """
                    CREATE UNIQUE INDEX IF NOT EXISTS index_blacklist_account_public_key
                    ON blacklist(account, public_key)
                    """
            )
        }

        // v5: full schema superset parity with Android + Desktop, preserving iOS contracts.
        migrator.registerMigration("v5_full_schema_superset_parity") { db in
            func hasColumn(_ table: String, _ column: String) throws -> Bool {
                try db.columns(in: table).contains { $0.name == column }
            }

            // MARK: - messages parity columns

            if try !hasColumn("messages", "content") {
                try db.execute(sql: "ALTER TABLE messages ADD COLUMN content TEXT NOT NULL DEFAULT ''")
            }
            if try !hasColumn("messages", "chacha_key") {
                try db.execute(sql: "ALTER TABLE messages ADD COLUMN chacha_key TEXT NOT NULL DEFAULT ''")
            }
            if try !hasColumn("messages", "plain_message") {
                try db.execute(sql: "ALTER TABLE messages ADD COLUMN plain_message TEXT NOT NULL DEFAULT ''")
            }
            if try !hasColumn("messages", "read") {
                try db.execute(sql: "ALTER TABLE messages ADD COLUMN read INTEGER NOT NULL DEFAULT 0")
            }
            if try !hasColumn("messages", "delivered") {
                try db.execute(sql: "ALTER TABLE messages ADD COLUMN delivered INTEGER NOT NULL DEFAULT 0")
            }
            if try !hasColumn("messages", "reply_to_message_id") {
                try db.execute(sql: "ALTER TABLE messages ADD COLUMN reply_to_message_id TEXT")
            }

            try db.execute(
                sql: """
                    UPDATE messages
                    SET read = is_read,
                        delivered = delivery_status,
                        plain_message = CASE
                            WHEN plain_message = '' THEN text
                            ELSE plain_message
                        END
                    """
            )

            // Keep iOS-native and Android/Desktop alias columns synchronized.
            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_messages_ai_sync_aliases")
            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_messages_au_sync_aliases")
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_messages_ai_sync_aliases
                    AFTER INSERT ON messages
                    BEGIN
                        UPDATE messages
                        SET read = NEW.is_read,
                            delivered = NEW.delivery_status,
                            plain_message = CASE
                                WHEN NEW.plain_message = '' THEN NEW.text
                                ELSE NEW.plain_message
                            END
                        WHERE id = NEW.id;
                    END
                    """
            )
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_messages_au_sync_aliases
                    AFTER UPDATE OF is_read, delivery_status, text ON messages
                    BEGIN
                        UPDATE messages
                        SET read = NEW.is_read,
                            delivered = NEW.delivery_status,
                            plain_message = NEW.text
                        WHERE id = NEW.id;
                    END
                    """
            )

            // MARK: - dialogs parity alias columns

            if try !hasColumn("dialogs", "dialog_id") {
                try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN dialog_id TEXT NOT NULL DEFAULT ''")
            }
            if try !hasColumn("dialogs", "last_message_id") {
                try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_message_id TEXT NOT NULL DEFAULT ''")
            }
            if try !hasColumn("dialogs", "last_timestamp") {
                try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_timestamp INTEGER NOT NULL DEFAULT 0")
            }
            if try !hasColumn("dialogs", "is_request") {
                try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN is_request INTEGER NOT NULL DEFAULT 0")
            }

            try db.execute(
                sql: """
                    UPDATE dialogs
                    SET dialog_id = CASE
                        WHEN dialog_id = '' THEN opponent_key
                        ELSE dialog_id
                    END,
                        last_timestamp = last_message_timestamp,
                        is_request = CASE WHEN i_have_sent = 1 THEN 0 ELSE 1 END
                    """
            )

            try db.execute(
                sql: """
                    UPDATE dialogs
                    SET last_message_id = COALESCE((
                        SELECT m.message_id
                        FROM messages m
                        WHERE m.account = dialogs.account
                          AND m.dialog_key = CASE
                              WHEN dialogs.account = dialogs.opponent_key THEN dialogs.account
                              WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE '#group:%' THEN dialogs.opponent_key
                              WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE 'group:%' THEN dialogs.opponent_key
                              WHEN LOWER(TRIM(dialogs.opponent_key)) LIKE 'conversation:%' THEN dialogs.opponent_key
                              WHEN dialogs.account < dialogs.opponent_key THEN dialogs.account || ':' || dialogs.opponent_key
                              ELSE dialogs.opponent_key || ':' || dialogs.account
                          END
                        ORDER BY m.timestamp DESC, m.message_id DESC
                        LIMIT 1
                    ), last_message_id)
                    """
            )

            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_dialogs_ai_sync_aliases")
            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_dialogs_au_sync_aliases")
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_dialogs_ai_sync_aliases
                    AFTER INSERT ON dialogs
                    BEGIN
                        UPDATE dialogs
                        SET dialog_id = NEW.opponent_key,
                            last_timestamp = NEW.last_message_timestamp,
                            is_request = CASE WHEN NEW.i_have_sent = 1 THEN 0 ELSE 1 END,
                            last_message_id = COALESCE((
                                SELECT m.message_id
                                FROM messages m
                                WHERE m.account = NEW.account
                                  AND m.dialog_key = CASE
                                      WHEN NEW.account = NEW.opponent_key THEN NEW.account
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'conversation:%' THEN NEW.opponent_key
                                      WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
                                      ELSE NEW.opponent_key || ':' || NEW.account
                                  END
                                ORDER BY m.timestamp DESC, m.message_id DESC
                                LIMIT 1
                            ), last_message_id),
                            last_message_attachments = COALESCE((
                                SELECT m.attachments
                                FROM messages m
                                WHERE m.account = NEW.account
                                  AND m.dialog_key = CASE
                                      WHEN NEW.account = NEW.opponent_key THEN NEW.account
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'conversation:%' THEN NEW.opponent_key
                                      WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
                                      ELSE NEW.opponent_key || ':' || NEW.account
                                  END
                                ORDER BY m.timestamp DESC, m.message_id DESC
                                LIMIT 1
                            ), '[]')
                        WHERE id = NEW.id;
                    END
                    """
            )
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_dialogs_au_sync_aliases
                    AFTER UPDATE OF opponent_key, last_message_timestamp, i_have_sent, account ON dialogs
                    BEGIN
                        UPDATE dialogs
                        SET dialog_id = NEW.opponent_key,
                            last_timestamp = NEW.last_message_timestamp,
                            is_request = CASE WHEN NEW.i_have_sent = 1 THEN 0 ELSE 1 END,
                            last_message_id = COALESCE((
                                SELECT m.message_id
                                FROM messages m
                                WHERE m.account = NEW.account
                                  AND m.dialog_key = CASE
                                      WHEN NEW.account = NEW.opponent_key THEN NEW.account
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'conversation:%' THEN NEW.opponent_key
                                      WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
                                      ELSE NEW.opponent_key || ':' || NEW.account
                                  END
                                ORDER BY m.timestamp DESC, m.message_id DESC
                                LIMIT 1
                            ), last_message_id),
                            last_message_attachments = COALESCE((
                                SELECT m.attachments
                                FROM messages m
                                WHERE m.account = NEW.account
                                  AND m.dialog_key = CASE
                                      WHEN NEW.account = NEW.opponent_key THEN NEW.account
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE '#group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'group:%' THEN NEW.opponent_key
                                      WHEN LOWER(TRIM(NEW.opponent_key)) LIKE 'conversation:%' THEN NEW.opponent_key
                                      WHEN NEW.account < NEW.opponent_key THEN NEW.account || ':' || NEW.opponent_key
                                      ELSE NEW.opponent_key || ':' || NEW.account
                                  END
                                ORDER BY m.timestamp DESC, m.message_id DESC
                                LIMIT 1
                            ), '[]')
                        WHERE id = NEW.id;
                    END
                    """
            )

            // MARK: - Desktop-only tables (for full schema parity surface)

            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS accounts (
                        public_key TEXT PRIMARY KEY,
                        private_key TEXT NOT NULL,
                        sfen TEXT NOT NULL
                    )
                    """
            )

            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS avatar_delivery (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        public_key TEXT NOT NULL,
                        account TEXT NOT NULL
                    )
                    """
            )

            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS cached_users (
                        public_key TEXT PRIMARY KEY,
                        title TEXT NOT NULL,
                        username TEXT NOT NULL,
                        verified INTEGER NOT NULL DEFAULT 0
                    )
                    """
            )

            // Android accounts table parity (iOS still uses Keychain as source of truth).
            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS encrypted_accounts (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        public_key TEXT NOT NULL,
                        private_key_encrypted TEXT NOT NULL,
                        seed_phrase_encrypted TEXT NOT NULL,
                        created_at TEXT NOT NULL,
                        last_used TEXT,
                        is_active INTEGER NOT NULL DEFAULT 1,
                        username TEXT
                    )
                    """
            )
            try db.execute(
                sql: """
                    CREATE UNIQUE INDEX IF NOT EXISTS index_encrypted_accounts_public_key
                    ON encrypted_accounts(public_key)
                    """
            )

            // Desktop shape compatibility (optional id while account remains canonical key).
            if try !hasColumn("accounts_sync_times", "id") {
                try db.execute(sql: "ALTER TABLE accounts_sync_times ADD COLUMN id INTEGER")
                try db.execute(
                    sql: """
                        UPDATE accounts_sync_times
                        SET id = rowid
                        WHERE id IS NULL OR id = 0
                        """
                )
            }
        }

        // v6: enforce bidirectional alias sync so canonical and iOS-native writes stay semantically identical.
        migrator.registerMigration("v6_bidirectional_alias_sync") { db in
            // MARK: - messages alias <-> native sync

            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_messages_ai_sync_aliases")
            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_messages_au_sync_aliases")
            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_messages_au_sync_native")

            // INSERT normalization: supports both native-first and canonical-first writes.
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_messages_ai_sync_aliases
                    AFTER INSERT ON messages
                    BEGIN
                        UPDATE messages
                        SET is_read = CASE
                                WHEN NEW.is_read != NEW.read THEN
                                    CASE WHEN NEW.read != 0 THEN NEW.read ELSE NEW.is_read END
                                ELSE NEW.is_read
                            END,
                            read = CASE
                                WHEN NEW.is_read != NEW.read THEN
                                    CASE WHEN NEW.read != 0 THEN NEW.read ELSE NEW.is_read END
                                ELSE NEW.read
                            END,
                            delivery_status = CASE
                                WHEN NEW.delivery_status = 0 AND NEW.delivered != 0 THEN NEW.delivered
                                ELSE NEW.delivery_status
                            END,
                            delivered = CASE
                                WHEN NEW.delivery_status = 0 AND NEW.delivered != 0 THEN NEW.delivered
                                ELSE NEW.delivery_status
                            END,
                            text = CASE
                                WHEN NEW.text = '' AND NEW.plain_message != '' THEN NEW.plain_message
                                ELSE NEW.text
                            END,
                            plain_message = CASE
                                WHEN NEW.plain_message = '' THEN NEW.text
                                ELSE NEW.plain_message
                            END
                        WHERE id = NEW.id;
                    END
                    """
            )

            // Native -> canonical sync.
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_messages_au_sync_aliases
                    AFTER UPDATE OF is_read, delivery_status, text ON messages
                    BEGIN
                        UPDATE messages
                        SET read = NEW.is_read,
                            delivered = NEW.delivery_status,
                            plain_message = CASE
                                WHEN NEW.plain_message = '' THEN NEW.text
                                ELSE NEW.plain_message
                            END
                        WHERE id = NEW.id
                          AND (read != NEW.is_read OR delivered != NEW.delivery_status OR plain_message != CASE
                                WHEN NEW.plain_message = '' THEN NEW.text
                                ELSE NEW.plain_message
                            END);
                    END
                    """
            )

            // Canonical -> native sync.
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_messages_au_sync_native
                    AFTER UPDATE OF read, delivered, plain_message ON messages
                    BEGIN
                        UPDATE messages
                        SET is_read = NEW.read,
                            delivery_status = NEW.delivered,
                            text = CASE
                                WHEN NEW.plain_message = '' THEN NEW.text
                                ELSE NEW.plain_message
                            END
                        WHERE id = NEW.id
                          AND (is_read != NEW.read OR delivery_status != NEW.delivered OR text != CASE
                                WHEN NEW.plain_message = '' THEN NEW.text
                                ELSE NEW.plain_message
                            END);
                    END
                    """
            )

            // MARK: - dialogs alias -> native compatibility mapping

            try db.execute(sql: "DROP TRIGGER IF EXISTS trg_dialogs_au_sync_native")
            try db.execute(
                sql: """
                    CREATE TRIGGER trg_dialogs_au_sync_native
                    AFTER UPDATE OF dialog_id, last_timestamp, is_request ON dialogs
                    BEGIN
                        UPDATE dialogs
                        SET opponent_key = CASE
                                WHEN NEW.dialog_id = '' THEN NEW.opponent_key
                                ELSE NEW.dialog_id
                            END,
                            last_message_timestamp = CASE
                                WHEN NEW.last_timestamp = 0 THEN NEW.last_message_timestamp
                                ELSE NEW.last_timestamp
                            END,
                            i_have_sent = CASE
                                WHEN NEW.is_request = 1 THEN 0
                                ELSE 1
                            END
                        WHERE id = NEW.id
                          AND (
                            opponent_key != CASE
                                WHEN NEW.dialog_id = '' THEN NEW.opponent_key
                                ELSE NEW.dialog_id
                            END
                            OR last_message_timestamp != CASE
                                WHEN NEW.last_timestamp = 0 THEN NEW.last_message_timestamp
                                ELSE NEW.last_timestamp
                            END
                            OR i_have_sent != CASE
                                WHEN NEW.is_request = 1 THEN 0
                                ELSE 1
                            END
                          );
                    END
                    """
            )
        }

        // v7: reconcile sync cursor data safely on all SQLite variants and add perf indexes.
        migrator.registerMigration("v7_sync_cursor_reconcile_and_perf_indexes") { db in
            func hasColumn(_ table: String, _ column: String) throws -> Bool {
                try db.columns(in: table).contains { $0.name == column }
            }

            try db.execute(
                sql: """
                    CREATE TABLE IF NOT EXISTS accounts_sync_times (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        account TEXT NOT NULL UNIQUE,
                        last_sync INTEGER NOT NULL
                    )
                    """
            )

            if try !hasColumn("accounts_sync_times", "id") {
                try db.execute(sql: "ALTER TABLE accounts_sync_times ADD COLUMN id INTEGER")
            }

            if try db.tableExists("sync_cursors") {
                try db.execute(
                    sql: """
                        INSERT OR IGNORE INTO accounts_sync_times (account, last_sync)
                        SELECT account, timestamp FROM sync_cursors
                        """
                )
                try db.execute(
                    sql: """
                        UPDATE accounts_sync_times
                        SET last_sync = CASE
                            WHEN COALESCE(
                                (SELECT s.timestamp FROM sync_cursors s WHERE s.account = accounts_sync_times.account),
                                last_sync
                            ) > last_sync
                            THEN (SELECT s.timestamp FROM sync_cursors s WHERE s.account = accounts_sync_times.account)
                            ELSE last_sync
                        END
                        WHERE account IN (SELECT account FROM sync_cursors)
                        """
                )
            }

            try db.execute(
                sql: """
                    UPDATE accounts_sync_times
                    SET id = rowid
                    WHERE id IS NULL OR id = 0
                    """
            )

            try db.execute(
                sql: """
                    CREATE INDEX IF NOT EXISTS idx_messages_account_dialog_fromme_isread
                    ON messages(account, dialog_key, from_me, is_read)
                    """
            )
            try db.execute(
                sql: """
                    CREATE INDEX IF NOT EXISTS idx_messages_account_dialog_fromme_timestamp
                    ON messages(account, dialog_key, from_me, timestamp)
                    """
            )
        }

        // MARK: v8 — last_message_sender_key (group chat preview: "Alice: hey")

        migrator.registerMigration("v8_last_message_sender_key") { db in
            let cols = Set(try db.columns(in: "dialogs").map(\.name))
            if !cols.contains("last_message_sender_key") {
                try db.execute(sql: "ALTER TABLE dialogs ADD COLUMN last_message_sender_key TEXT NOT NULL DEFAULT ''")
            }
        }

        // PERF: Composite index for sorted dialog queries (pinned first, then by timestamp).
        migrator.registerMigration("v9_perf_dialog_sort_index") { db in
            try db.execute(sql: """
                CREATE INDEX IF NOT EXISTS idx_dialogs_account_pinned_ts
                ON dialogs(account, is_pinned, last_message_timestamp)
            """)
        }

        try migrator.migrate(pool)
        dbPool = pool

        // Set iOS file protection on the database files
        let attrs: [FileAttributeKey: Any] = [
            .protectionKey: FileProtectionType.completeUntilFirstUserAuthentication
        ]
        try? FileManager.default.setAttributes(attrs, ofItemAtPath: dbURL.path)
        try? FileManager.default.setAttributes(attrs, ofItemAtPath: dbURL.path + "-wal")
        try? FileManager.default.setAttributes(attrs, ofItemAtPath: dbURL.path + "-shm")
    }

    func close() {
        dbPool = nil
        currentAccount = ""
    }

    // MARK: - Access

    /// Synchronous read on the current thread. Use for small queries from @MainActor.
    func read<T>(_ block: (Database) throws -> T) throws -> T {
        guard let pool = dbPool else {
            throw DatabaseError(message: "Database not open")
        }
        return try pool.read(block)
    }

    /// Write access (serialized on GRDB's writer queue).
    func write<T>(_ block: @Sendable @escaping (Database) throws -> T) async throws -> T {
        guard let pool = dbPool else {
            throw DatabaseError(message: "Database not open")
        }
        return try await pool.write(block)
    }

    /// Synchronous write for MainActor use (uses `writeWithoutTransaction` + manual transaction).
    func writeSync<T>(_ block: (Database) throws -> T) throws -> T {
        guard let pool = dbPool else {
            throw DatabaseError(message: "Database not open")
        }
        return try pool.write(block)
    }

    // MARK: - Database URL

    nonisolated private static func databaseURL(for accountPublicKey: String) -> URL {
        let fileManager = FileManager.default
        let baseURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
            ?? fileManager.urls(for: .documentDirectory, in: .userDomainMask).first
            ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)

        let dir = baseURL
            .appendingPathComponent("Rosetta", isDirectory: true)
            .appendingPathComponent("Database", isDirectory: true)

        let normalized = normalizedKey(accountPublicKey)
        return dir.appendingPathComponent("rosetta_\(normalized).sqlite")
    }

    /// Internal test-support accessor for deterministic DB path setup in unit tests.
    nonisolated static func databaseURLForTesting(accountPublicKey: String) -> URL {
        databaseURL(for: accountPublicKey)
    }

    nonisolated private static func normalizedKey(_ key: String) -> String {
        let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty { return "anonymous" }
        return String(trimmed.unicodeScalars.map { CharacterSet.alphanumerics.contains($0) ? Character($0) : "_" })
    }

    // MARK: - Dialog Key (Android parity)

    /// Returns true for group/conversation dialog identifiers that should not be pair-sorted.
    nonisolated static func isGroupDialogKey(_ value: String) -> Bool {
        let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        guard !normalized.isEmpty else { return false }
        return normalized.hasPrefix("#group:")
            || normalized.hasPrefix("group:")
            || normalized.hasPrefix("conversation:")
    }

    /// Compute dialog_key from account and opponent public keys.
    /// Android: `MessageRepository.getDialogKey()` — sorted pair for direct chats.
    nonisolated static func dialogKey(account: String, opponentKey: String) -> String {
        let normalizedAccount = account.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalizedOpponent = opponentKey.trimmingCharacters(in: .whitespacesAndNewlines)

        // Group/conversation dialogs keep stable server-provided identity.
        if isGroupDialogKey(normalizedOpponent) { return normalizedOpponent }
        // Saved Messages: dialogKey = account
        if normalizedAccount == normalizedOpponent { return normalizedAccount }
        // Normal: lexicographic sort
        return normalizedAccount < normalizedOpponent
            ? "\(normalizedAccount):\(normalizedOpponent)"
            : "\(normalizedOpponent):\(normalizedAccount)"
    }

    // MARK: - Sync Cursor (Android parity: SQLite, not UserDefaults)

    /// Load sync cursor for the current account. Returns milliseconds.
    func loadSyncCursor(account: String) -> Int64 {
        do {
            let stored = try read { db in
                if let lastSync = try Int64.fetchOne(
                    db,
                    sql: "SELECT last_sync FROM accounts_sync_times WHERE account = ?",
                    arguments: [account]
                ) {
                    return lastSync
                }

                if try db.tableExists("sync_cursors") {
                    return try Int64.fetchOne(
                        db,
                        sql: "SELECT timestamp FROM sync_cursors WHERE account = ?",
                        arguments: [account]
                    ) ?? 0
                }

                return 0
            }
            // Android parity: normalize seconds → milliseconds
            if stored > 0, stored < 1_000_000_000_000 {
                let corrected = stored * 1000
                try? writeSync { db in
                    try upsertSyncCursor(db: db, account: account, timestamp: corrected)
                }
                return corrected
            }
            return stored
        } catch { return 0 }
    }

    /// Deletes sync cursor, forcing next sync to start from timestamp=0 (full re-sync).
    /// Used for one-time recovery of orphaned group keys.
    func resetSyncCursor(account: String) {
        do {
            try writeSync { db in
                try db.execute(
                    sql: "DELETE FROM accounts_sync_times WHERE account = ?",
                    arguments: [account]
                )
            }
        } catch {}
    }

    /// Save sync cursor. Monotonic only — never decreases.
    func saveSyncCursor(account: String, timestamp: Int64) {
        guard timestamp > 0 else { return }
        let existing = loadSyncCursor(account: account)
        guard timestamp > existing else { return }
        do {
            try writeSync { db in
                try upsertSyncCursor(db: db, account: account, timestamp: timestamp)
            }
        } catch {}
    }

    private func upsertSyncCursor(db: Database, account: String, timestamp: Int64) throws {
        try db.execute(
            sql: """
                INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?)
                ON CONFLICT(account) DO UPDATE SET last_sync = excluded.last_sync
            """,
            arguments: [account, timestamp]
        )

        if try db.columns(in: "accounts_sync_times").contains(where: { $0.name == "id" }) {
            try db.execute(
                sql: """
                    UPDATE accounts_sync_times
                    SET id = rowid
                    WHERE account = ? AND (id IS NULL OR id = 0)
                    """,
                arguments: [account]
            )
        }

        if try db.tableExists("sync_cursors") {
            try db.execute(
                sql: """
                    INSERT INTO sync_cursors (account, timestamp) VALUES (?, ?)
                    ON CONFLICT(account) DO UPDATE SET timestamp = excluded.timestamp
                    """,
                arguments: [account, timestamp]
            )
        }
    }

    // MARK: - Delete

    /// Delete all data for the given account (used on account delete).
    func deleteDatabase(for accountPublicKey: String) {
        let url = Self.databaseURL(for: accountPublicKey)
        try? FileManager.default.removeItem(at: url)
        try? FileManager.default.removeItem(at: URL(fileURLWithPath: url.path + "-wal"))
        try? FileManager.default.removeItem(at: URL(fileURLWithPath: url.path + "-shm"))
    }
}

// GroupRepository is in Core/Data/Repositories/GroupRepository.swift
