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

import LibSignalClient
import SignalCoreKit

public enum IdentityManagerError: Error, IsRetryableProvider {
    case identityKeyMismatchForOutgoingMessage

    public var isRetryableProvider: Bool { false }
}

public protocol OWSIdentityManager {
    func libSignalStore(for identity: OWSIdentity, tx: DBReadTransaction) throws -> IdentityStore
    func groupContainsUnverifiedMember(_ groupUniqueID: String, tx: DBReadTransaction) -> Bool
    func fireIdentityStateChangeNotification(after tx: DBWriteTransaction)

    func recipientIdentity(for address: SignalServiceAddress, tx: DBReadTransaction) -> OWSRecipientIdentity?
    func recipientIdentity(for recipientId: AccountId, tx: DBReadTransaction) -> OWSRecipientIdentity?
    func removeRecipientIdentity(for recipientId: AccountId, tx: DBWriteTransaction)

    func identityKeyPair(for identity: OWSIdentity, tx: DBReadTransaction) -> ECKeyPair?
    func setIdentityKeyPair(_ keyPair: ECKeyPair?, for identity: OWSIdentity, tx: DBWriteTransaction)

    func identityKey(for address: SignalServiceAddress, tx: DBReadTransaction) -> Data?
    func identityKey(for serviceId: ServiceId, tx: DBReadTransaction) throws -> IdentityKey?

    @discardableResult
    func saveIdentityKey(_ identityKey: Data, for serviceId: ServiceId, tx: DBWriteTransaction) -> Result<Bool, RecipientIdError>

    func insertIdentityChangeInfoMessage(for serviceId: ServiceId, wasIdentityVerified: Bool, tx: DBWriteTransaction)
    func insertSessionSwitchoverEvent(for recipient: SignalRecipient, phoneNumber: String?, tx: DBWriteTransaction)
    func mergeRecipient(_ recipient: SignalRecipient, into targetRecipient: SignalRecipient, tx: DBWriteTransaction)

    func untrustedIdentityForSending(
        to address: SignalServiceAddress,
        untrustedThreshold: Date?,
        tx: DBReadTransaction
    ) -> OWSRecipientIdentity?

    func tryToSyncQueuedVerificationStates()

    func verificationState(for address: SignalServiceAddress, tx: DBReadTransaction) -> VerificationState
    func setVerificationState(
        _ verificationState: VerificationState,
        of identityKey: Data,
        for address: SignalServiceAddress,
        isUserInitiatedChange: Bool,
        tx: DBWriteTransaction
    ) -> ChangeVerificationStateResult

    func processIncomingVerifiedProto(_ verified: SSKProtoVerified, tx: DBWriteTransaction) throws

    func shouldSharePhoneNumber(with serviceId: ServiceId, tx: DBReadTransaction) -> Bool
    func setShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction)
    func clearShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction)
    func clearShouldSharePhoneNumberForEveryone(tx: DBWriteTransaction)

    func batchUpdateIdentityKeys(for serviceIds: [ServiceId]) -> Promise<Void>
}

extension OWSIdentityManager {
    @discardableResult
    public func saveIdentityKey(_ identityKey: IdentityKey, for serviceId: ServiceId, tx: DBWriteTransaction) -> Result<Bool, RecipientIdError> {
        return saveIdentityKey(identityKey.publicKey.keyBytes.asData, for: serviceId, tx: tx)
    }
}

public enum TSMessageDirection {
    case incoming
    case outgoing
}

public enum ChangeVerificationStateResult {
    case error
    case redundant
    case success
}

private extension TSMessageDirection {
    init(_ direction: Direction) {
        switch direction {
        case .receiving:
            self = .incoming
        case .sending:
            self = .outgoing
        }
    }
}

extension OWSIdentity: CustomStringConvertible {
    public var description: String {
        switch self {
        case .aci:
            return "ACI"
        case .pni:
            return "PNI"
        }
    }
}

public class IdentityStore: IdentityKeyStore {
    private let identityManager: OWSIdentityManagerImpl
    private let identityKeyPair: IdentityKeyPair
    private let fetchLocalRegistrationId: (DBWriteTransaction) -> UInt32

    fileprivate init(
        identityManager: OWSIdentityManagerImpl,
        identityKeyPair: IdentityKeyPair,
        fetchLocalRegistrationId: @escaping (DBWriteTransaction) -> UInt32
    ) {
        self.identityManager = identityManager
        self.identityKeyPair = identityKeyPair
        self.fetchLocalRegistrationId = fetchLocalRegistrationId
    }

    public func identityKeyPair(context: StoreContext) throws -> IdentityKeyPair {
        return identityKeyPair
    }

    public func localRegistrationId(context: StoreContext) throws -> UInt32 {
        return fetchLocalRegistrationId(context.asTransaction.asV2Write)
    }

    public func saveIdentity(
        _ identityKey: IdentityKey,
        for address: ProtocolAddress,
        context: StoreContext
    ) throws -> Bool {
        try identityManager.saveIdentityKey(
            identityKey,
            for: address.serviceId,
            tx: context.asTransaction.asV2Write
        ).get()
    }

