import Foundation
import LocalAuthentication

enum PluginError:Int {
    case BIOMETRIC_UNKNOWN_ERROR = -100
    case BIOMETRIC_UNAVAILABLE = -101
    case BIOMETRIC_AUTHENTICATION_FAILED = -102
    case BIOMETRIC_PERMISSION_NOT_GRANTED = -105
    case BIOMETRIC_NOT_ENROLLED = -106
    case BIOMETRIC_DISMISSED = -108
    case BIOMETRIC_SCREEN_GUARD_UNSECURED = -110
    case BIOMETRIC_LOCKED_OUT = -111
    case BIOMETRIC_SECRET_NOT_FOUND = -113
}

@objc(Fingerprint) class Fingerprint : CDVPlugin {

    struct ErrorCodes {
        var code: Int
    }


    @objc(isAvailable:)
    func isAvailable(_ command: CDVInvokedUrlCommand){
        let authenticationContext = LAContext();
        var biometryType = "finger";
        var errorResponse: [AnyHashable: Any] = [
            "code": 0,
            "message": "Not Available"
        ];
        var error:NSError?;
        let params = command.argument(at: 0) as? [AnyHashable: Any] ?? [:]
        let allowBackup = params["allowBackup"] as? Bool ?? false
        let policy:LAPolicy = allowBackup ? .deviceOwnerAuthentication : .deviceOwnerAuthenticationWithBiometrics;
        var pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: "Not available");
        let available = authenticationContext.canEvaluatePolicy(policy, error: &error);

        var results: [String : Any]

        if(error != nil){
            biometryType = "none";
            errorResponse["code"] = error?.code;
            errorResponse["message"] = error?.localizedDescription;
        }

        if (available == true) {
            if #available(iOS 11.0, *) {
                switch(authenticationContext.biometryType) {
                case .none:
                    biometryType = "none";
                case .touchID:
                    biometryType = "finger";
                case .faceID:
                    biometryType = "face"
                @unknown default:
                    errorResponse["message"] = "Unkown biometry type"
                }
            }

            pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: biometryType);
        }else{
            var code: Int;
            switch(error!._code) {
                case Int(kLAErrorBiometryNotAvailable):
                    code = PluginError.BIOMETRIC_UNAVAILABLE.rawValue;
                    break;
                case Int(kLAErrorBiometryNotEnrolled):
                    code = PluginError.BIOMETRIC_NOT_ENROLLED.rawValue;
                    break;

                default:
                    code = PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue;
                    break;
            }
            results = ["code": code, "message": error!.localizedDescription];
            pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: results);
        }

        commandDelegate.send(pluginResult, callbackId:command.callbackId);
    }

    func justAuthenticate(_ command: CDVInvokedUrlCommand) {
        let authenticationContext = LAContext();
        let errorResponse: [AnyHashable: Any] = [
            "message": "Something went wrong"
        ];
        var pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResponse);
        var reason = "Authentication";
        var policy:LAPolicy = .deviceOwnerAuthentication;
        let data  = command.arguments[0] as? [String: Any];

        if let disableBackup = data?["disableBackup"] as! Bool? {
            if disableBackup {
                authenticationContext.localizedFallbackTitle = "";
                policy = .deviceOwnerAuthenticationWithBiometrics;
            } else {
                if let fallbackButtonTitle = data?["fallbackButtonTitle"] as! String? {
                    authenticationContext.localizedFallbackTitle = fallbackButtonTitle;
                }else{
                    authenticationContext.localizedFallbackTitle = "Use Pin";
                }
            }
        }

        // Localized reason
        if let description = data?["description"] as! String? {
            reason = description;
        }

        authenticationContext.evaluatePolicy(
            policy,
            localizedReason: reason,
            reply: { [unowned self] (success, error) -> Void in
                if( success ) {
                    pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "Success");
                }else {
                    if (error != nil) {

                        var errorCodes = [Int: ErrorCodes]()
                        var errorResult: [String : Any] = ["code":  PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue, "message": error?.localizedDescription ?? ""];

                        errorCodes[1] = ErrorCodes(code: PluginError.BIOMETRIC_AUTHENTICATION_FAILED.rawValue)
                        errorCodes[2] = ErrorCodes(code: PluginError.BIOMETRIC_DISMISSED.rawValue)
                        errorCodes[5] = ErrorCodes(code: PluginError.BIOMETRIC_SCREEN_GUARD_UNSECURED.rawValue)
                        errorCodes[6] = ErrorCodes(code: PluginError.BIOMETRIC_UNAVAILABLE.rawValue)
                        errorCodes[7] = ErrorCodes(code: PluginError.BIOMETRIC_NOT_ENROLLED.rawValue)
                        errorCodes[8] = ErrorCodes(code: PluginError.BIOMETRIC_LOCKED_OUT.rawValue)

                        let errorCode = abs(error!._code)
                        if let e = errorCodes[errorCode] {
                           errorResult = ["code": e.code, "message": error!.localizedDescription];
                        }

                        pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResult);
                    }
                }
                self.commandDelegate.send(pluginResult, callbackId:command.callbackId);
            }
        );
    }

    func saveSecret(_ secretStr: String, command: CDVInvokedUrlCommand) {
        let data  = command.arguments[0] as AnyObject?;
        var pluginResult: CDVPluginResult
        do {
            let secret = Secret()
            try? secret.delete()
            let invalidateOnEnrollment = (data?.object(forKey: "invalidateOnEnrollment") as? Bool) ?? false
            try secret.save(secretStr, invalidateOnEnrollment: invalidateOnEnrollment)
            pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "Success");
        } catch {
            let errorResult = ["code": PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue, "message": error.localizedDescription] as [String : Any];
            pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResult);
        }
        self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
        return
    }


    func loadSecret(_ command: CDVInvokedUrlCommand) {
        let data  = command.arguments[0] as AnyObject?;
        var prompt = "Authentication"
        if let description = data?.object(forKey: "description") as! String? {
            prompt = description;
        }
        var pluginResult: CDVPluginResult
        do {
            let result = try Secret().load(prompt)
            pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: result);
        } catch {
            var code = PluginError.BIOMETRIC_UNKNOWN_ERROR.rawValue
            var message = error.localizedDescription
            if let err = error as? KeychainError {
                code = err.pluginError.rawValue
                message = err.localizedDescription
            }
            let errorResult = ["code": code, "message": message] as [String : Any]
            pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: errorResult);
        }
        self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
    }

    @objc(authenticate:)
    func authenticate(_ command: CDVInvokedUrlCommand){
        justAuthenticate(command)
    }

    @objc(registerBiometricSecret:)
    func registerBiometricSecret(_ command: CDVInvokedUrlCommand){
        let data  = command.arguments[0] as AnyObject?;
        if let secret = data?.object(forKey: "secret") as? String {
            self.saveSecret(secret, command: command)
            return
        }
    }

    @objc(loadBiometricSecret:)
    func loadBiometricSecret(_ command: CDVInvokedUrlCommand){
        self.loadSecret(command)
    }

    override func pluginInitialize() {
        super.pluginInitialize()
    }

}

