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

import Foundation
import GRDB
import SignalCoreKit

@available(swift, obsoleted: 1.0)
@objcMembers
public class OWSDeviceObjc: NSObject {
    public static let primaryDeviceId: UInt32 = OWSDevice.primaryDeviceId
}

public final class OWSDevice: SDSCodableModel, Decodable {
    public static let primaryDeviceId: UInt32 = 1

    public static let databaseTableName: String = "model_OWSDevice"
    public static var recordType: UInt { SDSRecordType.device.rawValue }

    public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
        case id
        case recordType
        case uniqueId

        case deviceId
        case encryptedName = "name"
        case createdAt
        case lastSeenAt
    }

    public var id: RowId?
    public let uniqueId: String

    public let deviceId: Int
    public let encryptedName: String?
    public let createdAt: Date
    public let lastSeenAt: Date

    init(
        deviceId: Int,
        encryptedName: String?,
        createdAt: Date,
        lastSeenAt: Date
    ) {
        self.uniqueId = UUID().uuidString

        self.deviceId = deviceId
        self.encryptedName = encryptedName
        self.createdAt = createdAt
        self.lastSeenAt = lastSeenAt
    }

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

        let decodedRecordType = try container.decode(UInt.self, forKey: .recordType)
        owsAssertDebug(decodedRecordType == Self.recordType, "Unexpectedly decoded record with wrong type!")

        id = try container.decodeIfPresent(RowId.self, forKey: .id)
        uniqueId = try container.decode(String.self, forKey: .uniqueId)

        deviceId = try container.decode(Int.self, forKey: .deviceId)
        encryptedName = try container.decodeIfPresent(String.self, forKey: .encryptedName)
        createdAt = try container.decode(Date.self, forKey: .createdAt)
        lastSeenAt = try container.decode(Date.self, forKey: .lastSeenAt)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(Self.recordType, forKey: .recordType)

        try id.map { try container.encode($0, forKey: .id) }
        try container.encode(uniqueId, forKey: .uniqueId)

        try container.encode(deviceId, forKey: .deviceId)
        try container.encodeIfPresent(encryptedName, forKey: .encryptedName)
        try container.encode(createdAt, forKey: .createdAt)
        try container.encode(lastSeenAt, forKey: .lastSeenAt)
    }
}

public extension OWSDevice {
    func displayName(
        identityManager: OWSIdentityManager,
        tx: DBReadTransaction
    ) -> String {
        if let encryptedName = self.encryptedName {
            if let identityKeyPair = identityManager.identityKeyPair(for: .aci, tx: tx) {
                do {
                    return try DeviceNames.decryptDeviceName(
                        base64String: encryptedName,
                        identityKeyPair: identityKeyPair.keyPair
                    )
                } catch let error {
                    Logger.error("Failed to decrypt device name: \(error). Is this a legacy device name?")
                }
            }

            return encryptedName
        }

        if deviceId == Self.primaryDeviceId {
            return OWSLocalizedString(
                "DEVICE_NAME_THIS_DEVICE",
                comment: "A label for this device in the device list."
            )
        }

        return OWSLocalizedString(
            "DEVICE_NAME_UNNAMED_DEVICE",
            comment: "A label for an unnamed device in the device list."
        )
    }

    var isPrimaryDevice: Bool {
        deviceId == Self.primaryDeviceId
    }

    var isLinkedDevice: Bool {
        !isPrimaryDevice
    }
}

// MARK: - Replace all

public extension OWSDevice {

    /// Update our persisted devices to match the given devices.
    ///
    /// - Returns
    /// `true` if any devices were added or removed, and `false` otherwise.
    static func replaceAll(
        with newDevices: [OWSDevice],
        transaction: SDSAnyWriteTransaction
    ) -> Bool {
        let existingDevices = anyFetchAll(transaction: transaction)

        for existingDevice in existingDevices {
            existingDevice.anyRemove(transaction: transaction)
        }

        for newDevice in newDevices {
            newDevice.anyInsert(transaction: transaction)
        }

        let existingDeviceIds = Set(existingDevices.map { $0.deviceId })
        let newDeviceIds = Set(newDevices.map { $0.deviceId })

        return !newDeviceIds.symmetricDifference(existingDeviceIds).isEmpty
    }
}
