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

import Foundation
import GRDB
import LibSignalClient

@objc
public class OWSUserProfileBadgeInfo: NSObject, Codable {
    @objc
    public let badgeId: String

    /// Details about how to render `badgeId`.
    ///
    /// Nil until a call to `loadBadge()` or `fetchBadgeContent(transaction:)`.
    public var badge: ProfileBadge?

    /// When the badge expires.
    ///
    /// Nil unless this is a badge for the local user.
    public let expiration: UInt64?

    /// True if the badge is visible.
    ///
    /// Nil unless this is a badge for the local user. (For other users, we only
    /// learn about visible badges, so we assume they're all visible.)
    public let isVisible: Bool?

    init(badgeId: String) {
        self.badgeId = badgeId
        self.expiration = nil
        self.isVisible = nil
    }

    init(badgeId: String, expiration: UInt64, isVisible: Bool) {
        self.badgeId = badgeId
        self.expiration = expiration
        self.isVisible = isVisible
    }

    private enum CodingKeys: String, CodingKey {
        // Skip encoding of the actual badge content
        case badgeId, expiration, isVisible
    }

    @objc
    public func loadBadge(transaction: SDSAnyReadTransaction) {
        badge = profileManager.badgeStore.fetchBadgeWithId(badgeId, readTx: transaction)
    }

    @objc
    public func fetchBadgeContent(transaction: SDSAnyReadTransaction) -> ProfileBadge? {
        return badge ?? {
            loadBadge(transaction: transaction)
            return badge
        }()
    }

    override public var description: String {
        var description = "Badge: \(badgeId)"
        if let expiration = expiration {
            description += ", Expires: \(Date(millisecondsSince1970: expiration))"
        }
        if let isVisible = isVisible {
            description += ", Visible: \(isVisible ? "Yes" : "No")"
        }
        return description
    }

    override public func isEqual(_ object: Any?) -> Bool {
        guard let other = object as? OWSUserProfileBadgeInfo else {
            return false
        }
        if badgeId != other.badgeId {
            return false
        }
        // NOTE: We do not compare badges because the badgeId is good enough for equality purposes.
        if expiration != other.expiration {
            return false
        }
        if isVisible != other.isVisible {
            return false
        }
        return true
    }
}

extension UserProfileWriter {
    var shouldUpdateStorageService: Bool {
        switch self {
        case .changePhoneNumber: fallthrough
        case .groupState: fallthrough
        case .localUser: fallthrough
        case .profileFetch: fallthrough
        case .registration: fallthrough
        case .reupload: fallthrough
        case .systemContactsFetch:
            return true

        case .avatarDownload: fallthrough
        case .debugging: fallthrough
        case .linking: fallthrough
        case .messageBackupRestore: fallthrough
        case .metadataUpdate: fallthrough
        case .storageService: fallthrough
        case .syncMessage: fallthrough
        case .tests:
            return false

        case .unknown: fallthrough
        @unknown default:
            return false
        }
    }
}

@objc
public final class UserProfileNotifications: NSObject {
    @objc
    public static let profileWhitelistDidChange = Notification.Name("kNSNotificationNameProfileWhitelistDidChange")

    @objc
    public static let localProfileDidChange = Notification.Name("kNSNotificationNameLocalProfileDidChange")

    @objc
    public static let localProfileKeyDidChange = Notification.Name("kNSNotificationNameLocalProfileKeyDidChange")

    @objc
    public static let otherUsersProfileDidChange = Notification.Name("kNSNotificationNameOtherUsersProfileDidChange")

    @objc
    public static let profileAddressKey = "kNSNotificationKey_ProfileAddress"

    @objc
    public static let profileGroupIdKey = "kNSNotificationKey_ProfileGroupId"
}

@objc
public final class OWSUserProfile: NSObject, NSCopying, SDSCodableModel, Decodable {
    public static let databaseTableName = "model_OWSUserProfile"
    public static var recordType: UInt { SDSRecordType.userProfile.rawValue }

    public enum Address: Hashable {
        case localUser
        case otherUser(SignalServiceAddress)
    }

    // MARK: - Constants

    public enum Constants {
        // For these values, "glyphs" represent what the user should be able to
        // type in an ideal world (eg "your name can contain 26 characters").
        // "Bytes" represents what the server enforces. Note that it's possible to
        // run into either limit (eg 5 emoji might hit the byte limit and 26 ASCII
        // characters might hit the glyph limit).

        public static let maxNameLengthGlyphs: Int = 26
        public static let maxNameLengthBytes: Int = 128

        public static let maxBioLengthGlyphs: Int = 140
        public static let maxBioLengthBytes: Int = 512

        fileprivate static let maxBioEmojiLengthGlyphs: Int = 1
        fileprivate static let maxBioEmojiLengthBytes: Int = 32

        static let localProfilePhoneNumber = "kLocalProfileUniqueId"
    }

    // MARK: - Properties

    public var id: RowId?
    public let uniqueId: String

    public var serviceIdString: String?
    public var phoneNumber: String?

    public var serviceId: ServiceId? { serviceIdString.flatMap { try? ServiceId.parseFrom(serviceIdString: $0) } }

    /// The "internal" address.
    ///
    /// The local user is represented by `localProfilePhoneNumber` and no ACI.
    /// All other users are represented by their real ACI/E164/PNI addresses.
    public var internalAddress: Address {
        if phoneNumber == Constants.localProfilePhoneNumber {
            return .localUser
        } else {
            return .otherUser(SignalServiceAddress.legacyAddress(serviceIdString: serviceIdString, phoneNumber: phoneNumber))
        }
    }

    /// The "public" address.
    ///
    /// All users are represented by their real ACI/PNI/E164 addresses.
    public func publicAddress(localIdentifiers: LocalIdentifiers) -> SignalServiceAddress {
        return Self.publicAddress(for: internalAddress, localIdentifiers: localIdentifiers)
    }

    /// The on-disk location of the downloaded avatar.
    ///
    /// This filename is relative to `profileAvatarsDirPath`.
    @objc
    private(set) public var avatarFileName: String?

    /// The on-server location of the encrypted avatar.
    ///
    /// This URL is downloaded, decrypted, and saved to `avatarFileName`.
    @objc
    private(set) public var avatarUrlPath: String?