    public func isTrustedIdentity(
        _ identityKey: IdentityKey,
        for address: ProtocolAddress,
        direction: Direction,
        context: StoreContext
    ) throws -> Bool {
        return try identityManager.isTrustedIdentityKey(
            identityKey,
            serviceId: address.serviceId,
            direction: TSMessageDirection(direction),
            tx: context.asTransaction.asV2Read
        )
    }

    public func identity(for address: ProtocolAddress, context: StoreContext) throws -> LibSignalClient.IdentityKey? {
        return try identityManager.identityKey(for: address.serviceId, tx: context.asTransaction.asV2Read)
    }
}

extension NSNotification.Name {
    // This notification will be fired whenever identities are created
    // or their verification state changes.
    public static let identityStateDidChange = Notification.Name("kNSNotificationNameIdentityStateDidChange")
}

extension OWSIdentityManagerImpl {
    public enum Constants {
        // The canonical key includes 32 bytes of identity material plus one byte specifying the key type
        static let identityKeyLength = 33

        // Cryptographic operations do not use the "type" byte of the identity key, so, for legacy reasons we store just
        // the identity material.
        fileprivate static let storedIdentityKeyLength = 32

        /// Don't trust an identity for sending to unless they've been around for at least this long.
        public static let defaultUntrustedInterval: TimeInterval = 5
    }
}

private extension OWSIdentity {
    var persistenceKey: String {
        switch self {
        case .aci:
            return "TSStorageManagerIdentityKeyStoreIdentityKey"
        case .pni:
            return "TSStorageManagerIdentityKeyStorePNIIdentityKey"
        }
    }
}

extension OWSIdentityManager {
    func generateNewIdentityKeyPair() -> ECKeyPair {
        ECKeyPair.generateKeyPair()
    }
}

public class OWSIdentityManagerImpl: OWSIdentityManager {
    private let aciProtocolStore: SignalProtocolStore
    private let db: DB
    private let messageSenderJobQueue: MessageSenderJobQueue
    private let networkManager: NetworkManager
    private let notificationPresenter: any NotificationPresenter
    private let ownIdentityKeyValueStore: KeyValueStore
    private let pniProtocolStore: SignalProtocolStore
    private let queuedVerificationStateSyncMessagesKeyValueStore: KeyValueStore
    private let recipientFetcher: RecipientFetcher
    private let recipientIdFinder: RecipientIdFinder
    private let schedulers: Schedulers
    private let shareMyPhoneNumberStore: KeyValueStore
    private let storageServiceManager: StorageServiceManager
    private let tsAccountManager: TSAccountManager

    public init(
        aciProtocolStore: SignalProtocolStore,
        db: DB,
        keyValueStoreFactory: KeyValueStoreFactory,
        messageSenderJobQueue: MessageSenderJobQueue,
        networkManager: NetworkManager,
        notificationPresenter: any NotificationPresenter,
        pniProtocolStore: SignalProtocolStore,
        recipientFetcher: RecipientFetcher,
        recipientIdFinder: RecipientIdFinder,
        schedulers: Schedulers,
        storageServiceManager: StorageServiceManager,
        tsAccountManager: TSAccountManager
    ) {
        self.aciProtocolStore = aciProtocolStore
        self.db = db
        self.messageSenderJobQueue = messageSenderJobQueue
        self.networkManager = networkManager
        self.notificationPresenter = notificationPresenter
        self.ownIdentityKeyValueStore = keyValueStoreFactory.keyValueStore(
            collection: "TSStorageManagerIdentityKeyStoreCollection"
        )
        self.pniProtocolStore = pniProtocolStore
        self.queuedVerificationStateSyncMessagesKeyValueStore = keyValueStoreFactory.keyValueStore(
            collection: "OWSIdentityManager_QueuedVerificationStateSyncMessages"
        )
        self.recipientFetcher = recipientFetcher
        self.recipientIdFinder = recipientIdFinder
        self.schedulers = schedulers
        self.shareMyPhoneNumberStore = keyValueStoreFactory.keyValueStore(
            collection: "OWSIdentityManager.shareMyPhoneNumberStore"
        )
        self.storageServiceManager = storageServiceManager
        self.tsAccountManager = tsAccountManager

        SwiftSingletons.register(self)
    }

    public func libSignalStore(for identity: OWSIdentity, tx: DBReadTransaction) throws -> IdentityStore {
        guard let identityKeyPair = self.identityKeyPair(for: identity, tx: tx) else {
            throw OWSAssertionError("no identity key pair for \(identity)")
        }
        return IdentityStore(
            identityManager: self,
            identityKeyPair: identityKeyPair.identityKeyPair,
            fetchLocalRegistrationId: { [tsAccountManager] in
                switch identity {
                case .aci:
                    return tsAccountManager.getOrGenerateAciRegistrationId(tx: $0)
                case .pni:
                    return tsAccountManager.getOrGeneratePniRegistrationId(tx: $0)
                }
            }
        )
    }

    public func groupContainsUnverifiedMember(_ groupUniqueID: String, tx: DBReadTransaction) -> Bool {
        return OWSRecipientIdentity.groupContainsUnverifiedMember(groupUniqueID, transaction: SDSDB.shimOnlyBridge(tx))
    }

    public func fireIdentityStateChangeNotification(after tx: DBWriteTransaction) {
        tx.addAsyncCompletion(on: schedulers.main) {
            NotificationCenter.default.post(name: .identityStateDidChange, object: nil)
        }
    }

