import Foundation
import NetworkExtension
import os
import Darwin
import OpenVPNAdapter

enum TunnelProtoType: String {
  case wireguard, openvpn, shadowsocks, none
}

struct Constants {
  static let kDefaultPathKey = "defaultPath"
  static let processQueueName = "org.amnezia.process-packets"
  static let kActivationAttemptId = "activationAttemptId"
  static let ovpnConfigKey = "ovpn"
  static let wireGuardConfigKey = "wireguard"
  static let loggerTag = "NET"

  static let kActionStart = "start"
  static let kActionRestart = "restart"
  static let kActionStop = "stop"
  static let kActionGetTunnelId = "getTunnelId"
  static let kActionStatus = "status"
  static let kActionIsServerReachable = "isServerReachable"
  static let kMessageKeyAction = "action"
  static let kMessageKeyTunnelId = "tunnelId"
  static let kMessageKeyConfig = "config"
  static let kMessageKeyErrorCode = "errorCode"
  static let kMessageKeyHost = "host"
  static let kMessageKeyPort = "port"
  static let kMessageKeyOnDemand = "is-on-demand"
  static let kMessageKeySplitTunnelType = "SplitTunnelType"
  static let kMessageKeySplitTunnelSites = "SplitTunnelSites"
}

class PacketTunnelProvider: NEPacketTunnelProvider {
  private lazy var wgAdapter = {
    WireGuardAdapter(with: self) { logLevel, message in
      wg_log(logLevel.osLogLevel, message: message)
    }
  }()

  private lazy var ovpnAdapter: OpenVPNAdapter = {
    let adapter = OpenVPNAdapter()
    adapter.delegate = self
    return adapter
  }()

