//
//  VPNController.swift
//  Lockdown
//
//  Copyright © 2018 Confirmed, Inc. All rights reserved.
//

import UIKit
import NetworkExtension
import CocoaLumberjackSwift
import WidgetKit

let kVPNLocalizedDescription = "Lockdown VPN"

class VPNController: NSObject {
    
    static let shared = VPNController()
    
    let manager = NEVPNManager.shared()
    
    private override init() {
        super.init()
        manager.loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in })
    }
    
    func status() -> NEVPNStatus {
        return manager.connection.status
    }
    
    func restart() {
        // Don't let this affect userWantsVPNOn/Off config
        VPNController.shared.setEnabled(false, completion: {
            error in
            // TODO: Handle the error
            VPNController.shared.setEnabled(true)
        })
    }
    
    func isConfigurationExisting(_ completion: @escaping (Bool) -> ()) {
        manager.loadFromPreferences { (error) in
            completion(self.manager.protocolConfiguration != nil)
        }
    }
 
    func setEnabled(_ enabled: Bool, completion: @escaping (_ error: Error?) -> Void = {_ in }) {
        DDLogInfo("VPNController set enabled: \(enabled)")
        setUserWantsVPNEnabled(enabled)
        if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
            WidgetCenter.shared.reloadAllTimelines()
        }
        if (enabled) {
            setUpAndEnableVPN { error in
                completion(error)
            }
        }
        else {
            manager.loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in
                self.manager.isEnabled = false
                self.manager.isOnDemandEnabled = false
                self.manager.saveToPreferences(completionHandler: {(_ error: Error?) -> Void in
                    // TODO: will this ever error?
                    completion(error)
                })
            })
        }
    }
    
    private func setUpAndEnableVPN(completion: @escaping (_ error: Error?) -> Void) {
        guard let vpnCredentials = getVPNCredentials() else {
            // TODO: handle error
            return completion("No VPN credentials found while enabling VPN")
        }
        
        let serverAddress = getSavedVPNRegion().serverPrefix + vpnSourceID + "." + vpnDomain
        let localIdentifier = vpnCredentials.id
        let identityData = Data(base64Encoded: vpnCredentials.keyBase64)
        
        manager.loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in
            let p = NEVPNProtocolIKEv2()
            
            p.serverAddress = serverAddress
            p.serverCertificateIssuerCommonName = vpnRemoteIdentifier
            p.remoteIdentifier = vpnRemoteIdentifier
            
            p.certificateType = NEVPNIKEv2CertificateType.ECDSA256
            p.authenticationMethod = NEVPNIKEAuthenticationMethod.certificate
            p.localIdentifier = localIdentifier
            p.useExtendedAuthentication = false
            p.disconnectOnSleep = false
            p.enablePFS = true
            
            p.childSecurityAssociationParameters.diffieHellmanGroup = NEVPNIKEv2DiffieHellmanGroup.group19
            p.childSecurityAssociationParameters.encryptionAlgorithm = NEVPNIKEv2EncryptionAlgorithm.algorithmAES128GCM
            p.childSecurityAssociationParameters.integrityAlgorithm = NEVPNIKEv2IntegrityAlgorithm.SHA512
            p.childSecurityAssociationParameters.lifetimeMinutes = 1440
            
            p.ikeSecurityAssociationParameters.diffieHellmanGroup = NEVPNIKEv2DiffieHellmanGroup.group19
            p.ikeSecurityAssociationParameters.encryptionAlgorithm = NEVPNIKEv2EncryptionAlgorithm.algorithmAES128GCM
            p.ikeSecurityAssociationParameters.integrityAlgorithm = NEVPNIKEv2IntegrityAlgorithm.SHA512
            p.ikeSecurityAssociationParameters.lifetimeMinutes = 1440
            
            p.deadPeerDetectionRate = NEVPNIKEv2DeadPeerDetectionRate.medium
            p.identityData = identityData
            p.identityDataPassword = ""
            
            if #available(iOS 13.0, *) {
                p.enableFallback = false
            }
            
            self.manager.protocolConfiguration = p
            self.manager.isEnabled = true
            self.manager.isOnDemandEnabled = true
            var onDemandRules:[NEOnDemandRule] = [];
            let connectRule = NEOnDemandRuleConnect()
            connectRule.interfaceTypeMatch = .any
            onDemandRules.append(connectRule)
            self.manager.onDemandRules = onDemandRules
            
            DDLogInfo("VPN status before loading: \(self.manager.connection.status)")
            self.manager.localizedDescription! = kVPNLocalizedDescription
            self.manager.saveToPreferences(completionHandler: {(_ error: Error?) -> Void in
                if let e = error {
                    DDLogError("Saving VPN Error \(e)")
                    if ((e as NSError).code == 4) { // if config is stale, probably multithreading bug
                        DDLogInfo("Stale config, trying again")
                        self.setUpAndEnableVPN(completion: { error in
                            completion(error)
                        })
                    }
                    else {
                        completion(e)
                    }
                }
                else {
                    self.loadFromPrefenrenceAndStartVPN(completion: completion)
                }
            })
        })
    }
    
    private func loadFromPrefenrenceAndStartVPN(completion: @escaping (_ error: Error?) -> Void) {
        manager.loadFromPreferences { [weak self] error in
            if let error {
                DDLogError("Read preference error before start vpn: " + error.localizedDescription)
            }
            do {
                // manually activate the starting of the tunnel, and also do a dummy connect to a nonexistant, invalid URL to force enabling
                try self?.manager.connection.startVPNTunnel()
                let config = URLSessionConfiguration.default
                config.requestCachePolicy = .reloadIgnoringLocalCacheData
                config.urlCache = nil
                let session = URLSession.init(configuration: config)
                let url = URL(string: "https://nonexistant_invalid_url")
                let task = session.dataTask(with: url!) { (data, response, error) in
                    return
                }
                task.resume()
            }
            catch {
                DDLogError("Unable to start the tunnel after saving: " + error.localizedDescription)
            }
            completion(nil)
        }
        
    }
}