    // MARK: - Fetching

    public func recipientIdentity(for address: SignalServiceAddress, tx: DBReadTransaction) -> OWSRecipientIdentity? {
        guard let recipientIdResult = recipientIdFinder.recipientId(for: address, tx: tx) else {
            return nil
        }
        switch recipientIdResult {
        case .failure(.mustNotUsePniBecauseAciExists):
            // If we pretend as though this identity doesn't exist, we'll get an error
            // when we try to send a message, we'll retry, and then we'll correctly
            // send to the ACI.
            return nil
        case .success(let recipientId):
            return recipientIdentity(for: recipientId, tx: tx)
        }
    }

    public func recipientIdentity(for recipientId: AccountId, tx: DBReadTransaction) -> OWSRecipientIdentity? {
        return OWSRecipientIdentity.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx))
    }

    public func removeRecipientIdentity(for recipientId: AccountId, tx: DBWriteTransaction) {
        recipientIdentity(for: recipientId, tx: tx)?.anyRemove(transaction: SDSDB.shimOnlyBridge(tx))
    }

    // MARK: - Local Identity

    public func identityKeyPair(for identity: OWSIdentity, tx: DBReadTransaction) -> ECKeyPair? {
        return ownIdentityKeyValueStore.getObject(forKey: identity.persistenceKey, transaction: tx) as? ECKeyPair
    }

    public func setIdentityKeyPair(_ keyPair: ECKeyPair?, for identity: OWSIdentity, tx: DBWriteTransaction) {
        // Under no circumstances may we *clear* our *ACI* identity key.
        owsAssert(keyPair != nil || identity != .aci)
        ownIdentityKeyValueStore.setObject(keyPair, key: identity.persistenceKey, transaction: tx)
    }

    // MARK: - Remote Identity Keys

    public func identityKey(for address: SignalServiceAddress, tx: DBReadTransaction) -> Data? {
        switch recipientIdFinder.recipientId(for: address, tx: tx) {
        case .none, .some(.failure(.mustNotUsePniBecauseAciExists)):
            return nil
        case .some(.success(let recipientId)):
            return _identityKey(for: recipientId, tx: tx)
        }
    }

    public func identityKey(for serviceId: ServiceId, tx: DBReadTransaction) throws -> IdentityKey? {
        guard let recipientIdResult = recipientIdFinder.recipientId(for: serviceId, tx: tx) else {
            return nil
        }
        guard let keyData = try _identityKey(for: recipientIdResult.get(), tx: tx) else { return nil }
        return try IdentityKey(publicKey: PublicKey(keyData: keyData))
    }

    private func _identityKey(for recipientId: AccountId, tx: DBReadTransaction) -> Data? {
        owsAssertDebug(!recipientId.isEmpty)
        return OWSRecipientIdentity.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx))?.identityKey
    }

    @discardableResult
    public func saveIdentityKey(_ identityKey: Data, for serviceId: ServiceId, tx: DBWriteTransaction) -> Result<Bool, RecipientIdError> {
        let recipientIdResult = recipientIdFinder.ensureRecipientId(for: serviceId, tx: tx)
        return recipientIdResult.map({ _saveIdentityKey(identityKey, for: serviceId, recipientId: $0, tx: tx) })
    }

    private func _saveIdentityKey(_ identityKey: Data, for serviceId: ServiceId, recipientId: AccountId, tx: DBWriteTransaction) -> Bool {
        owsAssertDebug(identityKey.count == Constants.storedIdentityKeyLength)

        let existingIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx))
        guard let existingIdentity else {
            Logger.info("Saving first-use identity for \(serviceId)")
            OWSRecipientIdentity(
                accountId: recipientId,
                identityKey: identityKey,
                isFirstKnownKey: true,
                createdAt: Date(),
                verificationState: .default
            ).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
            // Cancel any pending verification state sync messages for this recipient.
            clearSyncMessage(for: recipientId, tx: tx)
            fireIdentityStateChangeNotification(after: tx)
            storageServiceManager.recordPendingUpdates(updatedAccountIds: [recipientId])
            return false
        }

        guard existingIdentity.identityKey != identityKey else {
            return false
        }

        let verificationState: VerificationState
        switch VerificationState(existingIdentity.verificationState) {
        case .implicit(isAcknowledged: _):
            verificationState = .implicit(isAcknowledged: false)
        case .verified, .noLongerVerified:
            verificationState = .noLongerVerified
        }
        Logger.info("Saving new identity for \(serviceId): \(existingIdentity.verificationState) -> \(verificationState)")
        insertIdentityChangeInfoMessage(for: serviceId, wasIdentityVerified: existingIdentity.wasIdentityVerified, tx: tx)
        OWSRecipientIdentity(
            accountId: recipientId,
            identityKey: identityKey,
            isFirstKnownKey: false,
            createdAt: Date(),
            verificationState: verificationState.rawValue
        ).anyUpsert(transaction: SDSDB.shimOnlyBridge(tx))
        aciProtocolStore.sessionStore.archiveAllSessions(for: serviceId, tx: tx)
        // Cancel any pending verification state sync messages for this recipient.
        clearSyncMessage(for: recipientId, tx: tx)
        storageServiceManager.recordPendingUpdates(updatedAccountIds: [recipientId])
        return true
    }

    public func insertIdentityChangeInfoMessage(
        for serviceId: ServiceId,
        wasIdentityVerified: Bool,
        tx: DBWriteTransaction
    ) {
        let contactThread = TSContactThread.getOrCreateThread(
            withContactAddress: SignalServiceAddress(serviceId),
            transaction: SDSDB.shimOnlyBridge(tx)
        )
        let contactThreadMessage = TSErrorMessage.nonblockingIdentityChange(
            in: contactThread,
            address: SignalServiceAddress(serviceId),
            wasIdentityVerified: wasIdentityVerified
        )
        contactThreadMessage.anyInsert(transaction: SDSDB.shimOnlyBridge(tx))

        for groupThread in TSGroupThread.groupThreads(with: SignalServiceAddress(serviceId), transaction: SDSDB.shimOnlyBridge(tx)) {
            TSErrorMessage.nonblockingIdentityChange(
                in: groupThread,
                address: SignalServiceAddress(serviceId),
                wasIdentityVerified: wasIdentityVerified
            ).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
        }

        notificationPresenter.notifyUser(forErrorMessage: contactThreadMessage, thread: contactThread, transaction: SDSDB.shimOnlyBridge(tx))
        fireIdentityStateChangeNotification(after: tx)
    }

    public func insertSessionSwitchoverEvent(
        for recipient: SignalRecipient,
        phoneNumber: String?,
        tx: DBWriteTransaction
    ) {
        let tx = SDSDB.shimOnlyBridge(tx)
        guard let contactThread = TSContactThread.getWithContactAddress(recipient.address, transaction: tx) else {
            return
        }
        var userInfo = [InfoMessageUserInfoKey: Any]()
        if let phoneNumber {
            userInfo[.sessionSwitchoverPhoneNumber] = phoneNumber
        }
        let sessionSwitchoverEvent = TSInfoMessage(
            thread: contactThread,
            messageType: .sessionSwitchover,
            infoMessageUserInfo: userInfo
        )
        sessionSwitchoverEvent.anyInsert(transaction: tx)
    }

    public func mergeRecipient(_ recipient: SignalRecipient, into targetRecipient: SignalRecipient, tx: DBWriteTransaction) {
        let recipientPair = MergePair(fromValue: recipient, intoValue: targetRecipient)
        let recipientIdentity = recipientPair.map {
            OWSRecipientIdentity.anyFetch(uniqueId: $0.uniqueId, transaction: SDSDB.shimOnlyBridge(tx))
        }
        guard let fromValue = recipientIdentity.fromValue else {
            return
        }
        if recipientIdentity.intoValue == nil {
            OWSRecipientIdentity(
                accountId: targetRecipient.uniqueId,
                identityKey: fromValue.identityKey,
                isFirstKnownKey: fromValue.isFirstKnownKey,
                createdAt: fromValue.createdAt,
                verificationState: fromValue.verificationState
            ).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
        }
        fromValue.anyRemove(transaction: SDSDB.shimOnlyBridge(tx))
    }

    // MARK: - Trust

    public func untrustedIdentityForSending(
        to address: SignalServiceAddress,
        untrustedThreshold: Date?,
        tx: DBReadTransaction
    ) -> OWSRecipientIdentity? {
        let recipientIdentity = recipientIdentity(for: address, tx: tx)
        let isTrusted = isIdentityKeyTrustedForSending(
            address: address,
            recipientIdentity: recipientIdentity,
            untrustedThreshold: untrustedThreshold,
            tx: tx
        )
        return isTrusted ? nil : recipientIdentity
    }

    private func isIdentityKeyTrustedForSending(
        address: SignalServiceAddress,
        recipientIdentity: OWSRecipientIdentity?,
        untrustedThreshold: Date?,
        tx: DBReadTransaction
    ) -> Bool {
        owsAssertDebug(address.isValid)

        if address.isLocalAddress {
            guard let recipientIdentity else {
                // Trust on first use.
                return true
            }
            return isTrustedLocalKey(recipientIdentity.identityKey, tx: tx)
        }

        return canSend(to: recipientIdentity, untrustedThreshold: untrustedThreshold)
    }

    func isTrustedIdentityKey(
        _ identityKey: IdentityKey,
        serviceId: ServiceId,
        direction: TSMessageDirection,
        tx: DBReadTransaction
    ) throws -> Bool {
        let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx)
        if localIdentifiers?.aci == serviceId {
            return isTrustedLocalKey(identityKey.publicKey.keyBytes.asData, tx: tx)
        }

        switch direction {
        case .incoming:
            return true
        case .outgoing:
            guard let recipientId = try recipientIdFinder.recipientId(for: serviceId, tx: tx)?.get() else {
                owsFailDebug("Couldn't find recipientId for outgoing message.")
                return false
            }
            let recipientIdentity = OWSRecipientIdentity.anyFetch(
                uniqueId: recipientId,
                transaction: SDSDB.shimOnlyBridge(tx)
            )
            if let recipientIdentity, recipientIdentity.identityKey != identityKey.publicKey.keyBytes.asData {
                Logger.warn("Key mismatch for \(serviceId)")
                throw IdentityManagerError.identityKeyMismatchForOutgoingMessage
            }
            return canSend(to: recipientIdentity, untrustedThreshold: nil)
        }
    }

    private func isTrustedLocalKey(_ identityKey: Data, tx: DBReadTransaction) -> Bool {
        let localIdentityKeyPair = identityKeyPair(for: .aci, tx: tx)
        guard localIdentityKeyPair?.publicKey == identityKey else {
            owsFailDebug("Wrong identity key for local account.")
            return false
        }
        return true
    }

    private func canSend(to recipientIdentity: OWSRecipientIdentity?, untrustedThreshold: Date?) -> Bool {
        guard let recipientIdentity else {
            // Trust on first use.
            return true
        }

        if recipientIdentity.isFirstKnownKey {
            return true
        }

        switch recipientIdentity.verificationState {
        case .default:
            // This user has never been explicitly verified, but we still want to check
            // if the identity key is one we newly learned about to give the local user
            // time to ensure they wish to send. If it has been created in the last N
            // seconds, we'll treat it as untrusted so sends fail. This is a best
            // effort, and we'll continue to allow sending to the user after the "new"
            // window elapses without any explicit action from the local user.
            let untrustedThreshold = untrustedThreshold ?? Date(timeIntervalSinceNow: -Constants.defaultUntrustedInterval)
            guard recipientIdentity.createdAt <= untrustedThreshold else {
                Logger.warn("Not trusting new identity for \(recipientIdentity.accountId)")
                return false
            }
            return true
        case .defaultAcknowledged:
            return true
        case .verified:
            return true
        case .noLongerVerified:
            // This user was previously verified and their key has changed. We will not trust
            // them again until the user explicitly acknowledges the key change.
            Logger.warn("Not trusting no-longer-verified identity for \(recipientIdentity.accountId)")
            return false
        }
    }

    // MARK: - Sync Messages

    private func enqueueSyncMessage(for recipientId: AccountId, tx: DBWriteTransaction) {
        queuedVerificationStateSyncMessagesKeyValueStore.setObject(true, key: recipientId, transaction: tx)
        schedulers.main.async { self.tryToSyncQueuedVerificationStates() }
    }

    private func clearSyncMessage(for key: String, tx: DBWriteTransaction) {
        queuedVerificationStateSyncMessagesKeyValueStore.setObject(nil, key: key, transaction: tx)
    }

    public func tryToSyncQueuedVerificationStates() {
        AssertIsOnMainThread()
        AppReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
            self.schedulers.global().async { self.syncQueuedVerificationStates() }
        }
    }

    private func syncQueuedVerificationStates() {
        guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
            return
        }
        guard let thread = TSContactThread.getOrCreateLocalThreadWithSneakyTransaction() else {
            owsFailDebug("Missing thread.")
            return
        }
        let allKeys = db.read { tx in queuedVerificationStateSyncMessagesKeyValueStore.allKeys(transaction: tx) }
        // We expect very few keys in practice, and each key triggers multiple
        // database write transactions. If we do end up with thousands of keys,
        // using a separate transaction avoids long blocks.
        for key in allKeys {
            let syncMessage = db.write { (tx) -> OWSVerificationStateSyncMessage? in
                guard let syncMessage = buildVerificationStateSyncMessage(for: key, localThread: thread, tx: tx) else {
                    queuedVerificationStateSyncMessagesKeyValueStore.removeValue(forKey: key, transaction: tx)
                    return nil
                }
                return syncMessage
            }
            guard let syncMessage else {
                continue
            }
            sendVerificationStateSyncMessage(for: key, message: syncMessage)
        }
    }

    private func buildVerificationStateSyncMessage(
        for key: String,
        localThread: TSThread,
        tx: DBReadTransaction
    ) -> OWSVerificationStateSyncMessage? {
        guard let value = queuedVerificationStateSyncMessagesKeyValueStore.getObject(forKey: key, transaction: tx) else {
            return nil
        }
        let recipientId: AccountId
        switch value {
        case let value as Bool:
            guard value else {
                return nil
            }
            recipientId = key
        case is SignalServiceAddress:
            recipientId = key
        case let value as String:
            // Previously, we stored phone numbers in this KV store.
            let address = SignalServiceAddress.legacyAddress(serviceId: nil, phoneNumber: value)
            guard let accountId_ = try? recipientIdFinder.recipientId(for: address, tx: tx)?.get() else {
                return nil
            }
            recipientId = accountId_
        default:
            owsFailDebug("Invalid object: \(type(of: value))")
            return nil
        }

        if recipientId.isEmpty {
            return nil
        }

        let recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx))

        guard let recipientIdentity else {
            owsFailDebug("Couldn't load recipient identity for \(recipientId)")
            return nil
        }

        guard let identityKey = try? recipientIdentity.identityKeyObject else {
            owsFailDebug("Invalid recipient identity key for \(recipientId)")
            return nil
        }

        // We don't want to sync "no longer verified" state. Other
        // clients can figure this out from the /profile/ endpoint, and
        // this can cause data loss as a user's devices overwrite each
        // other's verification.
        if recipientIdentity.verificationState == .noLongerVerified {
            owsFailDebug("Queue verification state is invalid for \(recipientId)")
            return nil
        }

        guard let recipient = SignalRecipient.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx)) else {
            return nil
        }

        return OWSVerificationStateSyncMessage(
            thread: localThread,
            verificationState: recipientIdentity.verificationState,
            identityKey: identityKey.serialize().asData,
            verificationForRecipientAddress: recipient.address,
            transaction: SDSDB.shimOnlyBridge(tx)
        )
    }

    private func sendVerificationStateSyncMessage(for recipientId: AccountId, message: OWSVerificationStateSyncMessage) {
        let address = message.verificationForRecipientAddress
        let contactThread = TSContactThread.getOrCreateThread(contactAddress: address)

        // DURABLE CLEANUP - we could replace the custom durability logic in this class
        // with a durable JobQueue.
        let nullMessagePromise = db.write { tx in
            // Send null message to appear as though we're sending a normal message to cover the sync message sent
            // subsequently
            let nullMessage = OWSOutgoingNullMessage(
                contactThread: contactThread,
                verificationStateSyncMessage: message,
                transaction: SDSDB.shimOnlyBridge(tx)
            )
            let preparedMessage = PreparedOutgoingMessage.preprepared(
                transientMessageWithoutAttachments: nullMessage
            )
            return messageSenderJobQueue.add(
                .promise,
                message: preparedMessage,
                limitToCurrentProcessLifetime: true,
                transaction: SDSDB.shimOnlyBridge(tx)
            )
        }

        nullMessagePromise.done(on: schedulers.global()) {
            Logger.info("Successfully sent verification state NullMessage")
            let syncMessagePromise = self.db.write { tx in
                let preparedMessage = PreparedOutgoingMessage.preprepared(
                    transientMessageWithoutAttachments: message
                )
                return self.messageSenderJobQueue.add(
                    .promise,
                    message: preparedMessage,
                    limitToCurrentProcessLifetime: true,
                    transaction: SDSDB.shimOnlyBridge(tx)
                )
            }
            syncMessagePromise.done(on: self.schedulers.global()) {
                Logger.info("Successfully sent verification state sync message")
                self.db.write { tx in self.clearSyncMessage(for: recipientId, tx: tx) }
            }.catch(on: self.schedulers.global()) { error in
                Logger.error("Failed to send verification state sync message: \(error)")
            }
        }.catch(on: schedulers.global()) { error in
            Logger.error("Failed to send verification state NullMessage: \(error)")
            if error is MessageSenderNoSuchSignalRecipientError {
                Logger.info("Removing retries for syncing verification for unregistered user: \(address)")
                self.db.write { tx in self.clearSyncMessage(for: recipientId, tx: tx) }
            }
        }
    }

    // MARK: - Verification

    public func verificationState(for address: SignalServiceAddress, tx: DBReadTransaction) -> VerificationState {
        return VerificationState(recipientIdentity(for: address, tx: tx)?.verificationState ?? .default)
    }

    public func setVerificationState(
        _ verificationState: VerificationState,
        of identityKey: Data,
        for address: SignalServiceAddress,
        isUserInitiatedChange: Bool,
        tx: DBWriteTransaction
    ) -> ChangeVerificationStateResult {
        owsAssertDebug(identityKey.count == Constants.storedIdentityKeyLength)

        let recipient = OWSAccountIdFinder.ensureRecipient(forAddress: address, transaction: SDSDB.shimOnlyBridge(tx))
        let recipientId = recipient.uniqueId
        let recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx))
        guard let recipientIdentity else {
            owsFailDebug("Missing OWSRecipientIdentity.")
            return .error
        }

        guard recipientIdentity.identityKey == identityKey else {
            Logger.warn("Can't change verification state for outdated identity key")
            return .error
        }

        let oldVerificationState = VerificationState(recipientIdentity.verificationState)
        if oldVerificationState == verificationState {
            return .redundant
        }

        // If we're sending to a Pni, the identity key might change. If that
        // happens, we can acknowledge it, but we can't mark it as verified.
        if recipient.pni != nil, recipient.aciString == nil {
            switch verificationState {
            case .verified, .noLongerVerified, .implicit(isAcknowledged: false):
                owsFailDebug("Can't mark Pni recipient as verified/no longer verified.")
                return .error
            case .implicit(isAcknowledged: true):
                break
            }
        }

        Logger.info("setVerificationState for \(recipientId): \(recipientIdentity.verificationState) -> \(verificationState)")
        recipientIdentity.update(with: verificationState.rawValue, transaction: SDSDB.shimOnlyBridge(tx))

        switch (oldVerificationState, verificationState) {
        case (.implicit, .implicit):
            // We're only changing `isAcknowledged`, and that doesn't impact Storage
            // Service, sync messages, or chat events.
            break
        default:
            if isUserInitiatedChange {
                saveChangeMessages(for: recipient, verificationState: verificationState, isLocalChange: true, tx: tx)
                enqueueSyncMessage(for: recipientId, tx: tx)
            } else {
                // Cancel any pending verification state sync messages for this recipient.
                clearSyncMessage(for: recipientId, tx: tx)
            }
            // Verification state has changed, so notify storage service.
            storageServiceManager.recordPendingUpdates(updatedAccountIds: [recipientId])
        }
        fireIdentityStateChangeNotification(after: tx)
        return .success
    }

    // MARK: - Verified

    public func processIncomingVerifiedProto(_ verified: SSKProtoVerified, tx: DBWriteTransaction) throws {
        guard let aci = Aci.parseFrom(aciString: verified.destinationAci) else {
            return owsFailDebug("Verification state sync message missing destination.")
        }
        Logger.info("Received verification state message for \(aci)")
        guard let rawIdentityKey = verified.identityKey else {
            return owsFailDebug("Verification state sync message for \(aci) with malformed identityKey")
        }
        let identityKey = try IdentityKey(bytes: rawIdentityKey)

        switch verified.state {
        case .default:
            applyVerificationStateAction(
                .clearVerification,
                aci: aci,
                identityKey: identityKey,
                overwriteOnConflict: false,
                tx: tx
            )
        case .verified:
            applyVerificationStateAction(
                .markVerified,
                aci: aci,
                identityKey: identityKey,
                overwriteOnConflict: true,
                tx: tx
            )
        case .unverified:
            return owsFailDebug("Verification state sync message for \(aci) has unverified state")
        case .none:
            return owsFailDebug("Verification state sync message for \(aci) has no state")
        }
    }

    private enum VerificationStateAction {
        case markVerified
        case clearVerification
    }

    private func applyVerificationStateAction(
        _ verificationStateAction: VerificationStateAction,
        aci: Aci,
        identityKey: IdentityKey,
        overwriteOnConflict: Bool,
        tx: DBWriteTransaction
    ) {
        let recipient = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx)
        let recipientId = recipient.uniqueId
        var recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx))

        let shouldSaveIdentityKey: Bool
        let shouldInsertChangeMessages: Bool

        if let recipientIdentity {
            if recipientIdentity.accountId != recipientId {
                return owsFailDebug("Unexpected recipientId for \(aci)")
            }
            let didChangeIdentityKey = recipientIdentity.identityKey != identityKey.publicKey.keyBytes.asData
            if didChangeIdentityKey, !overwriteOnConflict {
                // The conflict case where we receive a verification sync message whose
                // identity key disagrees with the local identity key for this recipient.
                Logger.warn("Non-matching identityKey for \(aci)")
                return
            }
            shouldSaveIdentityKey = didChangeIdentityKey
            shouldInsertChangeMessages = true
        } else if verificationStateAction == .clearVerification {
            // There's no point in creating a new recipient identity just to set its
            // verification state to default.
            return
        } else {
            shouldSaveIdentityKey = true
            shouldInsertChangeMessages = false
        }

        if shouldSaveIdentityKey {
            // Ensure a remote identity exists for this key. We may be learning about
            // it for the first time.
            saveIdentityKey(identityKey, for: aci, tx: tx)
            recipientIdentity = OWSRecipientIdentity.anyFetch(uniqueId: recipientId, transaction: SDSDB.shimOnlyBridge(tx))
        }

        guard let recipientIdentity else {
            return owsFailDebug("Missing expected identity for \(aci)")
        }
        guard recipientIdentity.accountId == recipientId else {
            return owsFailDebug("Unexpected recipientId for \(aci)")
        }
        guard recipientIdentity.identityKey == identityKey.publicKey.keyBytes.asData else {
            return owsFailDebug("Unexpected identityKey for \(aci)")
        }

        let oldVerificationState: VerificationState = VerificationState(recipientIdentity.verificationState)
        let newVerificationState: VerificationState

        switch verificationStateAction {
        case .markVerified:
            switch oldVerificationState {
            case .verified:
                return
            case .noLongerVerified, .implicit(isAcknowledged: _):
                newVerificationState = .verified
            }
        case .clearVerification:
            switch oldVerificationState {
            case .implicit:
                return  // We can keep any implicit state.
            case .verified, .noLongerVerified:
                newVerificationState = .implicit(isAcknowledged: false)
            }
        }

        Logger.info("for \(aci): \(oldVerificationState) -> \(newVerificationState)")
        recipientIdentity.update(with: newVerificationState.rawValue, transaction: SDSDB.shimOnlyBridge(tx))

        if shouldInsertChangeMessages {
            saveChangeMessages(for: recipient, verificationState: newVerificationState, isLocalChange: false, tx: tx)
        }
    }

    private func saveChangeMessages(
        for signalRecipient: SignalRecipient,
        verificationState: VerificationState,
        isLocalChange: Bool,
        tx: DBWriteTransaction
    ) {
        let address = signalRecipient.address

        var relevantThreads = [TSThread]()
        relevantThreads.append(TSContactThread.getOrCreateThread(withContactAddress: address, transaction: SDSDB.shimOnlyBridge(tx)))
        relevantThreads.append(contentsOf: TSGroupThread.groupThreads(with: address, transaction: SDSDB.shimOnlyBridge(tx)))

        for thread in relevantThreads {
            OWSVerificationStateChangeMessage(
                thread: thread,
                recipientAddress: address,
                verificationState: verificationState.rawValue,
                isLocalChange: isLocalChange
            ).anyInsert(transaction: SDSDB.shimOnlyBridge(tx))
        }
    }

    // MARK: - Phone Number Sharing

    public func shouldSharePhoneNumber(with recipient: ServiceId, tx: DBReadTransaction) -> Bool {
        guard let recipient = recipient as? Aci else {
            return false
        }
        let aciString = recipient.serviceIdUppercaseString
        return shareMyPhoneNumberStore.getBool(aciString, defaultValue: false, transaction: tx)
    }

    public func setShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction) {
        let aciString = recipient.serviceIdUppercaseString
        shareMyPhoneNumberStore.setBool(true, key: aciString, transaction: tx)
    }

    public func clearShouldSharePhoneNumber(with recipient: Aci, tx: DBWriteTransaction) {
        let aciString = recipient.serviceIdUppercaseString
        shareMyPhoneNumberStore.removeValue(forKey: aciString, transaction: tx)
    }

    public func clearShouldSharePhoneNumberForEveryone(tx: DBWriteTransaction) {
        shareMyPhoneNumberStore.removeAll(transaction: tx)
    }

    // MARK: - Batch Identity Lookup

    public func batchUpdateIdentityKeys(for serviceIds: [ServiceId]) -> Promise<Void> {
        if serviceIds.isEmpty { return .value(()) }

        let serviceIds = Set(serviceIds)
        let batchServiceIds = serviceIds.prefix(OWSRequestFactory.batchIdentityCheckElementsLimit)
        let remainingServiceIds = Array(serviceIds.subtracting(batchServiceIds))

        return firstly(on: schedulers.global()) { () -> Promise<HTTPResponse> in
            Logger.info("Performing batch identity key lookup for \(batchServiceIds.count) addresses. \(remainingServiceIds.count) remaining.")

            let elements = self.db.read { tx in
                batchServiceIds.compactMap { serviceId -> [String: String]? in
                    guard let identityKey = try? self.identityKey(for: serviceId, tx: tx) else { return nil }

                    let externalIdentityKey = identityKey.serialize().asData
                    guard let identityKeyDigest = Cryptography.computeSHA256Digest(externalIdentityKey) else {
                        owsFailDebug("Failed to calculate SHA-256 digest for batch identity key update")
                        return nil
                    }

                    return ["uuid": serviceId.serviceIdString, "fingerprint": Data(identityKeyDigest.prefix(4)).base64EncodedString()]
                }
            }

            let request = OWSRequestFactory.batchIdentityCheckRequest(elements: elements)

            return self.networkManager.makePromise(request: request)
        }.done(on: schedulers.global()) { response in
            guard response.responseStatusCode == 200 else {
                throw OWSAssertionError("Unexpected response from batch identity request \(response.responseStatusCode)")
            }

            guard let json = response.responseBodyJson, let responseDictionary = json as? [String: AnyObject] else {
                throw OWSAssertionError("Missing or invalid JSON")
            }

            guard let responseElements = responseDictionary["elements"] as? [[String: String]], !responseElements.isEmpty else {
                return // No safety number changes
            }

            Logger.info("Detected \(responseElements.count) identity key changes via batch request")

            self.db.write { tx in
                for element in responseElements {
                    guard
                        let serviceIdString = element["uuid"],
                        let serviceId = try? ServiceId.parseFrom(serviceIdString: serviceIdString)
                    else {
                        owsFailDebug("Invalid uuid in batch identity response")
                        continue
                    }

                    guard
                        let encodedIdentityKey = element["identityKey"],
                        let externalIdentityKey = Data(base64Encoded: encodedIdentityKey),
                        let identityKey = try? IdentityKey(bytes: externalIdentityKey)
                    else {
                        owsFailDebug("Missing or invalid identity key in batch identity response")
                        continue
                    }

                    self.saveIdentityKey(identityKey, for: serviceId, tx: tx)
                }
            }
        }.then { () -> Promise<Void> in
            return self.batchUpdateIdentityKeys(for: remainingServiceIds)
        }.catch { error in
            owsFailDebug("Batch identity key update failed with error \(error)")
        }
    }
}

