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

import LibSignalClient

/// Represents a sync message, being sent from this device, related to "delete
/// for me" actions.
///
/// - SeeAlso ``DeleteForMeOutgoingSyncMessageManager``
@objc(DeleteForMeOutgoingSyncMessage)
class DeleteForMeOutgoingSyncMessage: OWSOutgoingSyncMessage {
    typealias Outgoing = DeleteForMeSyncMessage.Outgoing

    struct Contents: Codable {
        let messageDeletes: [Outgoing.MessageDeletes]
        let conversationDeletes: [Outgoing.ConversationDelete]
        let localOnlyConversationDelete: [Outgoing.LocalOnlyConversationDelete]

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMe {
            let protoBuilder = SSKProtoSyncMessageDeleteForMe.builder()
            protoBuilder.setMessageDeletes(messageDeletes.map { $0.asProto })
            protoBuilder.setConversationDeletes(conversationDeletes.map { $0.asProto })
            protoBuilder.setLocalOnlyConversationDeletes(localOnlyConversationDelete.map { $0.asProto })
            return protoBuilder.buildInfallibly()
        }
    }

    /// A JSON-serialized ``Contents`` struct.
    ///
    /// - Important: The ObjC name must not change, for Mantle compatibility.
    /// - Note
    /// Nullability is intentional, since Mantle will set this property via its
    /// reflection-based `init(coder:)` when we call `super.init(coder:)`.
    @objc(contents)
    private(set) var contentsData: Data!

    init?(
        contents: Contents,
        thread: TSThread,
        tx: SDSAnyReadTransaction
    ) {
        do {
            self.contentsData = try JSONEncoder().encode(contents)
        } catch {
            owsFailDebug("Failed to encode sync message contents!")
            return nil
        }

        super.init(thread: thread, transaction: tx)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    required init(dictionary dictionaryValue: [String: Any]!) throws {
        try super.init(dictionary: dictionaryValue)
    }

    override public var isUrgent: Bool { false }

    override public func syncMessageBuilder(transaction: SDSAnyReadTransaction) -> SSKProtoSyncMessageBuilder? {
        let contents: Contents
        do {
            contents = try JSONDecoder().decode(Contents.self, from: contentsData)
        } catch let error {
            owsFailDebug("Failed to decode serialized sync message contents! \(error)")
            return nil
        }

        let syncMessageBuilder = SSKProtoSyncMessage.builder()
        syncMessageBuilder.setDeleteForMe(contents.asProto)
        return syncMessageBuilder
    }
}

// MARK: -

extension DeleteForMeSyncMessage.Outgoing {
    enum ConversationIdentifier: Codable, Equatable {
        case threadServiceId(serviceId: String)
        case threadE164(e164: String)
        case threadGroupId(groupId: Data)

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeConversationIdentifier {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeConversationIdentifier.builder()
            switch self {
            case .threadServiceId(let serviceId): protoBuilder.setThreadServiceID(serviceId)
            case .threadE164(let e164): protoBuilder.setThreadE164(e164)
            case .threadGroupId(let groupId): protoBuilder.setThreadGroupID(groupId)
            }
            return protoBuilder.buildInfallibly()
        }
    }

    struct AddressableMessage: Codable, Equatable {
        enum Author: Codable, Equatable {
            /// The author's ACI. Note that the author of a message must be an
            /// ACI, never a PNI.
            case aci(aci: String)
            /// The author's E164, if their ACI is absent. This should only be
            /// relevant for old (pre-ACI) messages.
            case e164(e164: String)
        }

        let author: Author
        let sentTimestamp: UInt64

        private init(author: Author, sentTimestamp: UInt64) {
            self.author = author
            self.sentTimestamp = sentTimestamp
        }

        #if TESTABLE_BUILD
        static func forTests(author: Author, sentTimestamp: UInt64) -> AddressableMessage {
            return AddressableMessage(author: author, sentTimestamp: sentTimestamp)
        }
        #endif

        init?(incomingMessage: TSIncomingMessage) {
            if let authorAci = incomingMessage.authorAddress.aci {
                author = .aci(aci: authorAci.serviceIdUppercaseString)
            } else if let authorE164 = incomingMessage.authorAddress.e164 {
                author = .e164(e164: authorE164.stringValue)
            } else {
                return nil
            }

            sentTimestamp = incomingMessage.timestamp
        }

        init(outgoingMessage: TSOutgoingMessage, localIdentifiers: LocalIdentifiers) {
            author = .aci(aci: localIdentifiers.aci.serviceIdUppercaseString)
            sentTimestamp = outgoingMessage.timestamp
        }

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeAddressableMessage {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeAddressableMessage.builder()
            protoBuilder.setSentTimestamp(sentTimestamp)
            switch author {
            case .aci(let aci): protoBuilder.setAuthorServiceID(aci)
            case .e164(let e164): protoBuilder.setAuthorE164(e164)
            }
            return protoBuilder.buildInfallibly()
        }
    }

    struct MessageDeletes: Codable, Equatable {
        let conversationIdentifier: ConversationIdentifier
        let addressableMessages: [AddressableMessage]

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeMessageDeletes {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeMessageDeletes.builder()
            protoBuilder.setConversation(conversationIdentifier.asProto)
            protoBuilder.setMessages(addressableMessages.map { $0.asProto })
            return protoBuilder.buildInfallibly()
        }
    }

    struct ConversationDelete: Codable, Equatable {
        let conversationIdentifier: ConversationIdentifier
        let mostRecentAddressableMessages: [AddressableMessage]
        let isFullDelete: Bool

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeConversationDelete {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeConversationDelete.builder()
            protoBuilder.setConversation(conversationIdentifier.asProto)
            protoBuilder.setMostRecentMessages(mostRecentAddressableMessages.map { $0.asProto })
            protoBuilder.setIsFullDelete(isFullDelete)
            return protoBuilder.buildInfallibly()
        }
    }

    struct LocalOnlyConversationDelete: Codable, Equatable {
        let conversationIdentifier: ConversationIdentifier

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeLocalOnlyConversationDelete {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeLocalOnlyConversationDelete.builder()
            protoBuilder.setConversation(conversationIdentifier.asProto)
            return protoBuilder.buildInfallibly()
        }
    }
}
