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

import LibSignalClient

public protocol SignalKyberPreKeyStore: LibSignalClient.KyberPreKeyStore {
    func generateLastResortKyberPreKey(
        signedBy keyPair: ECKeyPair,
        tx: DBWriteTransaction
    ) throws -> SignalServiceKit.KyberPreKeyRecord

    /// Keys returned by this method should not be stored in the local
    /// KyberPreKeyStore since there is no guarantee the key ID is unique.
    func generateLastResortKyberPreKeyForLinkedDevice(
        signedBy keyPair: ECKeyPair
    ) throws -> SignalServiceKit.KyberPreKeyRecord

    func storeLastResortPreKeyFromLinkedDevice(
        record: KyberPreKeyRecord,
        tx: DBWriteTransaction
    ) throws

    func generateKyberPreKeyRecords(
        count: Int,
        signedBy keyPair: ECKeyPair,
        tx: DBWriteTransaction
    ) throws -> [SignalServiceKit.KyberPreKeyRecord]

    func storeLastResortPreKey(
        record: SignalServiceKit.KyberPreKeyRecord,
        tx: DBWriteTransaction
    ) throws

    func storeKyberPreKeyRecords(
        records: [SignalServiceKit.KyberPreKeyRecord],
        tx: DBWriteTransaction
    ) throws

    func cullLastResortPreKeyRecords(
        justUploadedLastResortPreKey: KyberPreKeyRecord,
        tx: DBWriteTransaction
    ) throws

    func removeLastResortPreKey(
        record: SignalServiceKit.KyberPreKeyRecord,
        tx: DBWriteTransaction
    )

    func cullOneTimePreKeyRecords(tx: DBWriteTransaction) throws

    func setLastSuccessfulRotationDate(
        _ date: Date,
        tx: DBWriteTransaction
    )

    func getLastSuccessfulRotationDate(
        tx: DBReadTransaction
    ) -> Date?

#if TESTABLE_BUILD
    func removeAll(tx: DBWriteTransaction)
#endif
}

public struct KyberPreKeyRecord: Codable {

    enum CodingKeys: String, CodingKey {
        case keyData
        case isLastResort
    }

    public let signature: Data
    public let generatedAt: Date
    public let id: UInt32
    public let keyPair: KEMKeyPair
    public let isLastResort: Bool

    public init(
        _ id: UInt32,
        keyPair: KEMKeyPair,
        signature: Data,
        generatedAt: Date,
        isLastResort: Bool
    ) {
        self.id = id
        self.keyPair = keyPair
        self.signature = signature
        self.generatedAt = generatedAt
        self.isLastResort = isLastResort
    }

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

        try container.encode(isLastResort, forKey: .isLastResort)
        try container.encode(Data(asLSCRecord().serialize()), forKey: .keyData)
    }

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

        let isLastResort = try container.decode(Bool.self, forKey: .isLastResort)
        let keyData = try container.decode(Data.self, forKey: .keyData)

        let record = try LibSignalClient.KyberPreKeyRecord(bytes: keyData)
        self.id = record.id
        self.keyPair = record.keyPair
        self.signature = Data(record.signature)
        self.generatedAt = Date(millisecondsSince1970: record.timestamp)
        self.isLastResort = isLastResort
    }
}

extension KyberPreKeyRecord: Equatable {
    public static func == (lhs: KyberPreKeyRecord, rhs: KyberPreKeyRecord) -> Bool {
        return (
            lhs.id == rhs.id
            && lhs.isLastResort == rhs.isLastResort
            && lhs.generatedAt == rhs.generatedAt
            && lhs.signature == rhs.signature
        )
    }
}

extension KyberPreKeyRecord: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(isLastResort)
        hasher.combine(generatedAt)
        hasher.combine(signature)
    }
}

public class SSKKyberPreKeyStore: SignalKyberPreKeyStore {

    internal enum Constants {
        internal enum ACI {
            static let keyStoreCollection = "SSKKyberPreKeyStoreACIKeyStore"
            static let metadataStoreCollection = "SSKKyberPreKeyStoreACIMetadataStore"
        }

        internal enum PNI {
            static let keyStoreCollection = "SSKKyberPreKeyStorePNIKeyStore"
            static let metadataStoreCollection = "SSKKyberPreKeyStorePNIMetadataStore"
        }

        static let lastKeyId = "lastKeyId"

        static let lastKeyRotationDate = "lastKeyRotationDate"

        static let oneTimeKeyExpirationInterval = kDayInterval * 90
        static let lastResortKeyExpirationInterval = kDayInterval * 30
    }

    let identity: OWSIdentity

    // Store both one-time and last resort keys
    private let keyStore: KeyValueStore

    // Store current last resort ID
    private let metadataStore: KeyValueStore

    private let dateProvider: DateProvider