    @objc
    private(set) public var profileKey: OWSAES256Key?

    @objc
    private(set) public var givenName: String?

    @objc
    private(set) public var familyName: String?

    @objc
    private(set) public var bio: String?

    @objc
    private(set) public var bioEmoji: String?

    @objc
    private(set) public var badges: [OWSUserProfileBadgeInfo]

    /// The last time we fetched a profile for this user.
    private(set) public var lastFetchDate: Date?

    /// This field reflects the last time we sent or received a message from
    /// this user. It is coarse; we only update it when it changes by more than
    /// one hour. It's not updated when sending messages to the local user
    /// (because we fetch our own profile more frequently than we fetch the
    /// profiles of other users).
    @objc
    private(set) public var lastMessagingDate: Date?

    /// Stores whether or not the phone number is shared for this account.
    ///
    /// Note that we may not yet know a phone number that's shared (and vice
    /// versa). If the value is nil, then it means there's not a value, we've
    /// never had a profile key for this user, or the value can't be decrypted.
    private(set) public var isPhoneNumberShared: Bool?

    public var isPhoneNumberSharedOrDefault: Bool {
        return isPhoneNumberShared ?? Self.isPhoneNumberSharedByDefault
    }

    public static var isPhoneNumberSharedByDefault: Bool {
        return PhoneNumberSharingMode.defaultValue == .everybody
    }

    public convenience init(
        address: Address,
        givenName: String? = nil,
        familyName: String? = nil,
        profileKey: OWSAES256Key? = nil,
        avatarUrlPath: String? = nil
    ) {
        let serviceId: ServiceId?
        let phoneNumber: String?
        switch address {
        case .localUser:
            serviceId = nil
            phoneNumber = Constants.localProfilePhoneNumber
        case .otherUser(let address):
            let normalizedAddress = NormalizedDatabaseRecordAddress(address: address)
            serviceId = normalizedAddress?.serviceId
            phoneNumber = normalizedAddress?.phoneNumber
        }
        self.init(
            id: nil,
            uniqueId: UUID().uuidString,
            serviceIdString: serviceId?.serviceIdUppercaseString,
            phoneNumber: phoneNumber,
            avatarFileName: nil,
            avatarUrlPath: avatarUrlPath,
            profileKey: profileKey,
            givenName: givenName,
            familyName: familyName,
            bio: nil,
            bioEmoji: nil,
            badges: [],
            lastFetchDate: nil,
            lastMessagingDate: nil,
            isPhoneNumberShared: nil
        )
    }

    init(
        id: RowId?,
        uniqueId: String,
        serviceIdString: String?,
        phoneNumber: String?,
        avatarFileName: String?,
        avatarUrlPath: String?,
        profileKey: OWSAES256Key?,
        givenName: String?,
        familyName: String?,
        bio: String?,
        bioEmoji: String?,
        badges: [OWSUserProfileBadgeInfo],
        lastFetchDate: Date?,
        lastMessagingDate: Date?,
        isPhoneNumberShared: Bool?
    ) {
        self.id = id
        self.uniqueId = uniqueId
        self.serviceIdString = serviceIdString
        self.phoneNumber = phoneNumber
        self.avatarFileName = avatarFileName
        self.avatarUrlPath = avatarUrlPath
        self.profileKey = profileKey
        self.givenName = givenName
        self.familyName = familyName
        self.bio = bio
        self.bioEmoji = bioEmoji
        self.badges = badges
        self.lastFetchDate = lastFetchDate
        self.lastMessagingDate = lastMessagingDate
        self.isPhoneNumberShared = isPhoneNumberShared
    }

    public func copy(with zone: NSZone? = nil) -> Any {
        return shallowCopy()
    }

    @objc
    public func shallowCopy() -> OWSUserProfile {
        return OWSUserProfile(
            id: id,
            uniqueId: uniqueId,
            serviceIdString: serviceIdString,
            phoneNumber: phoneNumber,
            avatarFileName: avatarFileName,
            avatarUrlPath: avatarUrlPath,
            profileKey: profileKey,
            givenName: givenName,
            familyName: familyName,
            bio: bio,
            bioEmoji: bioEmoji,
            badges: badges,
            lastFetchDate: lastFetchDate,
            lastMessagingDate: lastMessagingDate,
            isPhoneNumberShared: isPhoneNumberShared
        )
    }

    public override func isEqual(_ object: Any?) -> Bool {
        guard let otherProfile = object as? OWSUserProfile else {
            return false
        }
        guard id == otherProfile.id else { return false }
        guard uniqueId == otherProfile.uniqueId else { return false }
        guard serviceIdString == otherProfile.serviceIdString else { return false }
        guard phoneNumber == otherProfile.phoneNumber else { return false }
        guard avatarFileName == otherProfile.avatarFileName else { return false }
        guard avatarUrlPath == otherProfile.avatarUrlPath else { return false }
        guard profileKey == otherProfile.profileKey else { return false }
        guard givenName == otherProfile.givenName else { return false }
        guard familyName == otherProfile.familyName else { return false }
        guard bio == otherProfile.bio else { return false }
        guard bioEmoji == otherProfile.bioEmoji else { return false }
        guard badges == otherProfile.badges else { return false }
        guard lastFetchDate == otherProfile.lastFetchDate else { return false }
        guard lastMessagingDate == otherProfile.lastMessagingDate else { return false }
        guard isPhoneNumberShared == otherProfile.isPhoneNumberShared else { return false }
        return true
    }

