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

import Foundation
import GRDB
import LibSignalClient

public enum EditMessageTarget {
    case outgoingMessage(OutgoingEditMessageWrapper)
    case incomingMessage(IncomingEditMessageWrapper)

    var wrapper: any EditMessageWrapper {
        switch self {
        case .outgoingMessage(let outgoingMessage):
            return outgoingMessage
        case .incomingMessage(let incomingMessage):
            return incomingMessage
        }
    }
}

public protocol EditMessageStore {

    // MARK: - Reads

    func editTarget(
        timestamp: UInt64,
        authorAci: Aci?,
        tx: DBReadTransaction
    ) -> EditMessageTarget?

    func findMessage(
        fromEdit edit: TSMessage,
        tx: DBReadTransaction
    ) -> TSMessage?

    func numberOfEdits(
        for message: TSMessage,
        tx: DBReadTransaction
    ) -> Int

    func findEditHistory(
        for message: TSMessage,
        tx: DBReadTransaction
    ) throws -> [(EditRecord, TSMessage?)]

    /// This method is similar to findEditHistory, but will find records and interactions where the
    /// passed in message is _either_ the latest edit, or a past revision.  This is useful when
    /// deleting a message, since the record needs to be removed regardles of the type of edit
    func findEditDeleteRecords(
        for message: TSMessage,
        tx: DBReadTransaction
    ) throws -> [(EditRecord, TSMessage?)]

    // MARK: - Writes

    func insert(
        _ editRecord: EditRecord,
        tx: DBWriteTransaction
    )

    func update(
        _ editRecord: EditRecord,
        tx: DBWriteTransaction
    ) throws
}

public class EditMessageStoreImpl: EditMessageStore {

    public init() {}

    public func editTarget(
        timestamp: UInt64,
        authorAci: Aci?,
        tx: DBReadTransaction
    ) -> EditMessageTarget? {
        guard SDS.fitsInInt64(timestamp) else {
            owsFailDebug("Received invalid timestamp!")
            return nil
        }

        let transaction = SDSDB.shimOnlyBridge(tx)

        let sql = """
            SELECT *
            FROM \(InteractionRecord.databaseTableName)
            WHERE \(interactionColumn: .timestamp) = ?
            AND \(interactionColumn: .authorUUID) IS ?
            LIMIT 1
        """
        let interaction = TSInteraction.grdbFetchOne(
            sql: sql,
            arguments: [timestamp, authorAci?.serviceIdUppercaseString],
            transaction: transaction.unwrapGrdbRead
        )
        switch (interaction, authorAci) {
        case (let outgoingMessage as TSOutgoingMessage, nil):
            guard let thread = outgoingMessage.thread(tx: transaction) else {
                Logger.warn("No thread for message")
                return nil
            }
            return .outgoingMessage(OutgoingEditMessageWrapper(
                message: outgoingMessage,
                thread: thread
            ))
        case (let incomingMessage as TSIncomingMessage, let authorAci?):
            guard let thread = incomingMessage.thread(tx: transaction) else {
                Logger.warn("No thread for message")
                return nil
            }
            return .incomingMessage(IncomingEditMessageWrapper(
                message: incomingMessage,
                thread: thread,
                authorAci: authorAci
            ))
        case (.some, _):
            Logger.warn("Unexpected message type found for edit")
            fallthrough
        default:
            return nil
        }
    }

    public func findMessage(
        fromEdit edit: TSMessage,
        tx: DBReadTransaction
    ) -> TSMessage? {
        let transaction = SDSDB.shimOnlyBridge(tx)

        let sql = """
                SELECT * FROM \(InteractionRecord.databaseTableName) AS interaction
                INNER JOIN \(EditRecord.databaseTableName) AS editRecord
                ON interaction.\(interactionColumn: .id) = editRecord.latestRevisionId
                WHERE editRecord.pastRevisionId = ?
                LIMIT 1
            """

        let arguments: StatementArguments = [edit.grdbId]
        return TSMessage.grdbFetchOne(
            sql: sql,
            arguments: arguments,
            transaction: transaction.unwrapGrdbRead
        ) as? TSMessage
    }

    public func numberOfEdits(
        for message: TSMessage,
        tx: DBReadTransaction
    ) -> Int {
        let transaction = SDSDB.shimOnlyBridge(tx)

        let sql = """
                SELECT COUNT(*)
                FROM \(EditRecord.databaseTableName)
                WHERE editRecord.latestRevisionId = ?
            """

        let arguments: StatementArguments = [message.grdbId]

        do {
            return try Int.fetchOne(
                transaction.unwrapGrdbRead.database,
                sql: sql,
                arguments: arguments
            ) ?? 0
        } catch {
            DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
                userDefaults: CurrentAppContext().appUserDefaults(),
                error: error
            )
            owsFail("Missing instance.")
        }
    }

    public func findEditHistory(
        for message: TSMessage,
        tx: DBReadTransaction
    ) throws -> [(EditRecord, TSMessage?)] {
        let transaction = SDSDB.shimOnlyBridge(tx)

        let recordSQL = """
            SELECT * FROM \(EditRecord.databaseTableName)
            WHERE latestRevisionId = ?
            ORDER BY pastRevisionId DESC
        """

        let arguments: StatementArguments = [message.grdbId]

        let records = try EditRecord.fetchAll(
            transaction.unwrapGrdbRead.database,
            sql: recordSQL,
            arguments: arguments
        )

        return records.map { record -> (EditRecord, TSMessage?) in
            let interaction = InteractionFinder.fetch(
                rowId: record.pastRevisionId,
                transaction: transaction
            )
            guard let message = interaction as? TSMessage else {
                owsFailDebug("Interaction has unexpected type: \(type(of: interaction))")
                return (record, nil)
            }
            return (record, message)
        }
    }

    /// This method is similar to findEditHistory, but will find records and interactions where the
    /// passed in message is _either_ the latest edit, or a past revision.  This is useful when
    /// deleting a message, since the record needs to be removed regardles of the type of edit
    public func findEditDeleteRecords(
        for message: TSMessage,
        tx: DBReadTransaction
    ) throws -> [(EditRecord, TSMessage?)] {
        let transaction = SDSDB.shimOnlyBridge(tx)

        let recordSQL = """
            SELECT * FROM \(EditRecord.databaseTableName)
            WHERE latestRevisionId = ?
            OR pastRevisionId = ?
            ORDER BY pastRevisionId DESC
        """

        let arguments: StatementArguments = [message.grdbId, message.grdbId]

        let records = try EditRecord.fetchAll(
            transaction.unwrapGrdbRead.database,
            sql: recordSQL,
            arguments: arguments
        )

        return records.map { record -> (EditRecord, TSMessage?) in
            let interaction = InteractionFinder.fetch(
                rowId: record.pastRevisionId,
                transaction: transaction
            )
            guard let message = interaction as? TSMessage else {
                owsFailDebug("Interaction has unexpected type: \(type(of: interaction))")
                return (record, nil)
            }
            return (record, message)
        }
    }

    public func insert(
        _ editRecord: EditRecord,
        tx: DBWriteTransaction
    ) {
        do {
            try editRecord.insert(SDSDB.shimOnlyBridge(tx).unwrapGrdbWrite.database)
        } catch {
            owsFailDebug("Unexpected edit record insertion error \(error)")
        }
    }

    public func update(
        _ editRecord: EditRecord,
        tx: DBWriteTransaction
    ) throws {
        try editRecord.update(SDSDB.shimOnlyBridge(tx).unwrapGrdbWrite.database)
    }
}
