import Foundation
import NetworkExtension
import OpenVPNAdapter

struct OpenVPNConfig: Decodable {
    let config: String
    let splitTunnelType: Int
    let splitTunnelSites: [String]

    var str: String {
        "splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
    }
}

extension PacketTunnelProvider {
    func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
        guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
              let providerConfiguration = protocolConfiguration.providerConfiguration,
              let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
            ovpnLog(.error, message: "Can't start")
            return
        }

        do {
            let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
            ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
            let ovpnConfiguration = Data(openVPNConfig.config.utf8)
            setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
        } catch {
            ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")

            if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
                ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
            }

            return
        }
    }

    private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
                                       withShadowSocks viaSS: Bool = false,
                                       completionHandler: @escaping (Error?) -> Void) {
        ovpnLog(.info, message: "Setup and launch")

        let str = String(decoding: ovpnConfiguration, as: UTF8.self)

        let configuration = OpenVPNConfiguration()
        configuration.fileContent = ovpnConfiguration
        if str.contains("cloak") {
            configuration.setPTCloak()
        }

        let evaluation: OpenVPNConfigurationEvaluation?
        do {
            ovpnAdapter = OpenVPNAdapter()
            ovpnAdapter?.delegate = self
            evaluation = try ovpnAdapter?.apply(configuration: configuration)

        } catch {
            completionHandler(error)
            return
        }

        if evaluation?.autologin == false {
            ovpnLog(.info, message: "Implement login with user credentials")
        }

        vpnReachability.startTracking { [weak self] status in
            guard status == .reachableViaWiFi else { return }
            self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
        }

        startHandler = completionHandler
        ovpnAdapter?.connect(using: packetFlow)
    }
    
    func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
        guard let completionHandler = completionHandler else { return }
        let bytesin = ovpnAdapter?.transportStatistics.bytesIn
        let bytesout = ovpnAdapter?.transportStatistics.bytesOut

        guard let bytesin, let bytesout else {
            completionHandler(nil)
            return
        }

        let response: [String: Any] = [
            "rx_bytes": bytesin,
            "tx_bytes": bytesout
        ]

        completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
    }

    func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
        ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)")

        stopHandler = completionHandler
        if vpnReachability.isTracking {
            vpnReachability.stopTracking()
        }
        ovpnAdapter?.disconnect()
    }
}

extension PacketTunnelProvider: OpenVPNAdapterDelegate {
    // OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
    // `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
    // protocol if the tunnel is configured without errors. Otherwise send nil.
    // `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
    // you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
    // send `self.packetFlow` to `completionHandler` callback.
    func openVPNAdapter(
        _ openVPNAdapter: OpenVPNAdapter,
        configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
        completionHandler: @escaping (Error?) -> Void
    ) {
        // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
        // send empty string to NEDNSSettings.matchDomains
        networkSettings?.dnsSettings?.matchDomains = [""]

        if splitTunnelType == 1 {
            var ipv4IncludedRoutes = [NEIPv4Route]()

            guard let splitTunnelSites else {
                completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0))
                return
            }

            for allowedIPString in splitTunnelSites {
                if let allowedIP = IPAddressRange(from: allowedIPString) {
                    ipv4IncludedRoutes.append(NEIPv4Route(
                        destinationAddress: "\(allowedIP.address)",
                        subnetMask: "\(allowedIP.subnetMask())"))
                }
            }

            networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
        } else {
            if splitTunnelType == 2 {
                var ipv4ExcludedRoutes = [NEIPv4Route]()
                var ipv4IncludedRoutes = [NEIPv4Route]()
                var ipv6IncludedRoutes = [NEIPv6Route]()

                guard let splitTunnelSites else {
                    completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0))
                    return
                }

                for excludeIPString in splitTunnelSites {
                    if let excludeIP = IPAddressRange(from: excludeIPString) {
                        ipv4ExcludedRoutes.append(NEIPv4Route(
                            destinationAddress: "\(excludeIP.address)",
                            subnetMask: "\(excludeIP.subnetMask())"))
                    }
                }

                if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
                    ipv4IncludedRoutes.append(NEIPv4Route(
                        destinationAddress: "\(allIPv4.address)",
                        subnetMask: "\(allIPv4.subnetMask())"))
                }
                if let allIPv6 = IPAddressRange(from: "::/0") {
                    ipv6IncludedRoutes.append(NEIPv6Route(
                        destinationAddress: "\(allIPv6.address)",
                        networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
                }
                networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
                networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
                networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
            }
        }

        // Set the network settings for the current tunneling session.
        setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
    }

    // Process events returned by the OpenVPN library
    func openVPNAdapter(
        _ openVPNAdapter: OpenVPNAdapter,
        handleEvent event: OpenVPNAdapterEvent,
        message: String?) {
            switch event {
            case .connected:
                if reasserting {
                    reasserting = false
                }

                guard let startHandler = startHandler else { return }

                startHandler(nil)
                self.startHandler = nil
            case .disconnected:
                guard let stopHandler = stopHandler else { return }

                if vpnReachability.isTracking {
                    vpnReachability.stopTracking()
                }

                stopHandler()
                self.stopHandler = nil
            case .reconnecting:
                reasserting = true
            default:
                break
            }
        }

    // Handle errors thrown by the OpenVPN library
    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
        // Handle only fatal errors
        guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
              fatal == true else { return }

        if vpnReachability.isTracking {
            vpnReachability.stopTracking()
        }

        if let startHandler {
            startHandler(error)
            self.startHandler = nil
        } else {
            cancelTunnelWithError(error)
        }
    }

    // Use this method to process any log message returned by OpenVPN library.
    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
        // Handle log messages
        ovpnLog(.info, message: logMessage)
    }
}

extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
