//
/*
 * Copyright (c) 2021 BWI GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import Foundation
import MatrixSDK


class SecureFileStorage {
    
    static let shared: SecureFileStorage = SecureFileStorage(withFilename: "shared")
    
    private let fileOperationQueue = DispatchQueue(label: "SecureFileStoreQueue")

    private(set) var locked: Bool = true
    private var fileURL: URL!
    private let fileName: String
    private let directoryName: String = "SecureFiles"

    private var crypto: Cryptable!
    private let saltKeyName: String = "salt"
    private let iterationsKeyName: String = "iterations"
    private var memoryVault: MemoryVault!
    private var dict: [String: Data] = [:]

    private enum SecureStorageError: Error {
        case locked
        case noData
    }
    
    init(withFilename fileName: String) {
        self.fileName = fileName
        setupFilePath()
        
        let fileContent = loadFromFile()
        memoryVault = MemoryVault(fileContent)
        
        let salt = try? memoryVault.string(forKey: saltKeyName)
        let iterations = try? memoryVault.unsignedInteger(forKey: iterationsKeyName)
        if iterations == nil && salt == nil {
            do {
                try memoryVault.set(makeSalt(), forKey: saltKeyName)
                dict[saltKeyName] = makeSalt().data(using: .utf8)
                try memoryVault.set(BWIBuildSettings.shared.iterationsForSecureStorage, forKey: iterationsKeyName)
                dict[iterationsKeyName] = String(BWIBuildSettings.shared.iterationsForSecureStorage).data(using: .utf8)
                
                saveToFile()
            } catch {
            }
        }
    }
    
    // MARK: -

    private func setupFilePath() {
        var cachePath: URL!
        
        if let appGroupIdentifier = MXSDKOptions.sharedInstance().applicationGroupIdentifier {
            cachePath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
        } else {
            cachePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
        }
        
        fileURL = cachePath.appendingPathComponent(directoryName).appendingPathComponent(fileName)
        
        fileOperationQueue.async {
            try? FileManager.default.createDirectory(at: self.fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
        }
    }
    
    private func loadFromFile() -> [String: Any] {
        var fileContent: [String: Any] = [:]
        fileOperationQueue.sync {
            if let dict = NSDictionary(contentsOf: self.fileURL) as? [String: Any] {
                fileContent = dict
            }
        }
        return fileContent
    }
    
    private func saveToFile() {
        fileOperationQueue.async {
            (self.memoryVault.dict as NSDictionary).write(to: self.fileURL, atomically: true)
        }
    }
    
    // MARK: -
    
    private func makeSalt() -> String {
        let secret = ProcessInfo.processInfo.globallyUniqueString
        let index = secret.index(secret.startIndex, offsetBy: 32)
        return String(secret.prefix(upTo: index))
    }
    
    func update(passphrase: String) throws {
        guard !locked else {
            throw SecureStorageError.locked
        }
        guard let salt = try? memoryVault.string(forKey: saltKeyName), let iterations = try? memoryVault.unsignedInteger(forKey: iterationsKeyName) else {
            throw SecureStorageError.noData
        }
        
        let encryptionKey = try MXKeyBackupPassword.retrievePrivateKey(withPassword: passphrase, salt: salt, iterations: iterations)
        crypto = try CryptoAES(key: encryptionKey)
        
        try dict.forEach { (key: String, value: Data) in
            if key == saltKeyName || key == iterationsKeyName {
                return
            }
            let encrypted = try crypto.encrypt(value)
            try memoryVault.set(encrypted, forKey: key)
        }
        
        saveToFile()
    }

    func unlock(passphrase: String) throws {
        guard let salt = try? memoryVault.string(forKey: saltKeyName), let iterations = try? memoryVault.unsignedInteger(forKey: iterationsKeyName) else {
            throw SecureStorageError.noData
        }
        
        let encryptionKey = try MXKeyBackupPassword.retrievePrivateKey(withPassword: passphrase, salt: salt, iterations: iterations)
        crypto = try CryptoAES(key: encryptionKey)
        try memoryVault.dict.forEach { (key: String, value: Any) in
            guard let data = value as? Data else {
                return
            }
            if key == saltKeyName || key == iterationsKeyName {
                dict[key] = data
                return
            }
            dict[key] = try crypto.decrypt(data)
        }
        
        locked = false
    }

}

extension SecureFileStorage: KeyValueVault {
    
    func objectExists(withKey key: String) -> Bool {
        return memoryVault.objectExists(withKey: key)
    }
    
    func removeObject(forKey key: String) throws {
        dict.removeValue(forKey: key)
        try memoryVault.removeObject(forKey: key)
        
        saveToFile()
    }
    
    func reset() throws {
        dict.removeAll()
        try memoryVault.reset()
        
        saveToFile()
    }

    // MARK: - Getter
    
    func bool(forKey key: String) throws -> Bool? {
        if let stringValue = try string(forKey: key) {
            return Bool(stringValue)
        } else {
            return nil
        }
    }
    
    func integer(forKey key: String) throws -> Int? {
        if let stringValue = try string(forKey: key) {
            return Int(stringValue)
        } else {
            return nil
        }
    }
    
    func unsignedInteger(forKey key: String) throws -> UInt? {
        if let stringValue = try string(forKey: key) {
            return UInt(stringValue)
        } else {
            return nil
        }
    }
    
    func string(forKey key: String) throws -> String? {
        if let data = try data(forKey: key)  {
            return String(data: data, encoding: .utf8)
        } else {
            return nil
        }
    }

    func data(forKey key: String) throws -> Data? {
        guard !locked else {
            throw SecureStorageError.locked
        }
        return dict[key]
    }

    // MARK: - Setter

    func set(_ value: Bool?, forKey key: String) throws {
        if let value = value {
            try set(String(value).data(using: .utf8), forKey: key)
        } else {
            try set(nil as Data?, forKey: key)
        }
    }
    
    func set(_ value: Int?, forKey key: String) throws {
        if let value = value {
            try set(String(value).data(using: .utf8), forKey: key)
        } else {
            try set(nil as Data?, forKey: key)
        }
    }
    
    func set(_ value: UInt?, forKey key: String) throws {
        if let value = value {
            try set(String(value).data(using: .utf8), forKey: key)
        } else {
            try set(nil as Data?, forKey: key)
        }
    }
    
    func set(_ value: String?, forKey key: String) throws {
        try set(value?.data(using: .utf8), forKey: key)
    }

    func set(_ value: Data?, forKey key: String) throws {
        guard !locked else {
            throw SecureStorageError.locked
        }
        if let value = value {
            dict[key] = value
            let encrypted = try crypto.encrypt(value)
            try memoryVault.set(encrypted, forKey: key)
            
            saveToFile()
        } else {
            try removeObject(forKey: key)
        }
        
    }
    
}