// MARK: - ObjC Bridge

class OWSIdentityManagerObjCBridge: NSObject {
    @objc
    static let identityKeyLength = UInt(OWSIdentityManagerImpl.Constants.identityKeyLength)

    @objc
    static let identityStateDidChangeNotification = NSNotification.Name.identityStateDidChange

    @objc
    static func identityKeyPair(forIdentity identity: OWSIdentity) -> ECKeyPair? {
        return databaseStorage.read { tx in
            let identityManager = DependenciesBridge.shared.identityManager
            return identityManager.identityKeyPair(for: identity, tx: tx.asV2Read)
        }
    }

    @objc
    static func identityKey(forAddress address: SignalServiceAddress) -> Data? {
        return databaseStorage.read { tx in
            let identityManager = DependenciesBridge.shared.identityManager
            return identityManager.identityKey(for: address, tx: tx.asV2Read)
        }
    }

    @objc
    static func saveIdentityKey(_ identityKey: Data, forServiceId serviceId: ServiceIdObjC, transaction tx: SDSAnyWriteTransaction) {
        DependenciesBridge.shared.identityManager.saveIdentityKey(identityKey, for: serviceId.wrappedValue, tx: tx.asV2Write)
    }
}

// MARK: - Unit Tests

#if TESTABLE_BUILD

extension OWSIdentityManager {
    @discardableResult
    func generateAndPersistNewIdentityKey(for identity: OWSIdentity) -> ECKeyPair {
        let result = generateNewIdentityKeyPair()
        DependenciesBridge.shared.db.write { tx in
            setIdentityKeyPair(result, for: identity, tx: tx)
        }
        return result
    }
}

#endif