    public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
        case id
        case recordType
        case uniqueId
        case avatarFileName
        case avatarUrlPath
        case profileKey
        case givenName = "profileName"
        case phoneNumber = "recipientPhoneNumber"
        case serviceIdString = "recipientUUID"
        case familyName
        case lastFetchDate
        case lastMessagingDate
        case bio
        case bioEmoji
        case badges = "profileBadgeInfo"
        case isStoriesCapable
        case canReceiveGiftBadges
        case isPniCapable
        case isPhoneNumberShared
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(id, forKey: .id)
        try container.encode(Self.recordType, forKey: .recordType)
        try container.encode(uniqueId, forKey: .uniqueId)
        try container.encodeIfPresent(serviceIdString, forKey: .serviceIdString)
        try container.encodeIfPresent(phoneNumber, forKey: .phoneNumber)
        try container.encodeIfPresent(avatarFileName, forKey: .avatarFileName)
        try container.encodeIfPresent(avatarUrlPath, forKey: .avatarUrlPath)
        try container.encodeIfPresent(profileKey?.keyData, forKey: .profileKey)
        try container.encodeIfPresent(givenName, forKey: .givenName)
        try container.encodeIfPresent(familyName, forKey: .familyName)
        try container.encodeIfPresent(bio, forKey: .bio)
        try container.encodeIfPresent(bioEmoji, forKey: .bioEmoji)
        try container.encode(JSONEncoder().encode(badges), forKey: .badges)
        try container.encodeIfPresent(lastFetchDate, forKey: .lastFetchDate)
        try container.encodeIfPresent(lastMessagingDate, forKey: .lastMessagingDate)
        try container.encode(true, forKey: .isStoriesCapable)
        try container.encode(true, forKey: .canReceiveGiftBadges)
        try container.encode(true, forKey: .isPniCapable)
        try container.encodeIfPresent(isPhoneNumberShared, forKey: .isPhoneNumberShared)
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let decodedRecordType = try container.decode(UInt.self, forKey: .recordType)
        guard decodedRecordType == Self.recordType else {
            owsFailDebug("Unexpected record type: \(decodedRecordType)")
            throw SDSError.invalidValue()
        }

