//
//  KeychainService.swift
//  TrustBuilderMfaSampleApp
//
//  Created by Ludovic Lefevre on 16/03/2023.
//

import Foundation
import Security
import UIKit
import TrustBuilderMfaLibSwift
import os.log
import LocalAuthentication

public class KeychainService {
    let decoder = JSONDecoder()
    
    public func SaveItem(data : Data, itemId : String, label : String ) throws{
        let attributes: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemId,
            kSecValueData as String: data,
            kSecAttrLabel as String: label
        ]
        if SecItemAdd(attributes as CFDictionary, nil) == noErr {
            os_log(.error, "User saved successfully in the keychain")
        } else {
            os_log(.error, "Something went wrong trying to save the data in the keychain")
            throw ApplicationError.KEYCHAIN_AUTH_ERROR("Something went wrong trying to save the data in the keychain")
        }
    }
    
    public func UpdateItem(data : Data, itemId : String, label : String ) throws{
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemId,
            kSecAttrLabel as String: label
        ]
        let attributes: [String: Any] = [kSecValueData as String: data]

        if SecItemUpdate(query as CFDictionary, attributes as CFDictionary) == noErr {
            os_log(.error, "Keychain updated")
        } else {
            os_log(.error, "Keychain update error")
            throw ApplicationError.KEYCHAIN_AUTH_ERROR("Keychain update error")
        }
    }
    
    public func GetItem(itemId: String, label : String ) throws -> String? {
        let query: [String: Any] = [
                  kSecClass as String: kSecClassGenericPassword,
                  kSecAttrAccount as String: itemId,
                  kSecMatchLimit as String: kSecMatchLimitOne,
                  kSecReturnAttributes as String: true,
                  kSecReturnData as String: true,
                  kSecAttrLabel as String: label
              ]
              var item: CFTypeRef?
              if SecItemCopyMatching(query as CFDictionary, &item) == noErr {
                  if let existingItem = item as? [String: Any],
                     let passwordData = existingItem[kSecValueData as String] as? Data,
                     let password = String(data: passwordData, encoding: .utf8)
                  {
                      return password
                  }
              } else {
                  os_log(.error, "No keychain found")
              }
            os_log(.error, "Error in keychain")
        return nil
    }
    
    public func DeleteItem(itemId: String, label : String) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemId,
            kSecAttrLabel as String: label
        ];

        // Remove any old values from the keychain
        let osStatus = SecItemDelete(query as CFDictionary)
        if osStatus != noErr && osStatus != errSecItemNotFound {
            os_log(.error, "Keychain deletion error")
        }
    }
    
    public func GetAllItems(label : String ) throws -> [String]  {
        var values : [String] = []
        let query: [String: Any] = [
               kSecClass as String : kSecClassGenericPassword,
               kSecReturnData as String  : kCFBooleanTrue,
               kSecReturnAttributes as String : kCFBooleanTrue,
               kSecReturnRef as String : kCFBooleanTrue,
               kSecMatchLimit as String: kSecMatchLimitAll,
               kSecAttrLabel as String: label
           ]
        
           var result: AnyObject?

           let lastResultCode = withUnsafeMutablePointer(to: &result) {
               SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
           }
        
           if lastResultCode == noErr {
               let array = result as? Array<Dictionary<String, Any>>
                           
               for item in array! {
                   if let existingItem = item as? [String: Any],
                      let passwordData = existingItem[kSecValueData as String] as? Data,
                      let password = String(data: passwordData, encoding: .utf8)
                   {
                       values.append(password)
                   }
                }
           }
        return values
    }
    
    public func saveItemBio(privateKey : Data, itemId : String, label : String) throws{
        let context = LAContext()
        let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                    kSecAttrAccount as String: itemId,
                                    kSecAttrAccessControl as String: getBioSecAccessControl(),
                                    kSecUseAuthenticationContext as String: context,
                                    kSecValueData as String: privateKey,
                                    kSecAttrLabel as String: label]
        let attributes: [String: Any] = [kSecValueData as String: privateKey]
        if SecItemAdd(query as CFDictionary, nil) == noErr {
            os_log(.error, "User saved successfully in the keychain")
        } else if SecItemUpdate(query as CFDictionary, attributes as CFDictionary) == noErr {
            os_log(.error, "User saved successfully in the keychain")
        } else {
            os_log(.error, "Something went wrong trying to save the data in the keychain")
            throw ApplicationError.BIO_AUTH_ERROR( "Something went wrong trying to save the data in the keychain")
        }
    }
    
    public func GetItemBio(itemId: String, label : String, context: LAContext? = nil,  prompt: String? = nil) async throws -> String? {
        let bioState = self.biometryState
        let authContext = LAContext()
        guard bioState != .notAvailable else {
            throw ApplicationError.BIO_AUTH_ERROR("bio no available")
        }
        if bioState == .locked {
            authContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication,
                                                 localizedReason: "Access sample keychain entry",
                                                 reply: { (success, error) in
                          DispatchQueue.main.async {
                              if success {
                                  os_log(.error, "DispatchQueue.main.async sucess")
                              } else {
                                  os_log(.error, "DispatchQueue.main.async error")
                              }
                          }
                      })
        }
        authContext.localizedReason = "Access your password on the keychain"
        let accessControl = getBioSecAccessControl()
        var res = try await authContext.evaluateAccessControl(accessControl,
                                                              operation: .useItem,
                                                              localizedReason: "Access sample keychain entry")
        
        var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemId,
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecReturnAttributes as String: true,
            kSecReturnData as String: true,
            kSecAttrAccessControl as String: getBioSecAccessControl(),
            kSecAttrLabel as String: label,
            kSecUseAuthenticationContext as String : authContext,
            kSecUseAuthenticationUI as String : kSecUseAuthenticationUISkip
        ]
        var item: CFTypeRef?
        if SecItemCopyMatching(query as CFDictionary, &item) == noErr {
            if let existingItem = item as? [String: Any],
               let passwordData = existingItem[kSecValueData as String] as? Data,
               let password = String(data: passwordData, encoding: .utf8)
            {
                return password
            }
        } else {
            os_log(.error, "No keychain found for bio")
            throw ApplicationError.BIO_AUTH_ERROR("No keychain found for bio")
        }
        os_log(.error, "Error in keychain for bio")
        throw ApplicationError.BIO_AUTH_ERROR("Error in keychain for bio")
        
    }
    
    func getBioSecAccessControl() -> SecAccessControl {
        var access: SecAccessControl?
        var error: Unmanaged<CFError>?
        
        if #available(iOS 11.3, *) {
            access = SecAccessControlCreateWithFlags(nil,
                                                     kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                                     .biometryCurrentSet,
                                                     &error)
        } else {
            access = SecAccessControlCreateWithFlags(nil,
                                                     kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                                     .touchIDCurrentSet,
                                                     &error)
        }
        precondition(access != nil, "SecAccessControlCreateWithFlags failed")
        return access!
    }
    
    enum BiometryState {
        case available, locked, notAvailable
    }
    
    var biometryState: BiometryState {
        let authContext = LAContext()
        var error: NSError?
        
        let biometryAvailable = authContext.canEvaluatePolicy(
            LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error)
        if let laError = error as? LAError, laError.code == LAError.Code.biometryLockout {
            return .locked
        }
        return biometryAvailable ? .available : .notAvailable
    }
}