  /// Internal queue.
  private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)

  private var openVPNConfig: Data?
  var splitTunnelType: String?
  var splitTunnelSites: String?

  let vpnReachability = OpenVPNReachability()

  var startHandler: ((Error?) -> Void)?
  var stopHandler: (() -> Void)?
  var protoType: TunnelProtoType = .none

  override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
    let tmpStr = String(data: messageData, encoding: .utf8)!
    wg_log(.error, message: tmpStr)
    guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
      log(.error, message: "Failed to serialize message from app")
      return
    }

    guard let completionHandler else {
      log(.error, message: "Missing message completion handler")
      return
    }

    guard let action = message[Constants.kMessageKeyAction] as? String else {
      log(.error, message: "Missing action key in app message")
      completionHandler(nil)
      return
    }

    if action == Constants.kActionStatus {
      handleStatusAppMessage(messageData, completionHandler: completionHandler)
    }

    if action == Constants.kActionStart {
      splitTunnelType = message[Constants.kMessageKeySplitTunnelType] as? String
      splitTunnelSites = message[Constants.kMessageKeySplitTunnelSites] as? String
    }

    let callbackWrapper: (NSNumber?) -> Void = { errorCode in
      // let tunnelId = self.tunnelConfig?.id ?? ""
      let response: [String: Any] = [
        Constants.kMessageKeyAction: action,
        Constants.kMessageKeyErrorCode: errorCode ?? NSNull(),
        Constants.kMessageKeyTunnelId: 0
      ]

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

  override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
    dispatchQueue.async {
      let activationAttemptId = options?[Constants.kActivationAttemptId] as? String
      let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)

      log(.info, message: "PacketTunnelProvider startTunnel")

      if let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol {
        let providerConfiguration = protocolConfiguration.providerConfiguration
        if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
          self.protoType = .openvpn
        } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
          self.protoType = .wireguard
        }
      } else {
        self.protoType = .none
      }

      switch self.protoType {
      case .wireguard:
        self.startWireguard(activationAttemptId: activationAttemptId,
                            errorNotifier: errorNotifier,
                            completionHandler: completionHandler)
      case .openvpn:
        self.startOpenVPN(completionHandler: completionHandler)
      case .shadowsocks:
        break
        //  startShadowSocks(completionHandler: completionHandler)
      case .none:
        break
      }
    }
  }

  override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
    dispatchQueue.async {
      switch self.protoType {
      case .wireguard:
        self.stopWireguard(with: reason, completionHandler: completionHandler)
      case .openvpn:
        self.stopOpenVPN(with: reason, completionHandler: completionHandler)
      case .shadowsocks:
        break
        //  stopShadowSocks(with: reason, completionHandler: completionHandler)
      case .none:
        break
      }
    }
  }

  func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
    switch protoType {
    case .wireguard:
      handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
    case .openvpn:
      handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
    case .shadowsocks:
      break
      //            handleShadowSocksAppMessage(messageData, completionHandler: completionHandler)
    case .none:
      break
    }
  }

  // MARK: Private methods
  private func startWireguard(activationAttemptId: String?,
                              errorNotifier: ErrorNotifier,
                              completionHandler: @escaping (Error?) -> Void) {
    guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
          let providerConfiguration = protocolConfiguration.providerConfiguration,
          let wgConfig: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
      wg_log(.error, message: "Can't start WireGuard config missing")
      completionHandler(nil)
      return
    }

    guard let wgConfigStr = try? JSONDecoder().decode(WGConfig.self, from: wgConfig).wg,
          let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
    else {
      wg_log(.error, message: "Can't parse WireGuard config")
      completionHandler(nil)
      return
    }

    log(.info, message: "wgConfig: \(wgConfigStr.replacingOccurrences(of: "\n", with: " "))")

    if tunnelConfiguration.peers.first!.allowedIPs
      .map({ $0.stringRepresentation })
      .joined(separator: ", ") == "0.0.0.0/0, ::/0" {
      if splitTunnelType == "1" {
        for index in tunnelConfiguration.peers.indices {
          tunnelConfiguration.peers[index].allowedIPs.removeAll()
          var allowedIPs = [IPAddressRange]()
          let STSdata = Data(splitTunnelSites!.utf8)
          do {
            guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
            for allowedIPString in STSArray {
              if let allowedIP = IPAddressRange(from: allowedIPString) {
                allowedIPs.append(allowedIP)
              }
            }
          } catch {
            wg_log(.error, message: "Parse JSONSerialization Error")
          }
          tunnelConfiguration.peers[index].allowedIPs = allowedIPs
        }
      } else if splitTunnelType == "2" {
        for index in tunnelConfiguration.peers.indices {
          var excludeIPs = [IPAddressRange]()
          let STSdata = Data(splitTunnelSites!.utf8)
          do {
            guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
            for excludeIPString in STSArray {
              if let excludeIP = IPAddressRange(from: excludeIPString) {
                excludeIPs.append(excludeIP)
              }
            }
          } catch {
            wg_log(.error, message: "Parse JSONSerialization Error")
          }
          tunnelConfiguration.peers[index].excludeIPs = excludeIPs
        }
      }
    }

    wg_log(.info, message: "Starting wireguard tunnel from the " +
           (activationAttemptId == nil ? "OS directly, rather than the app" : "app"))

    // Start the tunnel
    wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
      guard let adapterError else {
        let interfaceName = self.wgAdapter.interfaceName ?? "unknown"
        wg_log(.info, message: "Tunnel interface is \(interfaceName)")
        completionHandler(nil)
        return
      }

      switch adapterError {
      case .cannotLocateTunnelFileDescriptor:
        wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
        errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
        completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
      case .dnsResolution(let dnsErrors):
        let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
          .joined(separator: ", ")
        wg_log(.error, message:
                "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
        errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
        completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
      case .setNetworkSettings(let error):
        wg_log(.error, message:
                "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
        errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
        completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
      case .startWireGuardBackend(let errorCode):
        wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
        errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
        completionHandler(PacketTunnelProviderError.couldNotStartBackend)
      case .invalidState:
        fatalError()
      }
    }
  }

  private func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
    guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
          let providerConfiguration = protocolConfiguration.providerConfiguration,
          let ovpnConfiguration: Data = providerConfiguration[Constants.ovpnConfigKey] as? Data else {

      wg_log(.error, message: "Can't start startOpenVPN()")
      return
    }

    setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
  }

  private func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
    wg_log(.info, staticMessage: "Stopping tunnel")

    wgAdapter.stop { error in
      ErrorNotifier.removeLastErrorFile()

      if let error {
        wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)")
      }
      completionHandler()

#if os(macOS)
      // HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
      // Remove it when they finally fix this upstream and the fix has been rolled out to
      // sufficient quantities of users.
      exit(0)