        id = try container.decodeIfPresent(RowId.self, forKey: .id)
        uniqueId = try container.decode(String.self, forKey: .uniqueId)
        serviceIdString = try container.decodeIfPresent(String.self, forKey: .serviceIdString)
        phoneNumber = try container.decodeIfPresent(String.self, forKey: .phoneNumber)
        avatarFileName = try container.decodeIfPresent(String.self, forKey: .avatarFileName)
        avatarUrlPath = try container.decodeIfPresent(String.self, forKey: .avatarUrlPath)
        profileKey = try container.decodeIfPresent(Data.self, forKey: .profileKey).map(Self.decodeProfileKey(_:))
        givenName = try container.decodeIfPresent(String.self, forKey: .givenName)
        familyName = try container.decodeIfPresent(String.self, forKey: .familyName)
        bio = try container.decodeIfPresent(String.self, forKey: .bio)
        bioEmoji = try container.decodeIfPresent(String.self, forKey: .bioEmoji)
        badges = try container.decodeIfPresent(Data.self, forKey: .badges).map {
            try JSONDecoder().decode([OWSUserProfileBadgeInfo].self, from: $0)
        } ?? []
        lastFetchDate = try container.decodeIfPresent(Date.self, forKey: .lastFetchDate)
        lastMessagingDate = try container.decodeIfPresent(Date.self, forKey: .lastMessagingDate)
        isPhoneNumberShared = try container.decodeIfPresent(Bool.self, forKey: .isPhoneNumberShared)
    }

    private static func decodeProfileKey(_ profileKeyData: Data) throws -> OWSAES256Key {
        guard profileKeyData.count == kAES256_KeyByteLength, let profileKey = OWSAES256Key(data: profileKeyData) else {
            // Historically, we encoded this using an NSKeyedArchiver. We assume it's
            // encoded in this way if it's not exactly 32 bytes.
            return try LegacySDSSerializer().deserializeLegacySDSData(profileKeyData, propertyName: "profileKey")
        }
        return profileKey
    }

    // MARK: - Profile Addresses

    /// Converts a "public" address to an "internal" one.
    public static func internalAddress(for publicAddress: SignalServiceAddress, localIdentifiers: LocalIdentifiers) -> Address {
        if localIdentifiers.contains(address: publicAddress) {
            return .localUser
        } else {
            return .otherUser(publicAddress)
        }
    }

    /// Converts an "internal" or "public" address to a "public" one.
    private static func publicAddress(for internalAddress: Address, localIdentifiers: LocalIdentifiers) -> SignalServiceAddress {
        switch internalAddress {
        case .localUser:
            return localIdentifiers.aciAddress
        case .otherUser(let address):
            return address
        }
    }

    static func insertableAddress(
        for serviceId: ServiceId,
        localIdentifiers: LocalIdentifiers
    ) -> InsertableAddress {
        return localIdentifiers.contains(serviceId: serviceId) ? .localUser : .otherUser(serviceId)
    }

    // MARK: - Avatar

    /// Updates the remote/local avatar path properties.
    ///
    /// If you specify a new `avatarFileName` (ie the local path), the file
    /// specified by the old value is deleted from the disk.
    ///
    /// If you specify a new `avatarUrlPath` (ie the remote URL) without
    /// specifying its corresponding `avatarFileName` (ie the local path), then
    /// `avatarFileName` is cleared and the existing file is deleted.
    private func updateAvatar(avatarUrlPath: OptionalChange<String?>, avatarFileName: OptionalChange<String?>) -> Bool {
        let oldAvatarUrlPath = self.avatarUrlPath
        let newAvatarUrlPath = avatarUrlPath.orExistingValue(oldAvatarUrlPath)
        let urlPathDidChange = oldAvatarUrlPath != newAvatarUrlPath

        let oldAvatarFileName = self.avatarFileName
        // If we're changing avatarUrlPath, we must provide a new avatarFileName.
        // If we don't, we use `nil` to delete the existing one that's wrong.
        let newAvatarFileName = avatarFileName.orExistingValue(urlPathDidChange ? nil : oldAvatarFileName)
        let fileNameDidChange = oldAvatarFileName != newAvatarFileName

        guard urlPathDidChange || fileNameDidChange else {
            return false
        }

        if fileNameDidChange, let oldAvatarFileName, !oldAvatarFileName.isEmpty {
            let oldAvatarFilePath = Self.profileAvatarFilePath(for: oldAvatarFileName)
            OWSFileSystem.deleteFileIfExists(oldAvatarFilePath)
        }

        self.avatarUrlPath = newAvatarUrlPath
        self.avatarFileName = newAvatarFileName
        return true
    }

    /// Moves a temporary avatar file into its permanent location.
    ///
    /// This method accepts a `tx` parameter to ensure that this move occurs
    /// within a write transaction. Callers must ensure that it's the same write
    /// transaction that assigns the result to an OWSUserProfile. In doing so,
    /// callers ensure that the orphaned cleanup logic doesn't delete avatars
    /// that are about to be referenced.
    static func consumeTemporaryAvatarFileUrl(
        _ avatarFileUrl: OptionalChange<URL?>,
        tx: SDSAnyWriteTransaction
    ) throws -> OptionalChange<String?> {
        switch avatarFileUrl {
        case .noChange:
            return .noChange
        case .setTo(.none):
            return .setTo(nil)
        case .setTo(.some(let temporaryFileUrl)):
            let filename = generateAvatarFilename()
            let filePath = profileAvatarFilePath(for: filename)
            try FileManager.default.moveItem(atPath: temporaryFileUrl.path, toPath: filePath)
            return .setTo(filename)
        }
    }

    @objc
    public static var legacyProfileAvatarsDirPath: String {
        return OWSFileSystem.appDocumentDirectoryPath().appendingPathComponent("ProfileAvatars")
    }

    @objc
    public static var sharedDataProfileAvatarsDirPath: String {
        return OWSFileSystem.appSharedDataDirectoryPath().appendingPathComponent("ProfileAvatars")
    }

    private static let profileAvatarsDirPath: String = {
        let result = sharedDataProfileAvatarsDirPath
        OWSFileSystem.ensureDirectoryExists(result)
        return result
    }()

    static func generateAvatarFilename() -> String {
        return UUID().uuidString + ".jpg"
    }

    @objc
    static func profileAvatarFilePath(for filename: String) -> String {
        owsAssertDebug(!filename.isEmpty)
        return Self.profileAvatarsDirPath.appendingPathComponent(filename)
    }

    // TODO: We may want to clean up this directory in the "orphan cleanup" logic.

    public static func resetProfileStorage() {
        AssertIsOnMainThread()
        do {
            try FileManager.default.removeItem(atPath: profileAvatarsDirPath)
        } catch {
            Logger.error("Failed to delete database: \(error)")
        }
    }

    @objc
    public static func allProfileAvatarFilePaths(tx: SDSAnyReadTransaction) -> Set<String> {
        var result = Set<String>()
        Self.anyEnumerate(transaction: tx, batchingPreference: .batched(Batching.kDefaultBatchSize)) { userProfile, _ in
            if let avatarFileName = userProfile.avatarFileName {
                result.insert(Self.profileAvatarsDirPath.appendingPathComponent(avatarFileName))
            }
        }
        return result
    }

    // MARK: - Badges

    public var visibleBadges: [OWSUserProfileBadgeInfo] {
        return badges.filter { $0.isVisible ?? true }
    }

    public var primaryBadge: OWSUserProfileBadgeInfo? {
        return visibleBadges.first
    }

    func loadBadgeContent(tx: SDSAnyReadTransaction) {
        badges.forEach({ $0.loadBadge(transaction: tx) })
    }

    // MARK: - Bio

    private static let bioComponentCache = LRUCache<String, String>(maxSize: 256)
    private static let unfairLock = UnfairLock()

    private static func filterBioComponentForDisplay(_ input: String?, maxLengthGlyphs: Int, maxLengthBytes: Int) -> String? {
        guard let input = input else {
            return nil
        }
        let cacheKey = "\(maxLengthGlyphs)-\(maxLengthBytes)-\(input)"
        return unfairLock.withLock {
            // Note: we use empty strings in the cache, but return nil for empty strings.
            if let cachedValue = bioComponentCache.get(key: cacheKey) {
                return cachedValue.nilIfEmpty
            }
            let value = input.filterStringForDisplay().trimToGlyphCount(maxLengthGlyphs).trimToUtf8ByteCount(maxLengthBytes)
            bioComponentCache.set(key: cacheKey, value: value)
            return value.nilIfEmpty
        }
    }

    /// Joins the two bio components into a single string ready for display. It
    /// filters and enforces length limits on the components.
    @objc
    public static func bioForDisplay(bio: String?, bioEmoji: String?) -> String? {
        var components = [String]()
        // TODO: We could use EmojiWithSkinTones to check for availability of the emoji.
        if let emoji = filterBioComponentForDisplay(
            bioEmoji,
            maxLengthGlyphs: Constants.maxBioEmojiLengthGlyphs,
            maxLengthBytes: Constants.maxBioEmojiLengthBytes
        ) {
            components.append(emoji)
        }
        if let bioText = filterBioComponentForDisplay(
            bio,
            maxLengthGlyphs: Constants.maxBioLengthGlyphs,
            maxLengthBytes: Constants.maxBioLengthBytes
        ) {
            components.append(bioText)
        }
        guard !components.isEmpty else {
            return nil
        }
        return components.joined(separator: " ")
    }

    // MARK: - Name

    public var nameComponents: PersonNameComponents? {
        guard let givenName = self.givenName?.strippedOrNil else {
            return nil
        }
        var result = PersonNameComponents()
        result.givenName = givenName
        result.familyName = self.familyName?.strippedOrNil
        return result
    }

    @objc
    public var filteredGivenName: String? { givenName?.filterForDisplay }

    @objc
    public var filteredFamilyName: String? { familyName?.filterForDisplay }

    @objc
    public var filteredNameComponents: PersonNameComponents? {
        guard let givenName = self.filteredGivenName?.nilIfEmpty else {
            return nil
        }
        var result = PersonNameComponents()
        result.givenName = givenName
        result.familyName = self.filteredFamilyName
        return result
    }

    @objc
    public var filteredFullName: String? {
        return filteredNameComponents.map(OWSFormat.formatNameComponents(_:))?.filterForDisplay
    }

    // MARK: - Encryption

    public class func encrypt(profileData: Data, profileKey: OWSAES256Key) throws -> Data {
        return try Aes256GcmEncryptedData.encrypt(profileData, key: profileKey.keyData).concatenate()
    }

    public class func decrypt(profileData: Data, profileKey: OWSAES256Key) throws -> Data {
        return try Aes256GcmEncryptedData(concatenated: profileData).decrypt(key: profileKey.keyData)
    }

    enum DecryptionError: Error {
        case missingName
        case malformedValue
    }

    class func decrypt(profileNameData: Data, profileKey: OWSAES256Key) throws -> (givenName: String, familyName: String?) {
        let decryptedData = try decrypt(profileData: profileNameData, profileKey: profileKey)

        func parseNameSegment(_ nameSegment: Data) throws -> String? {
            guard let nameValue = String(data: nameSegment, encoding: .utf8) else {
                throw DecryptionError.malformedValue
            }
            return nameValue.strippedOrNil
        }

        // Unpad profile name. The given and family name are stored in the string
        // like "<given name><null><family name><null padding>"
        let nameSegments: [Data] = decryptedData.split(separator: 0x00, maxSplits: 2, omittingEmptySubsequences: false)

        guard let givenName = try parseNameSegment(nameSegments.first!) else {
            throw DecryptionError.missingName
        }
        let familyName = nameSegments.dropFirst().first
        return (givenName, try familyName.flatMap(parseNameSegment(_:)))
    }

    class func decrypt(profileStringData: Data, profileKey: OWSAES256Key) throws -> String? {
        let decryptedData = try decrypt(profileData: profileStringData, profileKey: profileKey)

        // Remove padding.
        let segments: [Data] = decryptedData.split(separator: 0x00, maxSplits: 1, omittingEmptySubsequences: false)
        guard let value = String(data: segments.first!, encoding: .utf8) else {
            throw DecryptionError.malformedValue
        }
        return value.nilIfEmpty
    }

    class func decrypt(profileBooleanData: Data, profileKey: OWSAES256Key) throws -> Bool {
        switch try decrypt(profileData: profileBooleanData, profileKey: profileKey) {
        case Data([1]):
            return true
        case Data([0]):
            return false
        default:
            throw DecryptionError.malformedValue
        }
    }

    public class func encrypt(
        givenName: OWSUserProfile.NameComponent,
        familyName: OWSUserProfile.NameComponent?,
        profileKey: OWSAES256Key
    ) throws -> ProfileValue {
        let encodedValues: [Data] = [givenName.dataValue, familyName?.dataValue].compacted()
        let encodedValue = Data(encodedValues.joined(separator: Data([0])))
        assert(Constants.maxNameLengthBytes * 2 + 1 == 257)
        return try encrypt(data: encodedValue, profileKey: profileKey, paddedLengths: [53, 257])
    }

    public class func encrypt(data unpaddedData: Data, profileKey: OWSAES256Key, paddedLengths: [Int]) throws -> ProfileValue {
        assert(paddedLengths == paddedLengths.sorted())

        guard let paddedLength = paddedLengths.first(where: { $0 >= unpaddedData.count }) else {
            throw OWSAssertionError("Oversize value: \(unpaddedData.count) > \(paddedLengths)")
        }

        var paddedData = unpaddedData
        let paddingByteCount = paddedLength - paddedData.count
        paddedData.count += paddingByteCount
        assert(paddedData.count == paddedLength)

        return ProfileValue(encryptedData: try encrypt(profileData: paddedData, profileKey: profileKey))
    }

    // MARK: - Fetching & Creating

    @objc
    public static func getUserProfileForLocalUser(tx: SDSAnyReadTransaction) -> OWSUserProfile? {
        return getUserProfile(for: .localUser, tx: tx)
    }

    public static func getUserProfile(for address: Address, tx: SDSAnyReadTransaction) -> OWSUserProfile? {
        return UserProfileFinder().userProfile(for: address, transaction: tx)
    }

    @objc
    public static func doesLocalProfileExist(transaction tx: SDSAnyReadTransaction) -> Bool {
        return UserProfileFinder().userProfile(for: .localUser, transaction: tx) != nil
    }

    public enum InsertableAddress {
        case localUser
        case otherUser(ServiceId)
    }

    @objc
    public class func getOrBuildUserProfileForLocalUser(
        userProfileWriter: UserProfileWriter,
        tx: SDSAnyWriteTransaction
    ) -> OWSUserProfile {
        return getOrBuildUserProfile(
            for: .localUser,
            userProfileWriter: userProfileWriter,
            tx: tx
        )
    }

    public class func getOrBuildUserProfile(
        for insertableAddress: InsertableAddress,
        userProfileWriter: UserProfileWriter,
        tx: SDSAnyWriteTransaction
    ) -> OWSUserProfile {
        // If we already have a profile for this address, return it.
        if let userProfile = fetchAndExpungeUserProfiles(for: insertableAddress, tx: tx) {
            return userProfile
        }

        let address: Address
        switch insertableAddress {
        case .localUser:
            address = .localUser
        case .otherUser(let serviceId):
            address = .otherUser(SignalServiceAddress(serviceId))
        }

        // Otherwise, create & return a new profile for this address.
        let userProfile = OWSUserProfile(address: address)
        if case .localUser = address {
            userProfile.update(
                profileKey: .setTo(OWSAES256Key.generateRandom()),
                userProfileWriter: userProfileWriter,
                transaction: tx,
                completion: nil
            )
        }
        return userProfile
    }

    /// Ensures there's a single profile for a given recipient.
    ///
    /// We should only have one UserProfile for each SignalRecipient. However,
    /// it's possible that duplicates may exist. This method will find and
    /// remove duplicates.
    private class func fetchAndExpungeUserProfiles(
        for insertableAddress: InsertableAddress,
        tx: SDSAnyWriteTransaction
    ) -> OWSUserProfile? {
        let userProfiles: [OWSUserProfile]
        switch insertableAddress {
        case .localUser:
            userProfiles = UserProfileFinder().fetchUserProfiles(phoneNumber: Constants.localProfilePhoneNumber, tx: tx)
        case .otherUser(let serviceId):
            userProfiles = UserProfileFinder().fetchUserProfiles(serviceId: serviceId, tx: tx)
        }

        // Get rid of any duplicates -- these shouldn't exist.
        for redundantProfile in userProfiles.dropFirst() {
            redundantProfile.anyRemove(transaction: tx)
        }
        return userProfiles.first
    }

    // MARK: - Database Hooks

    public func didInsert(with rowID: Int64, for column: String?) {
        self.id = rowID
    }

    public func anyDidInsert(transaction: SDSAnyWriteTransaction) {
        modelReadCaches.userProfileReadCache.didInsertOrUpdate(userProfile: self, transaction: transaction)
        let searchableNameIndexer = DependenciesBridge.shared.searchableNameIndexer
        searchableNameIndexer.insert(self, tx: transaction.asV2Write)
    }

    public func anyDidUpdate(transaction: SDSAnyWriteTransaction) {
        modelReadCaches.userProfileReadCache.didInsertOrUpdate(userProfile: self, transaction: transaction)
        let searchableNameIndexer = DependenciesBridge.shared.searchableNameIndexer
        searchableNameIndexer.update(self, tx: transaction.asV2Write)
    }

    public func anyDidRemove(transaction: SDSAnyWriteTransaction) {
        modelReadCaches.userProfileReadCache.didRemove(userProfile: self, transaction: transaction)
    }

    public func anyDidFetchOne(transaction: SDSAnyReadTransaction) {
        modelReadCaches.userProfileReadCache.didReadUserProfile(self, transaction: transaction)
    }

    public func anyDidEnumerateOne(transaction: SDSAnyReadTransaction) {
        modelReadCaches.userProfileReadCache.didReadUserProfile(self, transaction: transaction)
    }

    // MARK: - ObjC Compability

    @objc
    public static func shouldUpdateStorageServiceForUserProfileWriter(_ userProfileWriter: UserProfileWriter) -> Bool {
        return userProfileWriter.shouldUpdateStorageService
    }
}

