//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import GRDB
import SignalServiceKit

final class FullTextSearchOptimizer {
    private let db: DB
    private let keyValueStore: KeyValueStore
    private let preconditions: Preconditions

    private enum Constants {
        static let numberOfPagesToMergeAtATime = 64

        static let nanosecondsBetweenMergeBatches = 50 * NSEC_PER_MSEC

        static let versionKey = "version"

        /// Incrementing this value will ensure the optimizer runs again for all users.
        static let currentVersion = 1
    }

    init(appContext: AppContext, db: DB, keyValueStoreFactory: KeyValueStoreFactory) {
        self.db = db
        self.keyValueStore = keyValueStoreFactory.keyValueStore(collection: "FullTextSearchOptimizer")
        self.preconditions = Preconditions([AppActivePrecondition(appContext: appContext)])
    }

    /// Optimizes a SQLite FTS5 table by issuing [merge commands][0] until no
    /// more merges are needed.
    ///
    /// To avoid hogging the database, a short delay is added between merges.
    ///
    /// [0]: https://www.sqlite.org/fts5.html#the_merge_command
    func run() async {
        do {
            let completedVersion = db.read { tx in
                keyValueStore.getInt(Constants.versionKey, defaultValue: 0, transaction: tx)
            }
            guard completedVersion < Constants.currentVersion else {
                return
            }
            try await performAllMerges()
            await db.awaitableWrite { tx in
                self.keyValueStore.setInt(Constants.currentVersion, key: Constants.versionKey, transaction: tx)
            }
        } catch {
            Logger.warn("\(error)")
        }
    }

    private func performAllMerges() async throws {
        var isFirstBatch = true
        while try await performMerge(isFirstBatch: isFirstBatch) {
            isFirstBatch = false
            try await Task.sleep(nanoseconds: Constants.nanosecondsBetweenMergeBatches)
        }
    }

    private func performMerge(isFirstBatch: Bool) async throws -> Bool {
        try await preconditions.waitUntilSatisfied()

        let backgroundTask = OWSBackgroundTask(label: #function)
        defer { backgroundTask.end() }

        let startTime = CACurrentMediaTime()

        let mergeResult = try await db.awaitableWrite { tx -> SqliteUtil.Fts5.MergeResult in
            return try SqliteUtil.Fts5.merge(
                db: SDSDB.shimOnlyBridge(tx).unwrapGrdbWrite.database,
                ftsTableName: FullTextSearchIndexer.ftsTableName,
                numberOfPages: Constants.numberOfPagesToMergeAtATime,
                isFirstBatch: isFirstBatch
            )
        }

        let formattedDuration = String(format: "%.1fms", (CACurrentMediaTime() - startTime)*1000)
        Logger.info("\(mergeResult) in \(formattedDuration)")

        return mergeResult == .workWasPerformed
    }
}
