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

import SignalCoreKit

extension MessageBackup {
    /// An identifier for the ``BackupProto.AccountData`` backup frame.
    ///
    /// Uses a singleton pattern, as there is only ever one account data frame
    /// in a backup.
    public struct AccountDataId: MessageBackupLoggableId {
        static let localUser = AccountDataId()

        private init() {}

        // MARK: MessageBackupLoggableId

        public var typeLogString: String { "BackupProto.AccountData" }
        public var idLogString: String { "localUser" }
    }

    public enum ArchiveAccountDataResult {
        case success
        case failure(ArchiveFrameError<AccountDataId>)
    }
}

/**
 * Archives the ``BackupProto.AccountData`` frame
 */
public protocol MessageBackupAccountDataArchiver: MessageBackupProtoArchiver {
    typealias RestoreFrameResult = MessageBackup.RestoreFrameResult<MessageBackup.AccountDataId>

    func archiveAccountData(
        stream: MessageBackupProtoOutputStream,
        tx: DBReadTransaction
    ) -> MessageBackup.ArchiveAccountDataResult

    func restore(
        _ accountData: BackupProto.AccountData,
        tx: DBWriteTransaction
    ) -> RestoreFrameResult
}

public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchiver {

    private let disappearingMessageConfigurationStore: DisappearingMessagesConfigurationStore
    private let localUsernameManager: LocalUsernameManager
    private let phoneNumberDiscoverabilityManager: PhoneNumberDiscoverabilityManager
    private let preferences: MessageBackup.AccountData.Shims.Preferences
    private let profileManager: MessageBackup.Shims.ProfileManager
    private let receiptManager: MessageBackup.AccountData.Shims.ReceiptManager
    private let reactionManager: MessageBackup.AccountData.Shims.ReactionManager
    private let sskPreferences: MessageBackup.AccountData.Shims.SSKPreferences
    private let subscriptionManager: MessageBackup.AccountData.Shims.SubscriptionManager
    private let storyManager: MessageBackup.AccountData.Shims.StoryManager
    private let systemStoryManager: MessageBackup.AccountData.Shims.SystemStoryManager
    private let typingIndicators: MessageBackup.AccountData.Shims.TypingIndicators
    private let udManager: MessageBackup.AccountData.Shims.UDManager
    private let usernameEducationManager: UsernameEducationManager

    public init(
        disappearingMessageConfigurationStore: DisappearingMessagesConfigurationStore,
        localUsernameManager: LocalUsernameManager,
        phoneNumberDiscoverabilityManager: PhoneNumberDiscoverabilityManager,
        preferences: MessageBackup.AccountData.Shims.Preferences,
        profileManager: MessageBackup.Shims.ProfileManager,
        receiptManager: MessageBackup.AccountData.Shims.ReceiptManager,
        reactionManager: MessageBackup.AccountData.Shims.ReactionManager,
        sskPreferences: MessageBackup.AccountData.Shims.SSKPreferences,
        subscriptionManager: MessageBackup.AccountData.Shims.SubscriptionManager,
        storyManager: MessageBackup.AccountData.Shims.StoryManager,
        systemStoryManager: MessageBackup.AccountData.Shims.SystemStoryManager,
        typingIndicators: MessageBackup.AccountData.Shims.TypingIndicators,
        udManager: MessageBackup.AccountData.Shims.UDManager,
        usernameEducationManager: UsernameEducationManager
    ) {
        self.disappearingMessageConfigurationStore = disappearingMessageConfigurationStore
        self.localUsernameManager = localUsernameManager
        self.phoneNumberDiscoverabilityManager = phoneNumberDiscoverabilityManager
        self.preferences = preferences
        self.receiptManager = receiptManager
        self.reactionManager = reactionManager
        self.sskPreferences = sskPreferences
        self.subscriptionManager = subscriptionManager
        self.storyManager = storyManager
        self.systemStoryManager = systemStoryManager
        self.typingIndicators = typingIndicators
        self.udManager = udManager
        self.usernameEducationManager = usernameEducationManager
        self.profileManager = profileManager
    }

    public func archiveAccountData(
        stream: MessageBackupProtoOutputStream,
        tx: DBReadTransaction
    ) -> MessageBackup.ArchiveAccountDataResult {

        guard let localProfile = profileManager.getUserProfileForLocalUser(tx: tx) else {
            return .failure(.archiveFrameError(.missingLocalProfile, .localUser))
        }
        guard let profileKeyData = localProfile.profileKey?.keyData else {
            return .failure(.archiveFrameError(.missingLocalProfileKey, .localUser))
        }

        var accountData = BackupProto.AccountData(
            profileKey: profileKeyData,
            givenName: localProfile.givenName ?? "",
            familyName: localProfile.familyName ?? "",
            avatarUrlPath: localProfile.avatarUrlPath ?? "",
            subscriberId: subscriptionManager.getSubscriberID(tx: tx) ?? .init(),
            subscriberCurrencyCode: subscriptionManager.getSubscriberCurrencyCode(tx: tx) ?? "",
            subscriptionManuallyCancelled: subscriptionManager.userManuallyCancelledSubscription(tx: tx)
        )

        if let result = buildUsernameLinkProto(tx: tx) {
            accountData.username = result.username
            accountData.usernameLink = result.usernameLink
        }

        accountData.accountSettings = buildAccountSettingsProto(tx: tx)

        let error = Self.writeFrameToStream(stream, objectId: MessageBackup.AccountDataId.localUser) {
            var frame = BackupProto.Frame()
            frame.item = .account(accountData)
            return frame
        }

        if let error {
            return .failure(error)
        } else {
            return .success
        }
    }

    private func buildUsernameLinkProto(tx: DBReadTransaction) -> (username: String, usernameLink: BackupProto.AccountData.UsernameLink)? {
        switch self.localUsernameManager.usernameState(tx: tx) {
        case .unset, .linkCorrupted, .usernameAndLinkCorrupted:
            return nil
        case .available(let username, let usernameLink):
            var usernameLink = BackupProto.AccountData.UsernameLink(
                entropy: usernameLink.entropy,
                serverId: usernameLink.handle.data
            )

            let linkColor = localUsernameManager.usernameLinkQRCodeColor(tx: tx)
            switch linkColor {
            case .unknown:
                usernameLink.color = .UNKNOWN
            case .blue, .grey, .green, .olive, .orange, .pink, .purple, .white:
                usernameLink.color = linkColor.backupProtoColor
            }
            return (username, usernameLink)
        }
    }

    private func buildAccountSettingsProto(tx: DBReadTransaction) -> BackupProto.AccountData.AccountSettings {

        // Fetch all the account settings
        let readReceipts = receiptManager.areReadReceiptsEnabled(tx: tx)
        let sealedSenderIndicators = preferences.shouldShowUnidentifiedDeliveryIndicators(tx: tx)
        let typingIndicatorsEnabled = typingIndicators.areTypingIndicatorsEnabled()
        let linkPreviews = sskPreferences.areLinkPreviewsEnabled(tx: tx)
        let notDiscoverableByPhoneNumber = {
            switch phoneNumberDiscoverabilityManager.phoneNumberDiscoverability(tx: tx) {
            case .everybody:
                return false
            case .nobody, .none:
                return true
            }
        }()
        let preferContactAvatars = sskPreferences.preferContactAvatars(tx: tx)
        let universalExpireTimer = disappearingMessageConfigurationStore.fetchOrBuildDefault(for: .universal, tx: tx).durationSeconds
        let displayBadgesOnProfile = subscriptionManager.displayBadgesOnProfile(tx: tx)
        let keepMutedChatsArchived = sskPreferences.shouldKeepMutedChatsArchived(tx: tx)
        let hasSetMyStoriesPrivacy = storyManager.hasSetMyStoriesPrivacy(tx: tx)
        let hasViewedOnboardingStory = systemStoryManager.isOnboardingStoryViewed(tx: tx)
        let storiesDisabled = storyManager.areStoriesEnabled(tx: tx).negated
        let hasSeenGroupStoryEducationSheet = systemStoryManager.hasSeenGroupStoryEducationSheet(tx: tx)
        let hasCompletedUsernameOnboarding = usernameEducationManager.shouldShowUsernameEducation(tx: tx).negated

        // Optional settings
        let preferredReactionEmoji = reactionManager.customEmojiSet(tx: tx)
        let storyViewReceiptsEnabled = storyManager.areViewReceiptsEnabled(tx: tx)
        let phoneNumberSharingMode: BackupProto.AccountData.PhoneNumberSharingMode? = {
            switch udManager.phoneNumberSharingMode(tx: tx) {
            case .everybody:
                return .EVERYBODY
            case .nobody:
                return .NOBODY
            case .none:
                return .UNKNOWN
            }
        }()

        // Populate the proto with the settings
        var accountSettings = BackupProto.AccountData.AccountSettings(
            readReceipts: readReceipts,
            sealedSenderIndicators: sealedSenderIndicators,
            typingIndicators: typingIndicatorsEnabled,
            linkPreviews: linkPreviews,
            notDiscoverableByPhoneNumber: notDiscoverableByPhoneNumber,
            preferContactAvatars: preferContactAvatars,
            universalExpireTimer: universalExpireTimer,
            displayBadgesOnProfile: displayBadgesOnProfile,
            keepMutedChatsArchived: keepMutedChatsArchived,
            hasSetMyStoriesPrivacy: hasSetMyStoriesPrivacy,
            hasViewedOnboardingStory: hasViewedOnboardingStory,
            storiesDisabled: storiesDisabled,
            hasSeenGroupStoryEducationSheet: hasSeenGroupStoryEducationSheet,
            hasCompletedUsernameOnboarding: hasCompletedUsernameOnboarding
        )

        if let preferredReactionEmoji {
            accountSettings.preferredReactionEmoji = preferredReactionEmoji
        }
        accountSettings.storyViewReceiptsEnabled = storyViewReceiptsEnabled
        accountSettings.phoneNumberSharingMode = phoneNumberSharingMode

        return accountSettings
    }

    public func restore(
        _ accountData: BackupProto.AccountData,
        tx: DBWriteTransaction
    ) -> RestoreFrameResult {
        guard let profileKey = OWSAES256Key(data: accountData.profileKey) else {
            return .failure([.restoreFrameError(
                .invalidProtoData(.invalidLocalProfileKey),
                .localUser
            )])
        }

        // Given name and profile key are required for the local profile. The
        // rest are optional.
        profileManager.insertLocalUserProfile(
            givenName: accountData.givenName,
            familyName: accountData.familyName.nilIfEmpty,
            avatarUrlPath: accountData.avatarUrlPath.nilIfEmpty,
            profileKey: profileKey,
            tx: tx
        )

        // Restore Subscription data, if nod a default value
        if accountData.subscriberId.count > 0, accountData.subscriberCurrencyCode.count > 0 {
            subscriptionManager.setSubscriberID(subscriberID: accountData.subscriberId, tx: tx)
            subscriptionManager.setSubscriberCurrencyCode(currencyCode: accountData.subscriberCurrencyCode, tx: tx)
        }
        subscriptionManager.setUserManuallyCancelledSubscription(value: accountData.subscriptionManuallyCancelled, tx: tx)

        // Restore local settings
        if let settings = accountData.accountSettings {
            receiptManager.setAreReadReceiptsEnabled(value: settings.readReceipts, tx: tx)
            preferences.setShouldShowUnidentifiedDeliveryIndicators(value: settings.sealedSenderIndicators, tx: tx)
            typingIndicators.setTypingIndicatorsEnabled(value: settings.typingIndicators, tx: tx)
            sskPreferences.setAreLinkPreviewsEnabled(value: settings.linkPreviews, tx: tx)
            phoneNumberDiscoverabilityManager.setPhoneNumberDiscoverability(
                settings.notDiscoverableByPhoneNumber ? .nobody : .everybody,
                updateAccountAttributes: false, // This should be updated later, similar to storage service
                updateStorageService: false,
                authedAccount: .implicit(),
                tx: tx
            )
            sskPreferences.setPreferContactAvatars(value: settings.preferContactAvatars, tx: tx)

            let token = DisappearingMessageToken(
                isEnabled: settings.universalExpireTimer > 0,
                durationSeconds: settings.universalExpireTimer
            )
            disappearingMessageConfigurationStore.set(token: token, for: .universal, tx: tx)

            subscriptionManager.setDisplayBadgesOnProfile(value: settings.displayBadgesOnProfile, tx: tx)
            sskPreferences.setShouldKeepMutedChatsArchived(value: settings.keepMutedChatsArchived, tx: tx)
            storyManager.setHasSetMyStoriesPrivacy(value: settings.hasSetMyStoriesPrivacy, tx: tx)
            systemStoryManager.setHasViewedOnboardingStory(value: settings.hasViewedOnboardingStory, tx: tx)
            storyManager.setAreStoriesEnabled(value: settings.storiesDisabled.negated, tx: tx)
            systemStoryManager.setHasSeenGroupStoryEducationSheet(value: settings.hasSeenGroupStoryEducationSheet, tx: tx)
            usernameEducationManager.setShouldShowUsernameEducation(settings.hasCompletedUsernameOnboarding.negated, tx: tx)

            if settings.preferredReactionEmoji.count > 0 {
                reactionManager.setCustomEmojiSet(emojis: settings.preferredReactionEmoji, tx: tx)
            }
            if let storyViewReceiptsEnabled = settings.storyViewReceiptsEnabled {
                storyManager.setAreViewReceiptsEnabled(value: storyViewReceiptsEnabled, tx: tx)
            }

            let phoneNumberSharingMode: PhoneNumberSharingMode = {
                switch settings.phoneNumberSharingMode {
                case .UNKNOWN, .none:
                    return .defaultValue
                case .EVERYBODY:
                    return .everybody
                case .NOBODY:
                    return .nobody
                }
            }()
            udManager.setPhoneNumberSharingMode(phoneNumberSharingMode, tx: tx)
        }

        // Restore username details (username, link, QR color)
        if let username = accountData.username, let usernameLink = accountData.usernameLink {
            if
                let handle = UUID(data: usernameLink.serverId),
                let linkData = Usernames.UsernameLink(handle: handle, entropy: usernameLink.entropy)
            {
                localUsernameManager.setLocalUsername(username: username, usernameLink: linkData, tx: tx)
            } else {
                return .failure([.restoreFrameError(.invalidProtoData(.invalidLocalUsernameLink), .localUser)])
            }
        }

        if let color = accountData.usernameLink?.color {
            localUsernameManager.setUsernameLinkQRCodeColor(color: color.qrCodeColor, tx: tx)
        }

        return .success
    }
}

private extension Usernames.QRCodeColor {
    var backupProtoColor: BackupProto.AccountData.UsernameLink.Color {
        switch self {
        case .blue: return .BLUE
        case .white: return .WHITE
        case .grey: return .GREY
        case .olive: return .OLIVE
        case .green: return .GREEN
        case .orange: return .ORANGE
        case .pink: return .PINK
        case .purple: return .PURPLE
        }
    }
}

private extension BackupProto.AccountData.UsernameLink.Color {
    var qrCodeColor: Usernames.QRCodeColor {
        switch self {
        case .BLUE: return .blue
        case .WHITE: return .white
        case .GREY: return .grey
        case .OLIVE: return .olive
        case .GREEN: return .green
        case .ORANGE: return .orange
        case .PINK: return .pink
        case .PURPLE: return .purple
        case .UNKNOWN: return .unknown
        }
    }
}