extension Optional where Wrapped == OWSUserProfile {
    public var isPhoneNumberSharedOrDefault: Bool {
        return self?.isPhoneNumberSharedOrDefault ?? OWSUserProfile.isPhoneNumberSharedByDefault
    }
}

// MARK: -

extension OWSUserProfile {
    public struct NameComponent: Equatable {
        public let stringValue: StrippedNonEmptyString
        public let dataValue: Data

        private init(stringValue: StrippedNonEmptyString, dataValue: Data) {
            self.stringValue = stringValue
            self.dataValue = dataValue
        }

        public init?(truncating: String) {
            guard let (parsedValue, _) = Self.parse(truncating: truncating) else {
                return nil
            }
            self = parsedValue
        }

        public static func parse(truncating: String) -> (nameComponent: Self, didTruncate: Bool)? {
            // We need to truncate to the required limit. Before doing so, we strip the
            // string in case there's any leading whitespace. For example, if the limit
            // is 3 characters, " Alice" should become "Ali" instead of "Al".
            let strippedString = truncating.stripped
            let truncatedString = strippedString
                .trimToGlyphCount(OWSUserProfile.Constants.maxNameLengthGlyphs)
                .trimToUtf8ByteCount(OWSUserProfile.Constants.maxNameLengthBytes)
            // After trimming, we need to strip AGAIN. If the string starts with a
            // control character, has a series of whitespaces, and then has
            // user-visible characters, and if we truncate those user visible
            // characters, we'll be left with a value that's now considered empty.
            guard let strippedTruncatedString = StrippedNonEmptyString(rawValue: truncatedString) else {
                return nil
            }
            guard let dataValue = strippedTruncatedString.rawValue.data(using: .utf8) else {
                return nil
            }
            return (
                NameComponent(stringValue: strippedTruncatedString, dataValue: dataValue),
                didTruncate: truncatedString != strippedString
            )
        }