#endif
    }
  }

  private func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
    stopHandler = completionHandler
    if vpnReachability.isTracking {
      vpnReachability.stopTracking()
    }
    ovpnAdapter.disconnect()
  }

  func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
    guard let completionHandler = completionHandler else { return }
    wgAdapter.getRuntimeConfiguration { settings in
      var data: Data?
      if let settings {
        data = settings.data(using: .utf8)!
      }

      let components = settings!.components(separatedBy: "\n")

      var settingsDictionary: [String: String] = [:]
      for component in components {
        let pair = component.components(separatedBy: "=")
        if pair.count == 2 {
          settingsDictionary[pair[0]] = pair[1]
        }
      }

      let response: [String: Any] = [
        "rx_bytes": settingsDictionary["rx_bytes"] ?? "0",
        "tx_bytes": settingsDictionary["tx_bytes"] ?? "0"
      ]

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

  private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
    guard let completionHandler = completionHandler else { return }
    if messageData.count == 1 && messageData[0] == 0 {
      wgAdapter.getRuntimeConfiguration { settings in
        var data: Data?
        if let settings {
          data = settings.data(using: .utf8)!
        }
        completionHandler(data)
      }
    } else if messageData.count >= 1 {
      // Updates the tunnel configuration and responds with the active configuration
      wg_log(.info, message: "Switching tunnel configuration")
      guard let configString = String(data: messageData, encoding: .utf8)
      else {
        completionHandler(nil)
        return
      }

      do {
        let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
        wgAdapter.update(tunnelConfiguration: tunnelConfiguration) { error in
          if let error {
            wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
            completionHandler(nil)
            return
          }

          self.wgAdapter.getRuntimeConfiguration { settings in
            var data: Data?
            if let settings {
              data = settings.data(using: .utf8)!
            }
            completionHandler(data)
          }
        }
      } catch {
        completionHandler(nil)
      }
    } else {
      completionHandler(nil)
    }
  }

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

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

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

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

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

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

    let evaluation: OpenVPNConfigurationEvaluation
    do {
      evaluation = try ovpnAdapter.apply(configuration: configuration)

    } catch {
      completionHandler(error)
      return
    }

    if !evaluation.autologin {
      wg_log(.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)

    //        let ifaces = Interface.allInterfaces()
    //            .filter { $0.family == .ipv4 }
    //            .map { iface in iface.name }

    //        wg_log(.error, message: "Available TUN Interfaces: \(ifaces)")
  }

  // MARK: Network observing methods

  private func startListeningForNetworkChanges() {
    stopListeningForNetworkChanges()
    addObserver(self, forKeyPath: Constants.kDefaultPathKey, options: .old, context: nil)
  }

  private func stopListeningForNetworkChanges() {
    removeObserver(self, forKeyPath: Constants.kDefaultPathKey)
  }

  override func observeValue(forKeyPath keyPath: String?,
                             of object: Any?,
                             change: [NSKeyValueChangeKey: Any]?,
                             context: UnsafeMutableRawPointer?) {
    guard Constants.kDefaultPathKey != keyPath else { return }
    // Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi,
    // even though the underlying network has not changed (i.e. `isEqualToPath` returns false),
    // leading to "wakeup crashes" due to excessive network activity. Guard against false positives by
    // comparing the paths' string description, which includes properties not exposed by the class
    guard let lastPath: NWPath = change?[.oldKey] as? NWPath,
          let defPath = defaultPath,
          lastPath != defPath || lastPath.description != defPath.description else {
      return
    }
    DispatchQueue.main.async { [weak self] in
      guard let self, self.defaultPath != nil else { return }
      self.handle(networkChange: self.defaultPath!) { _ in }
    }
  }

  private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) {
    wg_log(.info, message: "Tunnel restarted.")
    startTunnel(options: nil, completionHandler: completion)
  }

  private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) {
    dispatchPrecondition(condition: .onQueue(dispatchQueue))

    let emptyTunnelConfiguration = TunnelConfiguration(
      name: nil,
      interface: InterfaceConfiguration(privateKey: PrivateKey()),
      peers: []
    )

    wgAdapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in
      self.dispatchQueue.async {
        if let error {
          log(.error, message: "Failed to start an empty tunnel")
          completionHandler(error)
        } else {
          log(.info, message: "Started an empty tunnel")
          self.tunnelAdapterDidStart()
        }
      }
    }

    let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")

    self.setTunnelNetworkSettings(settings) { error in
      completionHandler(error)
    }
  }

  private func tunnelAdapterDidStart() {
    dispatchPrecondition(condition: .onQueue(dispatchQueue))
    // ...
  }
}

extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}

extension WireGuardLogLevel {
  var osLogLevel: OSLogType {
    switch self {
    case .verbose:
      return .debug
    case .error:
      return .error
    }
  }
}
