//
//  GopenPGPInterface.swift
//  passKit
//
//  Created by Danny Moesch on 08.09.19.
//  Copyright © 2019 Bob Sun. All rights reserved.
//

import Gopenpgp

struct GopenPGPInterface: PGPInterface {
    private static let errorMapping: [String: Error] = [
        "gopenpgp: error in unlocking key: openpgp: invalid data: private key checksum failure": AppError.wrongPassphrase,
        "gopenpgp: error in reading message: openpgp: incorrect key": AppError.keyExpiredOrIncompatible,
    ]

    private var publicKeys: [String: CryptoKey] = [:]
    private var privateKeys: [String: CryptoKey] = [:]

    init(publicArmoredKey: String, privateArmoredKey: String) throws {
        let pubKeys = extractKeysFromArmored(str: publicArmoredKey)
        let prvKeys = extractKeysFromArmored(str: privateArmoredKey)

        for key in pubKeys {
            var error: NSError?
            guard let cryptoKey = CryptoNewKeyFromArmored(key, &error) else {
                guard error == nil else {
                    throw error!
                }
                throw AppError.keyImport
            }
            publicKeys[cryptoKey.getFingerprint().lowercased()] = cryptoKey
        }

        for key in prvKeys {
            var error: NSError?
            guard let cryptoKey = CryptoNewKeyFromArmored(key, &error) else {
                guard error == nil else {
                    throw error!
                }
                throw AppError.keyImport
            }
            privateKeys[cryptoKey.getFingerprint().lowercased()] = cryptoKey
        }
    }

    func extractKeysFromArmored(str: String) -> [String] {
        var keys: [String] = []
        var key = ""
        for line in str.splitByNewline() {
            if line.trimmed.uppercased().hasPrefix("-----BEGIN PGP") {
                key = ""
                key += line
            } else if line.trimmed.uppercased().hasPrefix("-----END PGP") {
                key += line
                keys.append(key)
            } else {
                key += line
            }
            key += "\n"
        }
        return keys
    }

    func containsPublicKey(with keyID: String) -> Bool {
        publicKeys.keys.contains { key in key.hasSuffix(keyID.lowercased()) }
    }

    func containsPrivateKey(with keyID: String) -> Bool {
        privateKeys.keys.contains { key in key.hasSuffix(keyID.lowercased()) }
    }

    func decrypt(encryptedData: Data, keyID: String?, passphrase: String) throws -> Data? {
        let key: CryptoKey? = {
            if let keyID {
                return privateKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value
            }
            return privateKeys.first?.value
        }()

        guard let privateKey = key else {
            throw AppError.decryption
        }

        do {
            var isLocked: ObjCBool = false
            try privateKey.isLocked(&isLocked)
            var unlockedKey: CryptoKey!
            if isLocked.boolValue {
                unlockedKey = try privateKey.unlock(passphrase.data(using: .utf8))
            } else {
                unlockedKey = privateKey
            }
            var error: NSError?

            guard let keyRing = CryptoNewKeyRing(unlockedKey, &error) else {
                guard error == nil else {
                    throw error!
                }
                throw AppError.decryption
            }

            let message = createPGPMessage(from: encryptedData)
            return try keyRing.decrypt(message, verifyKey: nil, verifyTime: 0).data
        } catch {
            throw Self.errorMapping[error.localizedDescription, default: error]
        }
    }

    func encrypt(plainData: Data, keyID: String?) throws -> Data {
        let key: CryptoKey? = {
            if let keyID {
                return publicKeys.first(where: { key, _ in key.hasSuffix(keyID.lowercased()) })?.value
            }
            return publicKeys.first?.value
        }()

        guard let publicKey = key else {
            throw AppError.encryption
        }

        var error: NSError?

        guard let keyRing = CryptoNewKeyRing(publicKey, &error) else {
            guard error == nil else {
                throw error!
            }
            throw AppError.encryption
        }

        let encryptedData = try keyRing.encrypt(CryptoNewPlainMessage(plainData.mutable as Data), privateKey: nil)
        if Defaults.encryptInArmored {
            var error: NSError?
            let armor = encryptedData.getArmored(&error)
            guard error == nil else {
                throw error!
            }
            return armor.data(using: .ascii)!
        }
        return encryptedData.getBinary()!
    }

    var keyID: [String] {
        publicKeys.keys.map { $0.uppercased() }
    }

    var shortKeyID: [String] {
        publicKeys.keys.map { $0.suffix(8).uppercased() }
    }
}

public func createPGPMessage(from encryptedData: Data) -> CryptoPGPMessage? {
    // Important note:
    // Even if Defaults.encryptInArmored is true now, it could be different during the encryption.
    var error: NSError?
    let message = CryptoNewPGPMessageFromArmored(String(data: encryptedData, encoding: .ascii), &error)
    if error == nil {
        return message
    }
    return CryptoNewPGPMessage(encryptedData.mutable as Data)
}