        public static func == (lhs: Self, rhs: Self) -> Bool {
            // Don't check `dataValue` because it's essentially a computed property.
            return lhs.stringValue == rhs.stringValue
        }
    }
}

// MARK: -

public struct ProfileValue {
    let encryptedData: Data

    public init(encryptedData: Data) {
        self.encryptedData = encryptedData
    }

    var encryptedBase64Value: String {
        encryptedData.base64EncodedString()
    }
}

// MARK: -

private struct UserProfileChanges {
    var givenName: OptionalChange<String?>
    var familyName: OptionalChange<String?>
    var bio: OptionalChange<String?>
    var bioEmoji: OptionalChange<String?>
    var avatarUrlPath: OptionalChange<String?>
    var avatarFileName: OptionalChange<String?>
    var lastFetchDate: OptionalChange<Date>
    var lastMessagingDate: OptionalChange<Date>
    var profileKey: OptionalChange<OWSAES256Key>
    var badges: OptionalChange<[OWSUserProfileBadgeInfo]>
    var isPhoneNumberShared: OptionalChange<Bool?>
}

// MARK: - Update With... Methods

extension OWSUserProfile {
    private enum UserVisibleChange {
        /// *Something* -- the profile name, profile key, etc. -- has changed. We
        /// want to post notifications for this update because various UI components
        /// may need to be updated. Note: It's possible that the avatar was also
        /// updated in this case.
        case something

        /// Something has changed, but we know it's only the avatar properties. We
        /// still want to post notifications to update the UI, but there are a few
        /// steps we can skip if it's only the avatar that's different.
        case avatarOnly

        /// "Nothing" changed, where "nothing" means "nothing important". There
        /// might still be updates to the profile, but they're internal-only and
        /// aren't reflected in the UI. For example, "lastMessagingDate" is used to
        /// decide when to fetch profiles, but it's not displayed to the user, so we
        /// can skip notifications if that's the only property that changed.
        case nothing
    }

