/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "GetAddrInfo.h"
#include "mozilla/glean/NetwerkMetrics.h"
#include "mozilla/net/DNSPacket.h"
#include "nsIDNSService.h"
#include "mozilla/Maybe.h"
#include "mozilla/Atomics.h"
#include "mozilla/StaticPrefs_network.h"

#include <netinet/in.h>
#include <resolv.h>
#include <poll.h>
#include <android/multinetwork.h>

namespace mozilla::net {

// The first call to ResolveHTTPSRecordImpl will load the library
// and function pointers.
static Atomic<bool> sLibLoading{false};

// https://developer.android.com/ndk/reference/group/networking#android_res_nquery
// The function android_res_nquery is defined in <android/multinetwork.h>
typedef int (*android_res_nquery_ptr)(net_handle_t network, const char* dname,
                                      int ns_class, int ns_type,
                                      uint32_t flags);
static Atomic<android_res_nquery_ptr> sAndroidResNQuery;

#define LOG(msg, ...) \
  MOZ_LOG(gGetAddrInfoLog, LogLevel::Debug, ("[DNS]: " msg, ##__VA_ARGS__))

nsresult ResolveHTTPSRecordImpl(const nsACString& aHost,
                                nsIDNSService::DNSFlags aFlags,
                                TypeRecordResultType& aResult, uint32_t& aTTL) {
  DNSPacket packet;
  nsAutoCString host(aHost);
  nsAutoCString cname;
  nsresult rv;

  if (xpc::IsInAutomation() &&
      !StaticPrefs::network_dns_native_https_query_in_automation()) {
    return NS_ERROR_UNKNOWN_HOST;
  }

  if (!sLibLoading.exchange(true)) {
    // We're the first call here, load the library and symbols.
    if (__builtin_available(android 29, *)) {
      sAndroidResNQuery = android_res_nquery;  // API 29
    } else {
      LOG("No android_res_nquery symbol");
    }
  }

  if (!sAndroidResNQuery) {
    LOG("nquery not loaded");
    // The library hasn't been loaded yet.
    return NS_ERROR_UNKNOWN_HOST;
  }

  LOG("resolving %s\n", host.get());
  TimeStamp startTime = TimeStamp::Now();
  // Perform the query
  rv = packet.FillBuffer(
      [&](unsigned char response[DNSPacket::MAX_SIZE]) -> int {
        int fd = 0;
        auto closeSocket = MakeScopeExit([&] {
          if (fd > 0) {
            close(fd);
          }
        });
        uint32_t flags = 0;
        if (aFlags & nsIDNSService::RESOLVE_BYPASS_CACHE) {
          flags = ANDROID_RESOLV_NO_CACHE_LOOKUP;
        }
        fd = sAndroidResNQuery(0, host.get(), ns_c_in,
                               nsIDNSService::RESOLVE_TYPE_HTTPSSVC, flags);

        if (fd < 0) {
          LOG("DNS query failed");
          return fd;
        }

        struct pollfd fds;
        fds.fd = fd;
        fds.events = POLLIN;  // Wait for read events

        // Wait for an event on the file descriptor
        int ret = poll(&fds, 1,
                       StaticPrefs::network_dns_native_https_timeout_android());
        if (ret <= 0) {
          LOG("poll failed %d", ret);
          return -1;
        }

        ssize_t len = recv(fd, response, DNSPacket::MAX_SIZE - 1, 0);
        if (len <= 8) {
          LOG("size too small %zd", len);
          return len < 0 ? len : -1;
        }

        // The first 8 bytes are UDP header.
        // XXX: we should consider avoiding this move somehow.
        for (int i = 0; i < len - 8; i++) {
          response[i] = response[i + 8];
        }

        return len - 8;
      });
  mozilla::glean::networking::dns_native_https_call_time.AccumulateRawDuration(
      TimeStamp::Now() - startTime);
  if (NS_FAILED(rv)) {
    LOG("failed rv");
    return rv;
  }
  packet.SetNativePacket(true);

  int32_t loopCount = 64;
  while (loopCount > 0 && aResult.is<Nothing>()) {
    loopCount--;
    DOHresp resp;
    nsClassHashtable<nsCStringHashKey, DOHresp> additionalRecords;
    rv = packet.Decode(host, TRRTYPE_HTTPSSVC, cname, true, resp, aResult,
                       additionalRecords, aTTL);
    if (NS_FAILED(rv)) {
      LOG("Decode failed %x", static_cast<uint32_t>(rv));
      return rv;
    }
    if (!cname.IsEmpty() && aResult.is<Nothing>()) {
      host = cname;
      cname.Truncate();
      continue;
    }
  }

  if (aResult.is<Nothing>()) {
    LOG("Result is nothing");
    // The call succeeded, but no HTTPS records were found.
    return NS_ERROR_UNKNOWN_HOST;
  }

  return NS_OK;
}

void DNSThreadShutdown() {}

}  // namespace mozilla::net