    public init(
        for identity: OWSIdentity,
        keyValueStoreFactory: KeyValueStoreFactory,
        dateProvider: @escaping DateProvider
    ) {
        self.identity = identity
        self.dateProvider = dateProvider

        switch identity {
        case .aci:
            self.keyStore = keyValueStoreFactory.keyValueStore(collection: Constants.ACI.keyStoreCollection)
            self.metadataStore = keyValueStoreFactory.keyValueStore(collection: Constants.ACI.metadataStoreCollection)
        case .pni:
            self.keyStore = keyValueStoreFactory.keyValueStore(collection: Constants.PNI.keyStoreCollection)
            self.metadataStore = keyValueStoreFactory.keyValueStore(collection: Constants.PNI.metadataStoreCollection)
        }
    }

    private func nextKyberPreKeyId(minimumCapacity: UInt32 = 1, tx: DBReadTransaction) -> UInt32 {
        return PreKeyId.nextPreKeyId(
            lastPreKeyId: UInt32(exactly: metadataStore.getInt32(Constants.lastKeyId, defaultValue: 0, transaction: tx)) ?? 0,
            minimumCapacity: minimumCapacity
        )
    }

    private func generateKyberPreKeyRecord(
        id: UInt32,
        signedBy identityKeyPair: ECKeyPair,
        isLastResort: Bool
    ) throws -> KyberPreKeyRecord {
        let keyPair = KEMKeyPair.generate()
        let signature = Data(identityKeyPair.keyPair.privateKey.generateSignature(message: Data(keyPair.publicKey.serialize())))

        let record = KyberPreKeyRecord(
            id,
            keyPair: keyPair,
            signature: signature,
            generatedAt: dateProvider(),
            isLastResort: isLastResort
        )

        return record
    }

    // One-Time prekeys

    public func generateKyberPreKeyRecords(
        count: Int,
        signedBy keyPair: ECKeyPair,
        tx: DBWriteTransaction
    ) throws -> [KyberPreKeyRecord] {
        var nextKeyId = nextKyberPreKeyId(minimumCapacity: UInt32(count), tx: tx)
        let records = try (0..<count).map { _ in
            let record = try generateKyberPreKeyRecord(
                id: nextKeyId,
                signedBy: keyPair,
                isLastResort: false
            )
            nextKeyId += 1
            return record
        }
        metadataStore.setInt32(Int32(nextKeyId - 1), key: Constants.lastKeyId, transaction: tx)
        return records
    }

    // Last Resort Keys

    public func generateLastResortKyberPreKey(
        signedBy keyPair: ECKeyPair,
        tx: DBWriteTransaction
    ) throws -> SignalServiceKit.KyberPreKeyRecord {
        let keyId = nextKyberPreKeyId(tx: tx)
        let record = try generateKyberPreKeyRecord(
            id: keyId,
            signedBy: keyPair,
            isLastResort: true
        )
        metadataStore.setInt32(Int32(keyId), key: Constants.lastKeyId, transaction: tx)
        return record
    }

    public func generateLastResortKyberPreKeyForLinkedDevice(
        signedBy keyPair: ECKeyPair
    ) throws -> SignalServiceKit.KyberPreKeyRecord {
        return try generateKyberPreKeyRecord(
            id: PreKeyId.random(),
            signedBy: keyPair,
            isLastResort: true
        )
    }

    public func storeLastResortPreKey(record: KyberPreKeyRecord, tx: DBWriteTransaction) throws {
        try storeKyberPreKey(record: record, tx: tx)
    }

    public func storeLastResortPreKeyFromLinkedDevice(record: KyberPreKeyRecord, tx: DBWriteTransaction) throws {
        try storeLastResortPreKey(record: record, tx: tx)
        metadataStore.setInt32(Int32(record.id), key: Constants.lastKeyId, transaction: tx)
    }

    // MARK: - LibSignalClient.KyberPreKeyStore conformance

    public func loadKyberPreKey(id: UInt32, tx: DBReadTransaction) -> SignalServiceKit.KyberPreKeyRecord? {
        try? self.keyStore.getCodableValue(forKey: key(for: id), transaction: tx)
    }

    public func storeKyberPreKeyRecords(records: [KyberPreKeyRecord], tx: DBWriteTransaction) throws {
        for record in records {
            try storeKyberPreKey(record: record, tx: tx)
        }
    }

    private func storeKyberPreKey(record: SignalServiceKit.KyberPreKeyRecord, tx: DBWriteTransaction) throws {
        try self.keyStore.setCodable(record, key: key(for: record.id), transaction: tx)
    }

    public func markKyberPreKeyUsed(id: UInt32, tx: DBWriteTransaction) throws {
        // fetch the key, see if it's was a last resort.
        // if not, remove the key from the list of uses (or mark it as used?)
        guard let record = loadKyberPreKey(id: id, tx: tx) else { throw Error.noKyberPreKeyWithId(id) }
        if !record.isLastResort {
            self.keyStore.removeValue(forKey: key(for: id), transaction: tx)
        }
    }

#if TESTABLE_BUILD
    public func removeAll(tx: DBWriteTransaction) {
        self.keyStore.removeAll(transaction: tx)
        self.metadataStore.removeAll(transaction: tx)
    }
#endif
}

