/*
 * nghttp2 - HTTP/2 C Library
 *
 * Copyright (c) 2016 Tatsuhiro Tsujikawa
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include "shrpx_dns_tracker.h"
#include "shrpx_config.h"
#include "shrpx_log.h"
#include "util.h"

namespace shrpx {

namespace {
void gccb(struct ev_loop *loop, ev_timer *w, int revents) {
  auto dns_tracker = static_cast<DNSTracker *>(w->data);
  dns_tracker->gc();
}
} // namespace

DNSTracker::DNSTracker(struct ev_loop *loop, int family)
  : loop_(loop), family_(family) {
  ev_timer_init(&gc_timer_, gccb, 0., 12_h);
  gc_timer_.data = this;
}

DNSTracker::~DNSTracker() {
  ev_timer_stop(loop_, &gc_timer_);

  for (auto &p : ents_) {
    auto &qlist = p.second.qlist;
    while (!qlist.empty()) {
      auto head = qlist.head;
      qlist.remove(head);
      head->status = DNSResolverStatus::ERROR;
      head->in_qlist = false;
      // TODO Not sure we should call callback here, or it is even be
      // safe to do that.
    }
  }
}

ResolverEntry DNSTracker::make_entry(std::unique_ptr<DualDNSResolver> resolv,
                                     ImmutableString host,
                                     DNSResolverStatus status,
                                     const Address *result) {
  auto &dnsconf = get_config()->dns;

  auto ent = ResolverEntry{
    .host = std::move(host),
    .resolv = std::move(resolv),
    .status = status,
  };
  switch (status) {
  case DNSResolverStatus::ERROR:
  case DNSResolverStatus::OK:
    ent.expiry = std::chrono::steady_clock::now() +
                 util::duration_from(dnsconf.timeout.cache);
    break;
  default:
    break;
  }
  if (result) {
    ent.result = *result;
  }
  return ent;
}

void DNSTracker::update_entry(ResolverEntry &ent,
                              std::unique_ptr<DualDNSResolver> resolv,
                              DNSResolverStatus status, const Address *result) {
  auto &dnsconf = get_config()->dns;

  ent.resolv = std::move(resolv);
  ent.status = status;
  switch (status) {
  case DNSResolverStatus::ERROR:
  case DNSResolverStatus::OK:
    ent.expiry = std::chrono::steady_clock::now() +
                 util::duration_from(dnsconf.timeout.cache);
    break;
  default:
    break;
  }
  if (result) {
    ent.result = *result;
  }
}

DNSResolverStatus DNSTracker::resolve(Address *result, DNSQuery *dnsq) {
  int rv;

  auto it = ents_.find(dnsq->host);

  if (it == std::ranges::end(ents_)) {
    if (log_enabled(INFO)) {
      Log{INFO} << "DNS entry not found for " << dnsq->host;
    }

    auto resolv = std::make_unique<DualDNSResolver>(loop_, family_);
    auto host_copy = ImmutableString{dnsq->host};
    auto host = as_string_view(host_copy);

    rv = resolv->resolve(host);
    if (rv != 0) {
      if (log_enabled(INFO)) {
        Log{INFO} << "Name lookup failed for " << host;
      }

      ents_.emplace(host, make_entry(nullptr, std::move(host_copy),
                                     DNSResolverStatus::ERROR, nullptr));

      start_gc_timer();

      return DNSResolverStatus::ERROR;
    }

    switch (resolv->get_status(result)) {
    case DNSResolverStatus::ERROR:
      if (log_enabled(INFO)) {
        Log{INFO} << "Name lookup failed for " << host;
      }

      ents_.emplace(host, make_entry(nullptr, std::move(host_copy),
                                     DNSResolverStatus::ERROR, nullptr));

      start_gc_timer();

      return DNSResolverStatus::ERROR;
    case DNSResolverStatus::OK:
      if (log_enabled(INFO)) {
        Log{INFO} << "Name lookup succeeded: " << host << " -> "
                  << util::numeric_name(result->as_sockaddr(), result->size());
      }

      ents_.emplace(host, make_entry(nullptr, std::move(host_copy),
                                     DNSResolverStatus::OK, result));

      start_gc_timer();

      return DNSResolverStatus::OK;
    case DNSResolverStatus::RUNNING: {
      auto p =
        ents_.emplace(host, make_entry(std::move(resolv), std::move(host_copy),
                                       DNSResolverStatus::RUNNING, nullptr));

      start_gc_timer();

      auto &ent = (*p.first).second;

      add_to_qlist(ent, dnsq);

      return DNSResolverStatus::RUNNING;
    }
    default:
      assert(0);
    }
  }

  auto &ent = (*it).second;

  if (ent.status != DNSResolverStatus::RUNNING &&
      ent.expiry < std::chrono::steady_clock::now()) {
    if (log_enabled(INFO)) {
      Log{INFO} << "DNS entry found for " << dnsq->host
                << ", but it has been expired";
    }

    auto resolv = std::make_unique<DualDNSResolver>(loop_, family_);
    auto host = as_string_view(ent.host);

    rv = resolv->resolve(host);
    if (rv != 0) {
      if (log_enabled(INFO)) {
        Log{INFO} << "Name lookup failed for " << host;
      }

      update_entry(ent, nullptr, DNSResolverStatus::ERROR, nullptr);

      return DNSResolverStatus::ERROR;
    }

    switch (resolv->get_status(result)) {
    case DNSResolverStatus::ERROR:
      if (log_enabled(INFO)) {
        Log{INFO} << "Name lookup failed for " << host;
      }

      update_entry(ent, nullptr, DNSResolverStatus::ERROR, nullptr);

      return DNSResolverStatus::ERROR;
    case DNSResolverStatus::OK:
      if (log_enabled(INFO)) {
        Log{INFO} << "Name lookup succeeded: " << host << " -> "
                  << util::numeric_name(result->as_sockaddr(), result->size());
      }

      update_entry(ent, nullptr, DNSResolverStatus::OK, result);

      return DNSResolverStatus::OK;
    case DNSResolverStatus::RUNNING:
      update_entry(ent, std::move(resolv), DNSResolverStatus::RUNNING, nullptr);
      add_to_qlist(ent, dnsq);

      return DNSResolverStatus::RUNNING;
    default:
      assert(0);
    }
  }

  switch (ent.status) {
  case DNSResolverStatus::RUNNING:
    if (log_enabled(INFO)) {
      Log{INFO} << "Waiting for name lookup complete for " << dnsq->host;
    }
    ent.qlist.append(dnsq);
    dnsq->in_qlist = true;
    return DNSResolverStatus::RUNNING;
  case DNSResolverStatus::ERROR:
    if (log_enabled(INFO)) {
      Log{INFO} << "Name lookup failed for " << dnsq->host << " (cached)";
    }
    return DNSResolverStatus::ERROR;
  case DNSResolverStatus::OK:
    if (log_enabled(INFO)) {
      Log{INFO} << "Name lookup succeeded (cached): " << dnsq->host << " -> "
                << util::numeric_name(ent.result.as_sockaddr(),
                                      ent.result.size());
    }
    if (result) {
      *result = ent.result;
    }
    return DNSResolverStatus::OK;
  default:
    assert(0);
    abort();
  }
}

void DNSTracker::add_to_qlist(ResolverEntry &ent, DNSQuery *dnsq) {
  ent.resolv->set_complete_cb(
    [&ent](DNSResolverStatus status, const Address *result) {
      auto &qlist = ent.qlist;
      while (!qlist.empty()) {
        auto head = qlist.head;
        qlist.remove(head);
        head->status = status;
        head->in_qlist = false;
        auto cb = head->cb;
        cb(status, result);
      }

      auto &dnsconf = get_config()->dns;

      ent.resolv.reset();
      ent.status = status;
      ent.expiry = std::chrono::steady_clock::now() +
                   util::duration_from(dnsconf.timeout.cache);
      if (ent.status == DNSResolverStatus::OK) {
        ent.result = *result;
      }
    });
  ent.qlist.append(dnsq);
  dnsq->in_qlist = true;
}

void DNSTracker::cancel(DNSQuery *dnsq) {
  if (!dnsq->in_qlist) {
    return;
  }

  auto it = ents_.find(dnsq->host);
  if (it == std::ranges::end(ents_)) {
    return;
  }

  auto &ent = (*it).second;
  ent.qlist.remove(dnsq);
  dnsq->in_qlist = false;
}

void DNSTracker::start_gc_timer() {
  if (ev_is_active(&gc_timer_)) {
    return;
  }

  ev_timer_again(loop_, &gc_timer_);
}

void DNSTracker::gc() {
  if (log_enabled(INFO)) {
    Log{INFO} << "Starting removing expired DNS cache entries";
  }

  auto now = std::chrono::steady_clock::now();
  for (auto it = std::ranges::begin(ents_); it != std::ranges::end(ents_);) {
    auto &ent = (*it).second;
    if (ent.expiry >= now) {
      ++it;
      continue;
    }

    it = ents_.erase(it);
  }

  if (ents_.empty()) {
    ev_timer_stop(loop_, &gc_timer_);
  }
}

} // namespace shrpx
