/*
Copyright 2018-2024 New Vector Ltd.
Copyright (c) 2021 BWI GmbH

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import Foundation
import KeychainAccess
import LocalAuthentication
import MatrixSDK

extension Notification.Name {
    /// Posted when fallbacking to PIN from a biometrics protection
    static let biometricsDidFallbackToPin = Notification.Name("BiometricsDidFallbackToPin")
}

/// Pin code preferences.
@objcMembers
final class PinCodePreferences: NSObject {
    
    // MARK: - Constants
    
    private struct PinConstants {
        static let pinCodeKeychainService: String = BuildSettings.baseBundleIdentifier + ".pin-service"
    }
    
    private struct StoreKeys {
        static let pin: String = "pin"
        static let biometricsEnabled: String = "biometricsEnabled"
        static let canUseBiometricsToUnlock: String = "canUseBiometricsToUnlock"
        static let numberOfPinFailures: String = "numberOfPinFailures"
        static let numberOfBiometricsFailures: String = "numberOfBiometricsFailures"
    }
    
    static let shared = PinCodePreferences()
    
    /// Store. Defaults to `SecureFileStore`
    private let secureStore: SecureFileStorage
    
    override private init() {
        secureStore = SecureFileStorage.shared
        super.init()
        migrateOldData()
    }

    private func migrateOldData() {
        let oldStore = KeychainVault(Keychain(service: PinConstants.pinCodeKeychainService,
                                                            accessGroup: BuildSettings.keychainAccessGroup))

        if let oldPin = try? oldStore.string(forKey: StoreKeys.pin) {
            do {
                //  migrate
                pin = oldPin
                //  clear old pin
                try oldStore.removeObject(forKey: StoreKeys.pin)
            } catch let error {
                MXLog.error("[PinCodePreferences] Error when migrating old user pin:")
            }
        }
        
        if let oldBiometricsEnabled = try? oldStore.bool(forKey: StoreKeys.biometricsEnabled) {
            biometricsEnabled = oldBiometricsEnabled
            try? oldStore.removeObject(forKey: StoreKeys.biometricsEnabled)
        }
        
        if let oldCanUseBiometricsToUnlock = try? oldStore.bool(forKey: StoreKeys.canUseBiometricsToUnlock) {
            canUseBiometricsToUnlock = oldCanUseBiometricsToUnlock
            try? oldStore.removeObject(forKey: StoreKeys.canUseBiometricsToUnlock)
        }
        
        if let oldNumberOfPinFailures = try? oldStore.integer(forKey: StoreKeys.numberOfPinFailures) {
            numberOfPinFailures = oldNumberOfPinFailures
            try? oldStore.removeObject(forKey: StoreKeys.numberOfPinFailures)
        }
        
        if let oldNumberOfBiometricsFailures = try? oldStore.integer(forKey: StoreKeys.numberOfBiometricsFailures) {
            numberOfBiometricsFailures = oldNumberOfBiometricsFailures
            try? oldStore.removeObject(forKey: StoreKeys.numberOfBiometricsFailures)
        }
    }

    
    // MARK: - Public
    
    /// Setting to force protection by pin code
    var forcePinProtection: Bool {
        return BWIBuildSettings.shared.forcePinProtection
    }
    
    /// Not allowed pin codes. User won't be able to select one of the pin in the list.
    var notAllowedPINs: [String] {
        return BWIBuildSettings.shared.notAllowedPINs
    }
    
    /// Maximum number of allowed pin failures when unlocking, before force logging out the user
    var maxAllowedNumberOfPinFailures: Int {
        return BuildSettings.maxAllowedNumberOfPinFailures
    }
    
    /// Maximum number of allowed biometrics failures when unlocking, before fallbacking the user to the pin
    var maxAllowedNumberOfBiometricsFailures: Int {
        return BuildSettings.maxAllowedNumberOfBiometricsFailures
    }
    
    var isBiometricsAvailable: Bool {
        let context = LAContext()
        var error: NSError?

        let result = LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)

        // While in lockout they're still techincally available
        if error?.code == LAError.Code.biometryLockout.rawValue {
            return true
        }

        if result == true {
             return context.biometryType == .touchID || context.biometryType == .faceID
        }

        return false
    }
    
    /// Allowed number of PIN trials before showing forgot help alert
    let allowedNumberOfTrialsBeforeAlert: Int = 5
    
    /// Max allowed time to continue using the app without prompting PIN
    var graceTimeInSeconds: TimeInterval {
        return BWIBuildSettings.shared.pinCodeGraceTimeInSeconds
    }
    
    /// Number of digits for the PIN
    let numberOfDigits: Int = 4
    
    /// Is user has set a pin
    var isPinSet: Bool {
        return secureStore.objectExists(withKey: StoreKeys.pin)
    }
    
    /// Saved user PIN
    var pin: String? {
        get {
            do {
                return try secureStore.string(forKey: StoreKeys.pin)
            } catch let error {
                MXLog.debug("[PinCodePreferences] Error when reading user pin from store: \(error)")
                return nil
            }
        } set {
            do {
                if let newPin = newValue {
                    if !secureStore.locked {
                        try secureStore.update(passphrase: newPin)
                    } else {
                        try secureStore.unlock(passphrase: newPin)
                    }
                }
                try secureStore.set(newValue, forKey: StoreKeys.pin)
            } catch let error {
                MXLog.debug("[PinCodePreferences] Error when storing user pin to the store: \(error)")
            }
        }
    }
    
    func pinsMatch(_ pinCode: String) -> Bool {
        do {
            try secureStore.unlock(passphrase: pinCode)
            let pin = try secureStore.string(forKey: StoreKeys.pin)
            return pinCode == pin
        } catch let error {
            MXLog.error("[PinCodePreferences] Error when checking user pin")
            return false
        }
    }
    
    var biometricsEnabled: Bool {
        get {
            return RiotSettings.shared.biometricsEnabled
        } set {
            RiotSettings.shared.biometricsEnabled = newValue
        }
    }
    
    var canUseBiometricsToUnlock: Bool? {
        get {
            guard isBiometricsAvailable == true else {
                return false
            }
            
            do {
                return try secureStore.bool(forKey: StoreKeys.canUseBiometricsToUnlock)
            } catch {
                MXLog.debug("[PinCodePreferences] Error when reading canUseBiometricsToUnlock from store")
                return nil
            }
        } set {
            do {
                try secureStore.set(newValue, forKey: StoreKeys.canUseBiometricsToUnlock)
            } catch {
                MXLog.debug("[PinCodePreferences] Error when storing canUseBiometricsToUnlock to the store")
            }
        }
    }
    
    var numberOfPinFailures: Int {
        get {
            return RiotSettings.shared.numberOfPinFailures
        } set {
            RiotSettings.shared.numberOfPinFailures = newValue
        }
    }
    
    var numberOfBiometricsFailures: Int {
        get {
            return RiotSettings.shared.numberOfBiometricsFailures
        } set {
            RiotSettings.shared.numberOfBiometricsFailures = newValue
        }
    }
    
    var isBiometricsSet: Bool {
        return biometricsEnabled && (canUseBiometricsToUnlock ?? true) && isBiometricsAvailable
    }

    func localizedBiometricsName() -> String? {
        if isBiometricsAvailable {
            let context = LAContext()
            //  canEvaluatePolicy should be called for biometryType to be set
            _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
            switch context.biometryType {
            case .touchID:
                return VectorL10n.biometricsModeTouchId
            case .faceID:
                return VectorL10n.biometricsModeFaceId
            default:
                return nil
            }
        }
        return nil
    }
    
    func biometricsIcon() -> UIImage? {
        if isBiometricsAvailable {
            let context = LAContext()
            //  canEvaluatePolicy should be called for biometryType to be set
            _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
            switch context.biometryType {
            case .touchID:
                return Asset.Images.touchidIcon.image
            case .faceID:
                return Asset.Images.faceidIcon.image
            default:
                return nil
            }
        }
        return nil
    }
    
    /// Resets user PIN
    func reset() {
        pin = nil
        biometricsEnabled = false
        canUseBiometricsToUnlock = true
        resetCounters()
    }
    
    /// Reset number of failures for both pin and biometrics
    func resetCounters() {
        numberOfPinFailures = 0
        numberOfBiometricsFailures = 0
    }
}
