#pragma once

#include <windows.h>
#include <iphlpapi.h>
#include <io.h>
#include <fcntl.h>
#include <llarp/util/thread/queue.hpp>
#include <llarp/ev/vpn.hpp>

// DDK macros
#define CTL_CODE(DeviceType, Function, Method, Access) \
  (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
#define FILE_DEVICE_UNKNOWN 0x00000022
#define FILE_ANY_ACCESS 0x00000000
#define METHOD_BUFFERED 0

/* From OpenVPN tap driver, common.h */
#define TAP_CONTROL_CODE(request, method) \
  CTL_CODE(FILE_DEVICE_UNKNOWN, request, method, FILE_ANY_ACCESS)
#define TAP_IOCTL_GET_MAC TAP_CONTROL_CODE(1, METHOD_BUFFERED)
#define TAP_IOCTL_GET_VERSION TAP_CONTROL_CODE(2, METHOD_BUFFERED)
#define TAP_IOCTL_GET_MTU TAP_CONTROL_CODE(3, METHOD_BUFFERED)
#define TAP_IOCTL_GET_INFO TAP_CONTROL_CODE(4, METHOD_BUFFERED)
#define TAP_IOCTL_CONFIG_POINT_TO_POINT TAP_CONTROL_CODE(5, METHOD_BUFFERED)
#define TAP_IOCTL_SET_MEDIA_STATUS TAP_CONTROL_CODE(6, METHOD_BUFFERED)
#define TAP_IOCTL_CONFIG_DHCP_MASQ TAP_CONTROL_CODE(7, METHOD_BUFFERED)
#define TAP_IOCTL_GET_LOG_LINE TAP_CONTROL_CODE(8, METHOD_BUFFERED)
#define TAP_IOCTL_CONFIG_DHCP_SET_OPT TAP_CONTROL_CODE(9, METHOD_BUFFERED)
#define TAP_IOCTL_CONFIG_TUN TAP_CONTROL_CODE(10, METHOD_BUFFERED)

/* Windows registry crap */
#define MAX_KEY_LENGTH 255
#define MAX_VALUE_NAME 16383
#define NETWORK_ADAPTERS                                                 \
  "SYSTEM\\CurrentControlSet\\Control\\Class\\{4D36E972-E325-11CE-BFC1-" \
  "08002BE10318}"

typedef unsigned long IPADDR;

namespace llarp::vpn
{
  static char*
  reg_query(char* key_name)
  {
    HKEY adapters, adapter;
    DWORD i, ret, len;
    char* deviceid = nullptr;
    DWORD sub_keys = 0;

    ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT(key_name), 0, KEY_READ, &adapters);
    if (ret != ERROR_SUCCESS)
    {
      return nullptr;
    }

    ret = RegQueryInfoKey(
        adapters, NULL, NULL, NULL, &sub_keys, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
    if (ret != ERROR_SUCCESS)
    {
      return nullptr;
    }

    if (sub_keys <= 0)
    {
      return nullptr;
    }

    /* Walk througt all adapters */
    for (i = 0; i < sub_keys; i++)
    {
      char new_key[MAX_KEY_LENGTH];
      char data[256];
      TCHAR key[MAX_KEY_LENGTH];
      DWORD keylen = MAX_KEY_LENGTH;

      /* Get the adapter key name */
      ret = RegEnumKeyEx(adapters, i, key, &keylen, NULL, NULL, NULL, NULL);
      if (ret != ERROR_SUCCESS)
      {
        continue;
      }

      /* Append it to NETWORK_ADAPTERS and open it */
      snprintf(new_key, sizeof new_key, "%s\\%s", key_name, key);
      ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT(new_key), 0, KEY_READ, &adapter);
      if (ret != ERROR_SUCCESS)
      {
        continue;
      }

      /* Check its values */
      len = sizeof data;
      ret = RegQueryValueEx(adapter, "ComponentId", NULL, NULL, (LPBYTE)data, &len);
      if (ret != ERROR_SUCCESS)
      {
        /* This value doesn't exist in this adaptater tree */
        goto clean;
      }
      /* If its a tap adapter, its all good */
      if (strncmp(data, "tap0901", 7) == 0)
      {
        DWORD type;

        len = sizeof data;
        ret = RegQueryValueEx(adapter, "NetCfgInstanceId", NULL, &type, (LPBYTE)data, &len);
        if (ret != ERROR_SUCCESS)
        {
          goto clean;
        }
        deviceid = strdup(data);
        break;
      }
    clean:
      RegCloseKey(adapter);
    }
    RegCloseKey(adapters);
    return deviceid;
  }

  template <typename Visit>
  void
  ForEachWIN32Interface(Visit visit)
  {
#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x))
#define FREE(x) HeapFree(GetProcessHeap(), 0, (x))
    MIB_IPFORWARDTABLE* pIpForwardTable;
    DWORD dwSize = 0;
    DWORD dwRetVal = 0;

    pIpForwardTable = (MIB_IPFORWARDTABLE*)MALLOC(sizeof(MIB_IPFORWARDTABLE));
    if (pIpForwardTable == nullptr)
      return;

    if (GetIpForwardTable(pIpForwardTable, &dwSize, 0) == ERROR_INSUFFICIENT_BUFFER)
    {
      FREE(pIpForwardTable);
      pIpForwardTable = (MIB_IPFORWARDTABLE*)MALLOC(dwSize);
      if (pIpForwardTable == nullptr)
      {
        return;
      }
    }

    if ((dwRetVal = GetIpForwardTable(pIpForwardTable, &dwSize, 0)) == NO_ERROR)
    {
      for (int i = 0; i < (int)pIpForwardTable->dwNumEntries; i++)
      {
        visit(&pIpForwardTable->table[i]);
      }
    }
    FREE(pIpForwardTable);
#undef MALLOC
#undef FREE
  }

  std::optional<int>
  GetInterfaceIndex(huint32_t ip)
  {
    std::optional<int> ret = std::nullopt;
    ForEachWIN32Interface([&ret, n = ToNet(ip)](auto* iface) {
      if (ret.has_value())
        return;
      if (iface->dwForwardNextHop == n.n)
      {
        ret = iface->dwForwardIfIndex;
      }
    });
    return ret;
  }

  class Win32Interface final : public NetworkInterface
  {
    std::atomic<bool> m_Run;
    HANDLE m_Device, m_IOCP;
    std::vector<std::thread> m_Threads;
    thread::Queue<net::IPPacket> m_ReadQueue;

    InterfaceInfo m_Info;

    static std::wstring
    get_win_sys_path()
    {
      wchar_t win_sys_path[MAX_PATH] = {0};
      const wchar_t* default_sys_path = L"C:\\Windows\\system32";

      if (!GetSystemDirectoryW(win_sys_path, _countof(win_sys_path)))
      {
        wcsncpy(win_sys_path, default_sys_path, _countof(win_sys_path));
        win_sys_path[_countof(win_sys_path) - 1] = L'\0';
      }
      return win_sys_path;
    }

    static std::string
    NetSHCommand()
    {
      std::wstring wcmd = get_win_sys_path() + L"\\netsh.exe";

      using convert_type = std::codecvt_utf8<wchar_t>;
      std::wstring_convert<convert_type, wchar_t> converter;
      return converter.to_bytes(wcmd);
    }

    static void
    NetSH(std::string commands)
    {
      commands = NetSHCommand() + " interface IPv6 " + commands;
      LogInfo("exec: ", commands);
      ::system(commands.c_str());
    }

   public:
    static std::string
    RouteCommand()
    {
      std::wstring wcmd = get_win_sys_path() + L"\\route.exe";

      using convert_type = std::codecvt_utf8<wchar_t>;
      std::wstring_convert<convert_type, wchar_t> converter;
      return converter.to_bytes(wcmd);
    }

    Win32Interface(InterfaceInfo info) : m_ReadQueue{1024}, m_Info{std::move(info)}
    {
      DWORD len;

      const auto device_id = reg_query(NETWORK_ADAPTERS);
      if (device_id == nullptr)
      {
        LogError("cannot query registry");
        throw std::invalid_argument{"cannot query registery"};
      }
      std::stringstream ss;
      ss << "\\\\.\\Global\\" << device_id << ".tap";
      const auto fname = ss.str();
      m_Device = CreateFile(
          fname.c_str(),
          GENERIC_WRITE | GENERIC_READ,
          FILE_SHARE_READ | FILE_SHARE_WRITE,
          0,
          OPEN_EXISTING,
          FILE_ATTRIBUTE_SYSTEM | FILE_FLAG_OVERLAPPED,
          0);
      if (m_Device == INVALID_HANDLE_VALUE)
      {
        LogError("failed to open device");
        throw std::invalid_argument{"cannot open " + fname};
      }

      LogInfo("putting interface up...");
      ULONG flag = 1;
      // put the interface up
      if (not DeviceIoControl(
              m_Device,
              TAP_IOCTL_SET_MEDIA_STATUS,
              &flag,
              sizeof(flag),
              &flag,
              sizeof(flag),
              &len,
              nullptr))
      {
        LogError("cannot up interface up");
        throw std::invalid_argument{"cannot put interface up"};
      }

      LogInfo("setting addresses");
      huint32_t ip{};
      // set ipv4 addresses
      for (const auto& ifaddr : m_Info.addrs)
      {
        if (ifaddr.fam == AF_INET)
        {
          IPADDR sock[3]{};
          const nuint32_t addr = xhtonl(net::TruncateV6(ifaddr.range.addr));
          ip = net::TruncateV6(ifaddr.range.addr);
          m_Info.ifname = ip.ToString();
          const nuint32_t mask = xhtonl(net::TruncateV6(ifaddr.range.netmask_bits));
          LogInfo("address ", addr, " netmask ", mask);
          sock[0] = addr.n;
          sock[1] = addr.n & mask.n;
          sock[2] = mask.n;

          if (not DeviceIoControl(
                  m_Device,
                  TAP_IOCTL_CONFIG_TUN,
                  &sock,
                  sizeof(sock),
                  &sock,
                  sizeof(sock),
                  &len,
                  nullptr))
          {
            LogError("cannot set address");
            throw std::invalid_argument{"cannot configure tun interface address"};
          }

          IPADDR ep[4]{};

          ep[0] = addr.n;
          ep[1] = mask.n;
          ep[2] = (addr.n | ~mask.n) - htonl(1);
          ep[3] = 31536000;

          if (not DeviceIoControl(
                  m_Device,
                  TAP_IOCTL_CONFIG_DHCP_MASQ,
                  ep,
                  sizeof(ep),
                  ep,
                  sizeof(ep),
                  &len,
                  nullptr))
          {
            LogError("cannot set dhcp masq");
            throw std::invalid_argument{"Cannot configure tun interface dhcp"};
          }
#pragma pack(push)
#pragma pack(1)
          struct opt
          {
            uint8_t dhcp_opt;
            uint8_t length;
            uint32_t value;
          } dns, gateway;
#pragma pack(pop)

          const nuint32_t dnsaddr{xhtonl(m_Info.dnsaddr)};
          dns.dhcp_opt = 6;
          dns.length = 4;
          dns.value = dnsaddr.n;

          gateway.dhcp_opt = 3;
          gateway.length = 4;
          gateway.value = addr.n + htonl(1);

          if (not DeviceIoControl(
                  m_Device,
                  TAP_IOCTL_CONFIG_DHCP_SET_OPT,
                  &gateway,
                  sizeof(gateway),
                  &gateway,
                  sizeof(gateway),
                  &len,
                  nullptr))
          {
            LogError("cannot set gateway");
            throw std::invalid_argument{"cannot set tun gateway"};
          }

          if (not DeviceIoControl(
                  m_Device,
                  TAP_IOCTL_CONFIG_DHCP_SET_OPT,
                  &dns,
                  sizeof(dns),
                  &dns,
                  sizeof(dns),
                  &len,
                  nullptr))
          {
            LogError("cannot set dns");
            throw std::invalid_argument{"cannot set tun dns"};
          }
        }
      }
      // set ipv6 addresses
      for (const auto& ifaddr : m_Info.addrs)
      {
        if (ifaddr.fam == AF_INET6)
        {
          const auto maybe = GetInterfaceIndex(ip);
          if (maybe.has_value())
          {
            NetSH(
                "add address interface=" + std::to_string(*maybe) + " " + ifaddr.range.ToString());
          }
        }
      }
    }

    ~Win32Interface()
    {
      ULONG flag = 0;
      DWORD len;
      // put the interface down
      DeviceIoControl(
          m_Device,
          TAP_IOCTL_SET_MEDIA_STATUS,
          &flag,
          sizeof(flag),
          &flag,
          sizeof(flag),
          &len,
          nullptr);
      m_Run = false;
      CloseHandle(m_IOCP);
      // close the handle
      CloseHandle(m_Device);
      // close the reader threads
      for (auto& thread : m_Threads)
        thread.join();
    }

    int
    PollFD() const override
    {
      return -1;
    }

    std::string
    IfName() const override
    {
      return m_Info.ifname;
    }

    void
    Start()
    {
      m_Run = true;
      const auto numThreads = std::thread::hardware_concurrency();
      // allocate packets
      for (size_t idx = 0; idx < numThreads; ++idx)
        m_Packets.emplace_back(new asio_evt_pkt{true});

      // create completion port
      m_IOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
      // attach the handle or some shit
      CreateIoCompletionPort(m_Device, m_IOCP, 0, 0);
      // spawn reader threads
      for (size_t idx = 0; idx < numThreads; ++idx)
        m_Threads.emplace_back([this, idx]() { ReadLoop(idx); });
    }

    net::IPPacket
    ReadNextPacket() override
    {
      if (m_ReadQueue.empty())
        return net::IPPacket{};

      return m_ReadQueue.popFront();
    }

    struct asio_evt_pkt
    {
      explicit asio_evt_pkt(bool _read) : read{_read}
      {}

      OVERLAPPED hdr = {0, 0, 0, 0, nullptr};  // must be first, since this is part of the IO call
      bool read;
      net::IPPacket pkt;

      void
      Read(HANDLE dev)
      {
        ReadFile(dev, pkt.buf, sizeof(pkt.buf), nullptr, &hdr);
      }
    };

    std::vector<std::unique_ptr<asio_evt_pkt>> m_Packets;

    bool
    WritePacket(net::IPPacket pkt)
    {
      LogDebug("write packet ", pkt.sz);
      asio_evt_pkt* ev = new asio_evt_pkt{false};
      std::copy_n(pkt.buf, pkt.sz, ev->pkt.buf);
      ev->pkt.sz = pkt.sz;
      WriteFile(m_Device, ev->pkt.buf, ev->pkt.sz, nullptr, &ev->hdr);
      return true;
    }

    void
    ReadLoop(size_t packetIndex)
    {
      auto& ev = m_Packets[packetIndex];
      ev->Read(m_Device);
      while (m_Run)
      {
        DWORD size;
        ULONG_PTR user;
        OVERLAPPED* ovl = nullptr;
        if (not GetQueuedCompletionStatus(m_IOCP, &size, &user, &ovl, 1000))
          continue;
        asio_evt_pkt* pkt = (asio_evt_pkt*)ovl;
        LogDebug("got iocp event size=", size, " read=", pkt->read);
        if (pkt->read)
        {
          pkt->pkt.sz = size;
          m_ReadQueue.pushBack(pkt->pkt);
          pkt->Read(m_Device);
        }
        else
          delete pkt;
      }
    }
  };

  class Win32RouteManager : public IRouteManager
  {
    void
    Execute(std::string cmd) const
    {
      llarp::LogInfo("exec: ", cmd);
      ::system(cmd.c_str());
    }

    static std::string
    RouteCommand()
    {
      return Win32Interface::RouteCommand();
    }

    void
    Route(IPVariant_t ip, IPVariant_t gateway, std::string cmd)
    {
      std::stringstream ss;
      std::string ip_str;
      std::string gateway_str;

      std::visit([&ip_str](auto&& ip) { ip_str = ip.ToString(); }, ip);
      std::visit([&gateway_str](auto&& gateway) { gateway_str = gateway.ToString(); }, gateway);

      ss << RouteCommand() << " " << cmd << " " << ip_str << " MASK 255.255.255.255 " << gateway_str
         << " METRIC 2";
      Execute(ss.str());
    }

    void
    DefaultRouteViaInterface(std::string ifname, std::string cmd)
    {
      // poke hole for loopback bacause god is dead on windows
      Execute(RouteCommand() + " " + cmd + " 127.0.0.0 MASK 255.0.0.0 0.0.0.0");

      huint32_t ip{};
      ip.FromString(ifname);
      const auto ipv6 = net::ExpandV4Lan(ip);

      Execute(RouteCommand() + " " + cmd + " ::/2 " + ipv6.ToString());
      Execute(RouteCommand() + " " + cmd + " 4000::/2 " + ipv6.ToString());
      Execute(RouteCommand() + " " + cmd + " 8000::/2 " + ipv6.ToString());
      Execute(RouteCommand() + " " + cmd + " c000::/2 " + ipv6.ToString());

      ifname.back()++;
      Execute(RouteCommand() + " " + cmd + " 0.0.0.0 MASK 128.0.0.0 " + ifname);
      Execute(RouteCommand() + " " + cmd + " 128.0.0.0 MASK 128.0.0.0 " + ifname);
    }

    void
    RouteViaInterface(std::string ifname, IPRange range, std::string cmd)
    {
      if (range.IsV4())
      {
        const huint32_t addr4 = net::TruncateV6(range.addr);
        const huint32_t mask4 = net::TruncateV6(range.netmask_bits);
        Execute(
            RouteCommand() + " " + cmd + " " + addr4.ToString() + " MASK " + mask4.ToString() + " "
            + ifname);
      }
      else
      {
        Execute(
            RouteCommand() + " " + cmd + range.addr.ToString() + " MASK "
            + range.netmask_bits.ToString() + " " + ifname);
      }
    }

   public:
    void
    AddRoute(IPVariant_t ip, IPVariant_t gateway) override
    {
      Route(ip, gateway, "ADD");
    }

    void
    DelRoute(IPVariant_t ip, IPVariant_t gateway) override
    {
      Route(ip, gateway, "DELETE");
    }

    void
    AddRouteViaInterface(NetworkInterface& vpn, IPRange range) override
    {
      RouteViaInterface(vpn.IfName(), range, "ADD");
    }

    void
    DelRouteViaInterface(NetworkInterface& vpn, IPRange range) override
    {
      RouteViaInterface(vpn.IfName(), range, "DELETE");
    }

    std::vector<IPVariant_t>
    GetGatewaysNotOnInterface(std::string ifname) override
    {
      std::vector<IPVariant_t> gateways;
      ForEachWIN32Interface([&](auto w32interface) {
        struct in_addr gateway, interface_addr;
        gateway.S_un.S_addr = (u_long)w32interface->dwForwardDest;
        interface_addr.S_un.S_addr = (u_long)w32interface->dwForwardNextHop;
        std::string interface_name{inet_ntoa(interface_addr)};
        if ((!gateway.S_un.S_addr) and interface_name != ifname)
        {
          llarp::LogTrace(
              "Win32 find gateway: Adding gateway (", interface_name, ") to list of gateways.");
          huint32_t x{};
          if (x.FromString(interface_name))
            gateways.push_back(x);
        }
      });
      return gateways;
    }

    void
    AddDefaultRouteViaInterface(std::string ifname) override
    {
      DefaultRouteViaInterface(ifname, "ADD");
    }

    void
    DelDefaultRouteViaInterface(std::string ifname) override
    {
      DefaultRouteViaInterface(ifname, "DELETE");
    }
  };

  class Win32Platform : public Platform
  {
    Win32RouteManager _routeManager{};

   public:
    std::shared_ptr<NetworkInterface>
    ObtainInterface(InterfaceInfo info) override
    {
      auto netif = std::make_shared<Win32Interface>(std::move(info));
      netif->Start();
      return netif;
    };
    IRouteManager&
    RouteManager() override
    {
      return _routeManager;
    }
  };

}  // namespace llarp::vpn