    /// Applies `UserProfileChanges` to `self`.
    ///
    /// Returns a value indicating `changes`' user-visible impact.
    @discardableResult
    private func applyChanges(
        _ changes: UserProfileChanges,
        userProfileWriter: UserProfileWriter
    ) -> UserVisibleChange {
        let canModifyStorageServiceProperties: Bool
        switch internalAddress {
        case .localUser:
            canModifyStorageServiceProperties = {
                // Any properties stored in the storage service can only by modified by the
                // local user or the storage service. In particular, they should _not_ be
                // modified by profile fetches.
                switch userProfileWriter {
                case .debugging: fallthrough
                case .localUser: fallthrough
                case .messageBackupRestore: fallthrough
                case .registration: fallthrough
                case .storageService: fallthrough
                case .tests:
                    return true

                case .avatarDownload: fallthrough
                case .groupState: fallthrough
                case .linking: fallthrough
                case .metadataUpdate: fallthrough
                case .profileFetch: fallthrough
                case .reupload: fallthrough
                case .syncMessage:
                    return false

                case .changePhoneNumber: fallthrough
                case .systemContactsFetch: fallthrough
                case .unknown: fallthrough
                @unknown default:
                    owsFailDebug("Invalid userProfileWriter.")
                    return false
                }
            }()
        case .otherUser:
            canModifyStorageServiceProperties = true
        }

        func setIfChanged<T: Equatable>(_ newValue: OptionalChange<T>, keyPath: ReferenceWritableKeyPath<OWSUserProfile, T>) -> Int {
            switch newValue {
            case .setTo(let newValue) where newValue != self[keyPath: keyPath]:
                self[keyPath: keyPath] = newValue
                return 1
            case .setTo, .noChange:
                return 0
            }
        }

        // We special-case avatar changes in a few places.
        let canUpdateAvatarUrlPath = canModifyStorageServiceProperties || userProfileWriter == .reupload
        let canUpdateAvatarFileName = canUpdateAvatarUrlPath || userProfileWriter == .avatarDownload
        let didChangeAvatar = updateAvatar(
            avatarUrlPath: canUpdateAvatarUrlPath ? changes.avatarUrlPath : .noChange,
            avatarFileName: canUpdateAvatarFileName ? changes.avatarFileName : .noChange
        )

        // And we also care if user-visible properties change.
        var visibleChangeCount = 0
        if canModifyStorageServiceProperties {
            visibleChangeCount += setIfChanged(changes.givenName, keyPath: \.givenName)
            visibleChangeCount += setIfChanged(changes.familyName, keyPath: \.familyName)
        }
        visibleChangeCount += setIfChanged(changes.bio, keyPath: \.bio)
        visibleChangeCount += setIfChanged(changes.bioEmoji, keyPath: \.bioEmoji)
        visibleChangeCount += setIfChanged(changes.badges, keyPath: \.badges)
        visibleChangeCount += setIfChanged(changes.profileKey.map { $0 as OWSAES256Key? }, keyPath: \.profileKey)
        visibleChangeCount += setIfChanged(changes.isPhoneNumberShared, keyPath: \.isPhoneNumberShared)

        // Some properties are invisible/"polled", so changes don't matter.
        _ = setIfChanged(changes.lastFetchDate.map { $0 as Date? }, keyPath: \.lastFetchDate)
        _ = setIfChanged(changes.lastMessagingDate.map { $0 as Date? }, keyPath: \.lastMessagingDate)

        if visibleChangeCount > 0 {
            return .something
        }
        if didChangeAvatar {
            return .avatarOnly
        }
        return .nothing
    }

    /// Applies `UserProfileChanges` to `self` (& the db).
    ///
    /// If `self` hasn't been inserted, it will be inserted. Otherwise, a
    /// pattern similar to `anyUpdate` is followed where `self` and a copy from
    /// the database are both modified.
    ///
    /// This method has lots of side effects, such as:
    /// - Reconciling badges when fetching the local user's profile.
    /// - Inserting "profile update" chat events.
    /// - Re-indexing threads, group members, etc.
    /// - Updating storage service.
    /// - Posting notifications about profile updates & new profile keys.
    private func applyChanges(
        _ changes: UserProfileChanges,
        userProfileWriter: UserProfileWriter,
        tx: SDSAnyWriteTransaction,
        completion: (() -> Void)?
    ) {
        let internalAddress = self.internalAddress

        if case .otherUser(let address) = internalAddress {
            // We should never be writing to or updating the "local address" profile;
            // we should be using the "localProfilePhoneNumber" profile instead.
            owsAssertDebug(!address.isLocalAddress)
        }

        let oldInstance = Self.anyFetch(uniqueId: uniqueId, transaction: tx)
        oldInstance?.loadBadgeContent(tx: tx)
        let newInstance: OWSUserProfile

        // Always apply the changes to `self`.
        applyChanges(changes, userProfileWriter: userProfileWriter)

        let changeResult: UserVisibleChange
        if let oldInstance {
            newInstance = oldInstance.shallowCopy()
            changeResult = newInstance.applyChanges(changes, userProfileWriter: userProfileWriter)
            newInstance.anyOverwritingUpdate(transaction: tx)
        } else {
            newInstance = self
            changeResult = .something
            newInstance.anyInsert(transaction: tx)
        }

        loadBadgeContent(tx: tx)
        newInstance.loadBadgeContent(tx: tx)

        func changeSummary<T: Equatable>(for keyPath: KeyPath<OWSUserProfile, T?>, logDescription: StaticString) -> String? {
            let oldValue: T? = oldInstance?[keyPath: keyPath]
            let newValue: T? = newInstance[keyPath: keyPath]
            if newValue == oldValue {
                return nil
            }
            let oldValueDescription = oldValue != nil ? "something" : "nil"
            let newValueDescription = newValue != nil ? "something else" : "nil"
            return "\(logDescription) changed (\(oldValueDescription) -> \(newValueDescription))"
        }

        let changeSummaries: [String] = [
            changeSummary(for: \.profileKey, logDescription: "profileKey"),
            changeSummary(for: \.givenName, logDescription: "givenName"),
            changeSummary(for: \.familyName, logDescription: "familyName"),
            changeSummary(for: \.avatarUrlPath, logDescription: "avatarUrlPath"),
            changeSummary(for: \.avatarFileName, logDescription: "avatarFileName"),
        ].compacted()

        if !changeSummaries.isEmpty {
            Logger.info("Updated \(internalAddress): \(changeSummaries.joined(separator: ", "))")
        }

        if let completion {
            tx.addAsyncCompletionOffMain(completion)
        }

        if case .localUser = internalAddress {
            profileManager.localProfileWasUpdated(self)
        }

        if case .localUser = internalAddress, case .setTo = changes.badges {
            subscriptionManager.reconcileBadgeStates(transaction: tx)
        }

        if let oldInstance {
            // Insert a profile change update in conversations, if necessary
            TSInfoMessage.insertProfileChangeMessagesIfNecessary(
                oldProfile: oldInstance,
                newProfile: newInstance,
                transaction: tx
            )
        }

        updatePhoneNumberVisibilityIfNeeded(
            oldUserProfile: oldInstance,
            newUserProfile: newInstance,
            tx: tx.asV2Write
        )

        if changeResult == .nothing {
            return
        }

        // Profile changes, record updates with storage service. We don't store
        // avatar information on the service except for the local user.
        let shouldUpdateStorageService: Bool = {
            let tsAccountManager = DependenciesBridge.shared.tsAccountManager
            guard tsAccountManager.registrationState(tx: tx.asV2Read).isRegistered else {
                return false
            }
            guard userProfileWriter.shouldUpdateStorageService else {
                return false
            }
            switch internalAddress {
            case .localUser:
                // Never update local profile on storage service to reflect profile fetches.
                return userProfileWriter != .profileFetch
            case .otherUser:
                // Only update storage service if we changed something other than the avatar.
                return changeResult != .avatarOnly
            }
        }()

        if shouldUpdateStorageService {
            tx.addAsyncCompletionOffMain {
                switch internalAddress {
                case .localUser:
                    self.storageServiceManager.recordPendingLocalAccountUpdates()
                case .otherUser(let address):
                    self.storageServiceManager.recordPendingUpdates(updatedAddresses: [address])
                }
            }
        }

        let oldProfileKey = oldInstance?.profileKey
        let newProfileKey = newInstance.profileKey

        tx.addAsyncCompletionOnMain {
            switch internalAddress {
            case .localUser:
                if oldProfileKey != newProfileKey {
                    NotificationCenter.default.postNotificationNameAsync(UserProfileNotifications.localProfileKeyDidChange, object: nil)
                }
                NotificationCenter.default.postNotificationNameAsync(UserProfileNotifications.localProfileDidChange, object: nil)
            case .otherUser(let address):
                NotificationCenter.default.postNotificationNameAsync(
                    UserProfileNotifications.otherUsersProfileDidChange,
                    object: nil,
                    userInfo: [UserProfileNotifications.profileAddressKey: address]
                )
            }
        }
    }