/// Keychain errors we might encounter.
struct KeychainError: Error {
    var status: OSStatus

    var localizedDescription: String {
        if #available(iOS 11.3, *) {
            if let result = SecCopyErrorMessageString(status, nil) as String? {
                return result
            }
        }
        switch status {
            case errSecItemNotFound:
                return "Secret not found"
            case errSecUserCanceled:
                return "Biometric dissmissed"
            case errSecAuthFailed:
                return "Authentication failed"
            default:
                return "Unknown error \(status)"
        }
    }

    var pluginError: PluginError {
        switch status {
        case errSecItemNotFound:
            return PluginError.BIOMETRIC_SECRET_NOT_FOUND
        case errSecUserCanceled:
            return PluginError.BIOMETRIC_DISMISSED
        case errSecAuthFailed:
                return PluginError.BIOMETRIC_AUTHENTICATION_FAILED
        default:
            return PluginError.BIOMETRIC_UNKNOWN_ERROR
        }
    }
}

class Secret {

    private static let keyName: String = "__aio_key"

    private func getBioSecAccessControl(invalidateOnEnrollment: Bool) -> SecAccessControl {
        var access: SecAccessControl?
        var error: Unmanaged<CFError>?

        if #available(iOS 11.3, *) {
            access = SecAccessControlCreateWithFlags(nil,
                kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                invalidateOnEnrollment ? .biometryCurrentSet : .userPresence,
                &error)
        } else {
            access = SecAccessControlCreateWithFlags(nil,
                kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                invalidateOnEnrollment ? .touchIDCurrentSet : .userPresence,
                &error)
        }
        precondition(access != nil, "SecAccessControlCreateWithFlags failed")
        return access!
    }

    func save(_ secret: String, invalidateOnEnrollment: Bool) throws {
        let password = secret.data(using: String.Encoding.utf8)!

        // Allow a device unlock in the last 10 seconds to be used to get at keychain items.
        // let context = LAContext()
        // context.touchIDAuthenticationAllowableReuseDuration = 10

        // Build the query for use in the add operation.
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: Secret.keyName,
                                    kSecAttrAccessControl as String: getBioSecAccessControl(invalidateOnEnrollment: invalidateOnEnrollment),
                                    kSecValueData as String: password]

        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else { throw KeychainError(status: status) }
    }

    func load(_ prompt: String) throws -> String {
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: Secret.keyName,
                                    kSecMatchLimit as String: kSecMatchLimitOne,
                                    kSecReturnData as String : kCFBooleanTrue,
                                    kSecAttrAccessControl as String: getBioSecAccessControl(invalidateOnEnrollment: true),
                                    kSecUseOperationPrompt as String: prompt]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        guard status == errSecSuccess else { throw KeychainError(status: status) }

        guard let passwordData = item as? Data,
            let password = String(data: passwordData, encoding: String.Encoding.utf8)
            // let account = existingItem[kSecAttrAccount as String] as? String
            else {
                throw KeychainError(status: errSecInternalError)
        }

        return password
    }

    func delete() throws {
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: Secret.keyName]

        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess else { throw KeychainError(status: status) }
    }
}