extension SSKKyberPreKeyStore {
    internal func key(for id: UInt32) -> String { "\(id)" }
}

extension SSKKyberPreKeyStore: LibSignalClient.KyberPreKeyStore {
    enum Error: Swift.Error {
        case noKyberPreKeyWithId(UInt32)
        case noKyberLastResortKey
    }

    public func loadKyberPreKey(
        id: UInt32,
        context: LibSignalClient.StoreContext
    ) throws -> LibSignalClient.KyberPreKeyRecord {
        guard let preKey = self.loadKyberPreKey(
            id: id,
            tx: context.asTransaction.asV2Read
        ) else {
            throw Error.noKyberPreKeyWithId(id)
        }

        return try LibSignalClient.KyberPreKeyRecord(
            id: preKey.id,
            timestamp: preKey.generatedAt.ows_millisecondsSince1970,
            keyPair: preKey.keyPair,
            signature: preKey.signature
        )
    }

    // This method isn't used in practice, so it doesn't matter
    // if this is a one time or last resort Kyber key
    public func storeKyberPreKey(
        _ record: LibSignalClient.KyberPreKeyRecord,
        id: UInt32,
        context: LibSignalClient.StoreContext
    ) throws {
        let record = SignalServiceKit.KyberPreKeyRecord(
            id,
            keyPair: record.keyPair,
            signature: Data(record.signature),
            generatedAt: Date(millisecondsSince1970: record.timestamp),
            isLastResort: false
        )
        try self.storeKyberPreKey(record: record, tx: context.asTransaction.asV2Write)
    }

    public func markKyberPreKeyUsed(id: UInt32, context: StoreContext) throws {
        try self.markKyberPreKeyUsed(id: id, tx: context.asTransaction.asV2Write)
    }

    public func setLastSuccessfulRotationDate(_ date: Date, tx: DBWriteTransaction) {
        self.metadataStore.setDate(date, key: Constants.lastKeyRotationDate, transaction: tx)
    }

    public func getLastSuccessfulRotationDate(tx: DBReadTransaction) -> Date? {
        self.metadataStore.getDate(Constants.lastKeyRotationDate, transaction: tx)
    }

    public func cullOneTimePreKeyRecords(tx: DBWriteTransaction) throws {

        // get all keys
        // filter by isLastResort = false
        // remove all keys older than 90 days

        let recordsToRemove: [KyberPreKeyRecord] = try self.keyStore
            .allCodableValues(transaction: tx)
            .filter { record in
                guard !record.isLastResort else { return false }
                let keyAge = dateProvider().timeIntervalSince(record.generatedAt)
                return keyAge >= Constants.oneTimeKeyExpirationInterval
            }

        let keysToRemove = recordsToRemove.map { key(for: $0.id) }
        self.keyStore.removeValues(forKeys: keysToRemove, transaction: tx)
    }

    public func cullLastResortPreKeyRecords(
        justUploadedLastResortPreKey: KyberPreKeyRecord,
        tx: DBWriteTransaction
    ) throws {
        // get a list of keys
        // don't touch what we just uploaded
        // remove all others older than 30 days

        let recordsToRemove: [KyberPreKeyRecord] = try self.keyStore
            .allCodableValues(transaction: tx)
            .filter { record in
                guard record.isLastResort else { return false }
                guard record.id != justUploadedLastResortPreKey.id else { return false }
                let keyAge = dateProvider().timeIntervalSince(record.generatedAt)
                return keyAge >= Constants.lastResortKeyExpirationInterval
            }

        let keysToRemove = recordsToRemove.map { key(for: $0.id) }
        self.keyStore.removeValues(forKeys: keysToRemove, transaction: tx)
    }

    public func removeLastResortPreKey(
        record: SignalServiceKit.KyberPreKeyRecord,
        tx: DBWriteTransaction
    ) {
        self.keyStore.removeValue(forKey: key(for: record.id), transaction: tx)
    }
}

extension LibSignalClient.KyberPreKeyRecord {
    func asSSKLastResortRecord() -> SignalServiceKit.KyberPreKeyRecord {
        return SignalServiceKit.KyberPreKeyRecord(
            self.id,
            keyPair: self.keyPair,
            signature: Data(self.signature),
            generatedAt: Date(millisecondsSince1970: self.timestamp),
            isLastResort: true
        )
    }
}

extension SignalServiceKit.KyberPreKeyRecord {
    func asLSCRecord() throws -> LibSignalClient.KyberPreKeyRecord {
        try LibSignalClient.KyberPreKeyRecord(
            id: self.id,
            timestamp: self.generatedAt.ows_millisecondsSince1970,
            keyPair: self.keyPair,
            signature: self.signature
        )
    }
}