    private func updatePhoneNumberVisibilityIfNeeded(
        oldUserProfile: OWSUserProfile?,
        newUserProfile: OWSUserProfile,
        tx: DBWriteTransaction
    ) {
        // Don't do anything unless the sharing setting was changed.
        if newUserProfile.isPhoneNumberSharedOrDefault == oldUserProfile.isPhoneNumberSharedOrDefault {
            return
        }
        guard case .otherUser(let address) = internalAddress, let aci = address.aci else {
            return
        }
        let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
        let recipient = recipientDatabaseTable.fetchRecipient(serviceId: aci, transaction: tx)
        guard let recipient else {
            return
        }
        // Tell the cache to refresh its state for this recipient. It will check
        // whether or not the number should be visible based on this state and the
        // state of system contacts.
        signalServiceAddressCache.updateRecipient(recipient, tx: tx)
    }

    /// Applies changes specified by the properties.
    public func update(
        givenName: OptionalChange<String?> = .noChange,
        familyName: OptionalChange<String?> = .noChange,
        bio: OptionalChange<String?> = .noChange,
        bioEmoji: OptionalChange<String?> = .noChange,
        avatarUrlPath: OptionalChange<String?> = .noChange,
        avatarFileName: OptionalChange<String?> = .noChange,
        lastFetchDate: OptionalChange<Date> = .noChange,
        lastMessagingDate: OptionalChange<Date> = .noChange,
        profileKey: OptionalChange<OWSAES256Key> = .noChange,
        badges: OptionalChange<[OWSUserProfileBadgeInfo]> = .noChange,
        isPhoneNumberShared: OptionalChange<Bool?> = .noChange,
        userProfileWriter: UserProfileWriter,
        transaction: SDSAnyWriteTransaction,
        completion: (() -> Void)?
    ) {
        applyChanges(
            UserProfileChanges(
                givenName: givenName,
                familyName: familyName,
                bio: bio,
                bioEmoji: bioEmoji,
                avatarUrlPath: avatarUrlPath,
                avatarFileName: avatarFileName,
                lastFetchDate: lastFetchDate,
                lastMessagingDate: lastMessagingDate,
                profileKey: profileKey,
                badges: badges,
                isPhoneNumberShared: isPhoneNumberShared
            ),
            userProfileWriter: userProfileWriter,
            tx: transaction,
            completion: completion
        )
    }

    @available(swift, obsoleted: 1.0)
    @objc
    public func clearProfile(
        profileKey: OWSAES256Key,
        userProfileWriter: UserProfileWriter,
        transaction: SDSAnyWriteTransaction,
        completion: (() -> Void)?
    ) {
        // This is only used for debugging.
        owsAssertDebug(userProfileWriter == .debugging)
        update(
            givenName: .setTo(nil),
            familyName: .setTo(nil),
            bio: .setTo(nil),
            bioEmoji: .setTo(nil),
            avatarUrlPath: .setTo(nil),
            avatarFileName: .setTo(nil),
            profileKey: .setTo(profileKey),
            userProfileWriter: userProfileWriter,
            transaction: transaction,
            completion: completion
        )
    }

    @available(swift, obsoleted: 1.0)
    @objc
    public func update(
        profileKey: OWSAES256Key,
        userProfileWriter: UserProfileWriter,
        transaction: SDSAnyWriteTransaction,
        completion: (() -> Void)?
    ) {
        update(
            profileKey: .setTo(profileKey),
            userProfileWriter: userProfileWriter,
            transaction: transaction,
            completion: completion
        )
    }
}

extension OWSUserProfile {
    static func getUserProfiles(for addresses: [Address], tx: SDSAnyReadTransaction) -> [OWSUserProfile?] {
        return UserProfileFinder().userProfiles(for: addresses, tx: tx)
    }
}

// MARK: - StringInterpolation

public extension String.StringInterpolation {
    mutating func appendInterpolation(userProfileColumn column: OWSUserProfile.CodingKeys) {
        appendLiteral(OWSUserProfile.columnName(column))
    }
    mutating func appendInterpolation(userProfileColumnFullyQualified column: OWSUserProfile.CodingKeys) {
        appendLiteral(OWSUserProfile.columnName(column, fullyQualified: true))
    }
}
