#include <algorithm>
#include <condition_variable>
#include <future>
#include <random>
#include <stdexcept>
#include <thread>
#include <tuple>
#include <utility>
#include <variant>
#include <boost/format.hpp>
#include <boost/format/format_fwd.hpp>
#include <boost/uuid/string_generator.hpp>
#include <boost/algorithm/string/erase.hpp>

#include "misc.hh"
#include "qtype.hh"
#include "version.hh"
#include "ext/luawrapper/include/LuaContext.hpp"
#include "lock.hh"
#include "lua-auth4.hh"
#include "sstuff.hh"
#include "minicurl.hh"
#include "ueberbackend.hh"
#include "dns_random.hh"
#include "auth-main.hh"
#include "../modules/geoipbackend/geoipinterface.hh" // only for the enum

/* to do:
   block AXFR unless TSIG, or override

   investigate IPv6

   check the wildcard 'no cache' stuff, we may get it wrong

   ponder ECS scopemask setting

   ponder netmask tree from file for huge number of netmasks

   add attribute for certificate check in genericIfUp

   add list of current monitors
      expire them too?

   pool of UeberBackends?

   Pool checks ?
 */

extern int  g_luaRecordExecLimit;

// Slightly different from dnsdist-lua.hh, as we need ordering
template <class T>
using LuaAssociativeTable = std::map<std::string, T>;

using iplist_t = vector<pair<int, string> >;
using wiplist_t = std::unordered_map<int, string>;
using ipunitlist_t = vector<pair<int, iplist_t> >;
using optvalue_t = std::variant<LuaAssociativeTable<string>, string>;
using opts_t = LuaAssociativeTable<optvalue_t>;

class IsUpOracle
{
private:
  struct CheckDesc
  {
    ComboAddress rem;
    string url;
    opts_t opts;
    std::shared_ptr<Logr::Logger> slog;

    template <class T>
    T getOption(const std::string &optName) const
    {
      if (!opts.count(optName)) {
        throw std::runtime_error(boost::str(boost::format("%s does not exist") % optName));
      }
      if (!std::holds_alternative<T>(opts.at(optName))) {
        throw std::runtime_error(boost::str(boost::format("%s is not of the correct type") % optName));
      }
      return std::get<T>(opts.at(optName));
    }
    template <class T>
    T getOption(const std::string &optName, const T& default_value) const
    {
      if (!opts.count(optName)) {
        return default_value;
      }
      if (!std::holds_alternative<T>(opts.at(optName))) {
        throw std::runtime_error(boost::str(boost::format("%s is not of the correct type") % optName));
      }
      return std::get<T>(opts.at(optName));
    }

    bool operator<(const CheckDesc& rhs) const
    {
      std::map<string, optvalue_t> oopts, rhsoopts;

      for(const auto& m : opts)
        oopts[m.first]=m.second;
      for(const auto& m : rhs.opts)
        rhsoopts[m.first]=m.second;

      return std::tuple(rem, url, oopts) <
        std::tuple(rhs.rem, rhs.url, rhsoopts);
    }
  };
  struct CheckState
  {
    CheckState(time_t _lastAccess): lastAccess(_lastAccess) {}
    /* current status */
    std::atomic<bool> status{false};
    /* current weight */
    std::atomic<int> weight{0};
    /* first check? */
    std::atomic<bool> first{true};
    /* number of successive checks returning failure */
    std::atomic<unsigned int> failures{0};
    /* last time the status was accessed */
    std::atomic<time_t> lastAccess{0};
    /* last time the status was modified */
    std::atomic<time_t> lastStatusUpdate{0};
  };

public:
  IsUpOracle()
  {
    d_checkerThreadStarted.clear();
  }
  ~IsUpOracle() = default;
  int isUp(Logr::log_t slog, const ComboAddress& remote, const opts_t& opts);
  int isUp(Logr::log_t slog, const ComboAddress& remote, const std::string& url, const opts_t& opts);
  //NOLINTNEXTLINE(readability-identifier-length)
  int isUp(const CheckDesc& cd);

private:
  void checkURL(const CheckDesc& cd, const bool status, const bool first) // NOLINT(readability-identifier-length)
  {
    setThreadName("pdns/lua-c-url");

    string remstring;
    try {
      int timeout = std::atoi(cd.getOption<string>("timeout", "2").c_str());
      string useragent = cd.getOption("useragent", productName());
      size_t byteslimit = pdns::checked_stoi<size_t>(cd.getOption<string>("byteslimit", "0"));
      int http_code = pdns::checked_stoi<int>(cd.getOption<string>("httpcode", "200"));

      MiniCurl minicurl(useragent, false);
      
      MiniCurl::MiniCurlHeaders mch;
      for (auto const & header:cd.getOption<LuaAssociativeTable<string>>("headers", {})) {
        auto headername = header.first;
        std::replace(headername.begin(), headername.end(), '_', '-');
        mch.emplace(headername, header.second);
      }

      string content;
      const ComboAddress* rem = nullptr;
      if(cd.rem.sin4.sin_family != AF_UNSPEC) {
        rem = &cd.rem;
        remstring = rem->toString();
      } else {
        remstring = "[externally checked IP]";
      }

      if (cd.opts.count("source")) {
        ComboAddress src{cd.getOption<string>("source")};
        content=minicurl.getURL(cd.url, rem, &src, timeout, &mch, false, false, byteslimit, http_code);
      }
      else {
        content=minicurl.getURL(cd.url, rem, nullptr, timeout, &mch, false, false, byteslimit, http_code);
      }
      if (cd.opts.count("stringmatch") && content.find(cd.getOption<string>("stringmatch")) == string::npos) {
        throw std::runtime_error(boost::str(boost::format("unable to match content with `%s`") % cd.getOption<string>("stringmatch")));
      }

      int weight = 0;
      try {
        weight = stoi(content);
        if(!status) {
          SLOG(g_log<<Logger::Info<<"Lua record monitoring declaring "<<remstring<<" UP for URL "<<cd.url<<"!"<<" with WEIGHT "<<content<<"!"<<endl,
               cd.slog->info(Logr::Info, "Lua record monitoring declares url UP", "ip", Logging::Loggable(remstring), "url", Logging::Loggable(cd.url), "weight", Logging::Loggable(content)));
        }
      }
      catch (const std::exception&) {
        if(!status) {
          SLOG(g_log<<Logger::Info<<"Lua record monitoring declaring "<<remstring<<" UP for URL "<<cd.url<<"!"<<endl,
               cd.slog->info(Logr::Info, "Lua record monitoring declares url UP", "ip", Logging::Loggable(remstring), "url", Logging::Loggable(cd.url)));
        }
      }

      setWeight(cd, weight);
      setUp(cd);
    }
    catch(std::exception& ne) {
      if(status || first) {
        SLOG(g_log<<Logger::Info<<"Lua record monitoring declaring "<<remstring<<" DOWN for URL "<<cd.url<<", error: "<<ne.what()<<endl,
             cd.slog->error(Logr::Info, ne.what(), "Lua record monitoring declares url DOWN", "ip", Logging::Loggable(remstring), "url", Logging::Loggable(cd.url)));
      }
      setWeight(cd, 0);
      setDown(cd);
    }
  }
  void checkTCP(const CheckDesc& cd, const bool status, const bool first) { // NOLINT(readability-identifier-length)
    setThreadName("pdns/lua-c-tcp");
    try {
      int timeout = std::atoi(cd.getOption<string>("timeout", "2").c_str());
      Socket s(cd.rem.sin4.sin_family, SOCK_STREAM);
      ComboAddress src;
      s.setNonBlocking();
      if (cd.opts.count("source")) {
        src = ComboAddress(cd.getOption<string>("source"));
        s.bind(src);
      }
      s.connect(cd.rem, timeout);
      if (!status) {
        if (g_slogStructured) {
          if(cd.opts.count("source")) {
            cd.slog->info(Logr::Info, "Lua record monitoring declares remote UP", "remote", Logging::Loggable(cd.rem.toStringWithPort()), "source", Logging::Loggable(src));
          }
          else {
            cd.slog->info(Logr::Info, "Lua record monitoring declares remote UP", "remote", Logging::Loggable(cd.rem.toStringWithPort()));
          }
        }
        else {
          g_log<<Logger::Info<<"Lua record monitoring declaring TCP/IP "<<cd.rem.toStringWithPort()<<" ";
          if(cd.opts.count("source")) {
            g_log<<"(source "<<src.toString()<<") ";
          }
          g_log<<"UP!"<<endl;
        }
      }
      setUp(cd);
    }
    catch (const NetworkError& ne) {
      if(status || first) {
        SLOG(g_log<<Logger::Info<<"Lua record monitoring declaring TCP/IP "<<cd.rem.toStringWithPort()<<" DOWN: "<<ne.what()<<endl,
            cd.slog->error(Logr::Info, ne.what(), "Lua record monitoring declares remote DOWN", "remote", Logging::Loggable(cd.rem.toStringWithPort())));
      }
      setDown(cd);
    }
  }
  void checkThread()
  {
    setThreadName("pdns/luaupcheck");
    while (true)
    {
      std::chrono::system_clock::time_point checkStart = std::chrono::system_clock::now();
      std::vector<std::future<void>> results;
      std::vector<CheckDesc> toDelete;
      {
        // make sure there's no insertion
        auto statuses = d_statuses.read_lock();
        for (auto& it: *statuses) {
          auto& desc = it.first;
          auto& state = it.second;
          time_t interval{g_luaHealthChecksInterval};
          time_t checkInterval{0};
          auto lastAccess = std::chrono::system_clock::from_time_t(state->lastAccess);

          checkInterval = std::atoi(desc.getOption<string>("interval", "0").c_str());
          if (checkInterval != 0) {
            interval = std::gcd(interval, checkInterval);
          }

          if (not state->first) {
            time_t nextCheckSecond = state->lastStatusUpdate;
            if (checkInterval != 0) {
               nextCheckSecond += checkInterval;
            }
            else {
               nextCheckSecond += g_luaHealthChecksInterval;
            }
            if (checkStart < std::chrono::system_clock::from_time_t(nextCheckSecond)) {
              continue; // too early
            }
          }

          if (desc.url.empty()) { // TCP
            results.push_back(std::async(std::launch::async, &IsUpOracle::checkTCP, this, desc, state->status.load(), state->first.load()));
          } else { // URL
            results.push_back(std::async(std::launch::async, &IsUpOracle::checkURL, this, desc, state->status.load(), state->first.load()));
          }
          // Give it a chance to run at least once.
          // If minimumFailures * interval > lua-health-checks-expire-delay, then a down status will never get reported.
          // This is unlikely to be a problem in practice due to the default value of the expire delay being one hour.
          if (not state->first &&
              lastAccess < (checkStart - std::chrono::seconds(g_luaHealthChecksExpireDelay))) {
            toDelete.push_back(desc);
          }
        }
      }
      // we can release the lock as nothing will be deleted
      for (auto& future: results) {
        future.wait();
      }
      if (!toDelete.empty()) {
        auto statuses = d_statuses.write_lock();
        for (auto& it: toDelete) {
          statuses->erase(it);
        }
      }

      // set thread name again, in case std::async surprised us by doing work in this thread
      setThreadName("pdns/luaupcheck");

      // Wait for at most one complete check interval, but allow an earlier
      // wakeup in case more work is being put in d_statuses.
      {
        std::unique_lock<std::mutex> lock(d_mutex);
        auto sleepTime = std::chrono::seconds(g_luaHealthChecksInterval) - (std::chrono::system_clock::now() - checkStart);
        if (sleepTime > std::chrono::seconds::zero()) {
          d_condvar.wait_until(lock, std::chrono::system_clock::now() + sleepTime);
        }
      }
    }
  }

  typedef map<CheckDesc, std::unique_ptr<CheckState>> statuses_t;
  SharedLockGuarded<statuses_t> d_statuses;

  std::unique_ptr<std::thread> d_checkerThread;
  std::atomic_flag d_checkerThreadStarted;

  std::mutex d_mutex; // used with the condition variable below
  std::condition_variable d_condvar;

  void setStatus(const CheckDesc& cd, bool status)
  {
    auto statuses = d_statuses.write_lock();
    auto& state = (*statuses)[cd];
    state->lastStatusUpdate = time(nullptr);
    state->first = false;
    if (status) {
      state->failures = 0;
      state->status = true;
    } else {
      unsigned int minimumFailures = 1;
      unsigned int value = std::atoi(cd.getOption<string>("minimumFailures", "0").c_str());
      if (value != 0) {
        minimumFailures = std::max(minimumFailures, value);
      }
      // Since `status' was set to false at constructor time, we need to
      // recompute its value unconditionally to expose "down, but not enough
      // times yet" targets as up.
      state->status = ++state->failures < minimumFailures;
    }
  }

  //NOLINTNEXTLINE(readability-identifier-length)
  void setWeight(const CheckDesc& cd, int weight){
    auto statuses = d_statuses.write_lock();
    auto& state = (*statuses)[cd];
    state->weight = weight;
  }

  void setDown(const CheckDesc& cd)
  {
    setStatus(cd, false);
  }

  void setUp(const CheckDesc& cd)
  {
    setStatus(cd, true);
  }
};

// The return value of this function can be one of three sets of values:
// - positive integer: the target is up, the return value is its weight.
//   (1 if weights are not used)
// - zero: the target is down.
// - negative integer: the check for this target has not completed yet.
//   (this value is only reported if the failOnIncompleteCheck option is
//    set, otherwise zero will be returned)
//NOLINTNEXTLINE(readability-identifier-length)
int IsUpOracle::isUp(const CheckDesc& cd)
{
  time_t now = time(nullptr);
  {
    auto statuses = d_statuses.read_lock();
    auto iter = statuses->find(cd);
    if (iter != statuses->end()) {
      iter->second->lastAccess = now;
      if (iter->second->weight > 0) {
        return iter->second->weight;
      }
      return static_cast<int>(iter->second->status);
    }
  }
  // try to parse options so we don't insert any malformed content
  if (cd.opts.count("source")) {
    ComboAddress src(cd.getOption<string>("source"));
  }
  {
    auto statuses = d_statuses.write_lock();
    // Make sure we don't insert new entry twice now we have the lock
    if (statuses->find(cd) == statuses->end()) {
      (*statuses)[cd] = std::make_unique<CheckState>(now);
    }
  }
  // Now that we have given it work to do, make sure the checker thread runs,
  // and notify it if it had already been running.
  if (!d_checkerThreadStarted.test_and_set()) {
    d_checkerThread = std::make_unique<std::thread>([this] { return checkThread(); });
  }
  else {
    d_condvar.notify_all();
  }
  // If explicitly asked to fail on incomplete checks, report this (as
  // a negative value).
  static const std::string foic{"failOnIncompleteCheck"};
  if (cd.opts.count(foic) != 0) {
    if (cd.getOption<string>(foic) == "true") {
      return -1;
    }
  }
  return 0;
}

int IsUpOracle::isUp(Logr::log_t slog, const ComboAddress& remote, const opts_t& opts)
{
  CheckDesc cd{remote, "", opts, slog};
  return isUp(cd);
}

int IsUpOracle::isUp(Logr::log_t slog, const ComboAddress& remote, const std::string& url, const opts_t& opts)
{
  CheckDesc cd{remote, url, opts, slog};
  return isUp(cd);
}

IsUpOracle g_up;
namespace {
template<typename T, typename C>
bool doCompare(const T& var, const std::string& res, const C& cmp)
{
  if(auto country = boost::get<string>(&var))
    return cmp(*country, res);

  auto countries=boost::get<vector<pair<int,string> > >(&var);
  for(const auto& country : *countries) {
    if(cmp(country.second, res))
      return true;
  }
  return false;
}
}

static std::string getGeo(Logr::log_t slog, const std::string& ip, GeoIPInterface::GeoIPQueryAttribute qa)
{
  static bool initialized;
  extern std::function<std::string(const std::string& ip, int)> g_getGeo;
  if(!g_getGeo) {
    if(!initialized) {
      SLOG(g_log<<Logger::Error<<"Lua record attempted to use GeoIPBackend functionality, but backend not launched"<<endl,
           slog->info(Logr::Error, "Lua record attempted to use GeoIPBackend functionality, but backend not launched"));
      initialized=true;
    }
    return "unknown";
  }
  else
    return g_getGeo(ip, (int)qa);
}

template <typename T>
static T pickRandom(const vector<T>& items)
{
  if (items.empty()) {
    throw std::invalid_argument("The items list cannot be empty");
  }
  return items[dns_random(items.size())];
}

template <typename T>
static T pickHashed(const ComboAddress& who, const vector<T>& items)
{
  if (items.empty()) {
    throw std::invalid_argument("The items list cannot be empty");
  }
  ComboAddress::addressOnlyHash aoh;
  return items[aoh(who) % items.size()];
}

template <typename T>
static T pickWeightedRandom(const vector< pair<unsigned int, T> >& items)
{
  if (items.empty()) {
    throw std::invalid_argument("The items list cannot be empty");
  }
  unsigned int sum=0;
  vector< pair<unsigned int, T> > pick;
  pick.reserve(items.size());

  for(auto& i : items) {
    sum += i.first;
    pick.emplace_back(sum, i.second);
  }

  if (sum == 0) {
    throw std::invalid_argument("The sum of items cannot be zero");
  }

  int r = dns_random(sum);
  auto p = upper_bound(pick.begin(), pick.end(), r, [](unsigned int rarg, const typename decltype(pick)::value_type& a) { return rarg < a.first; });
  return p->second;
}

template <typename T>
static T pickWeightedHashed(const ComboAddress& bestwho, const vector< pair<unsigned int, T> >& items)
{
  if (items.empty()) {
    throw std::invalid_argument("The items list cannot be empty");
  }
  unsigned int sum=0;
  vector< pair<unsigned int, T> > pick;
  pick.reserve(items.size());

  for(auto& i : items) {
    sum += i.first;
    pick.push_back({sum, i.second});
  }

  if (sum == 0) {
    throw std::invalid_argument("The sum of items cannot be zero");
  }

  ComboAddress::addressOnlyHash aoh;
  int r = aoh(bestwho) % sum;
  auto p = upper_bound(pick.begin(), pick.end(), r, [](unsigned int rarg, const typename decltype(pick)::value_type& a) { return rarg < a.first; });
  return p->second;
}

template <typename T>
static T pickWeightedNameHashed(const DNSName& dnsname, vector< pair<unsigned int, T> >& items)
{
  if (items.empty()) {
    throw std::invalid_argument("The items list cannot be empty");
  }
  size_t sum=0;
  vector< pair<unsigned int, T> > pick;
  pick.reserve(items.size());

  for(auto& i : items) {
    sum += i.first;
    pick.push_back({sum, i.second});
  }

  if (sum == 0) {
    throw std::invalid_argument("The sum of items cannot be zero");
  }

  size_t r = dnsname.hash() % sum;
  auto p = upper_bound(pick.begin(), pick.end(), r, [](unsigned int rarg, const typename decltype(pick)::value_type& a) { return rarg < a.first; });
  return p->second;
}

template <typename T>
static vector<T> pickRandomSample(int n, const vector<T>& items)
{
  if (items.empty()) {
    throw std::invalid_argument("The items list cannot be empty");
  }

  vector<T> pick;
  pick.reserve(items.size());

  for(auto& item : items) {
    pick.push_back(item);
  }

  int count = std::min(std::max<size_t>(0, n), items.size());

  if (count == 0) {
    return vector<T>();
  }

  std::shuffle(pick.begin(), pick.end(), pdns::dns_random_engine());

  vector<T> result = {pick.begin(), pick.begin() + count};
  return result;
}

static bool getLatLon(Logr::log_t slog, const std::string& ip, double& lat, double& lon)
{
  string inp = getGeo(slog, ip, GeoIPInterface::Location);
  if(inp.empty())
    return false;
  lat=atof(inp.c_str());
  auto pos=inp.find(' ');
  if(pos != string::npos)
    lon=atof(inp.c_str() + pos);
  return true;
}

static bool getLatLon(Logr::log_t slog, const std::string& ip, string& loc)
{
  int latdeg, latmin, londeg, lonmin;
  double latsec, lonsec;
  char lathem='X', lonhem='X';

  double lat = 0, lon = 0;
  if(!getLatLon(slog, ip, lat, lon))
    return false;

  if(lat > 0) {
    lathem='N';
  }
  else {
    lat = -lat;
    lathem='S';
  }

  if(lon > 0) {
    lonhem='E';
  }
  else {
    lon = -lon;
    lonhem='W';
  }

  latdeg = lat;
  latmin = (lat - latdeg)*60.0;
  latsec = (((lat - latdeg)*60.0) - latmin)*60.0;

  londeg = lon;
  lonmin = (lon - londeg)*60.0;
  lonsec = (((lon - londeg)*60.0) - lonmin)*60.0;

  // 51 59 00.000 N 5 55 00.000 E 4.00m 1.00m 10000.00m 10.00m

  boost::format fmt("%d %d %d %c %d %d %d %c 0.00m 1.00m 10000.00m 10.00m");

  loc= (fmt % latdeg % latmin % latsec % lathem % londeg % lonmin % lonsec % lonhem ).str();
  return true;
}

static ComboAddress pickclosest(Logr::log_t slog, const ComboAddress& bestwho, const vector<ComboAddress>& wips)
{
  if (wips.empty()) {
    throw std::invalid_argument("The IP list cannot be empty");
  }
  map<double, vector<ComboAddress> > ranked;
  double wlat=0, wlon=0;
  getLatLon(slog, bestwho.toString(), wlat, wlon);
  //        cout<<"bestwho "<<wlat<<", "<<wlon<<endl;
  vector<string> ret;
  for(const auto& c : wips) {
    double lat=0, lon=0;
    getLatLon(slog, c.toString(), lat, lon);
    //          cout<<c.toString()<<": "<<lat<<", "<<lon<<endl;
    double latdiff = wlat-lat;
    double londiff = wlon-lon;
    if(londiff > 180)
      londiff = 360 - londiff;
    double dist2=latdiff*latdiff + londiff*londiff;
    //          cout<<"    distance: "<<sqrt(dist2) * 40000.0/360<<" km"<<endl; // length of a degree
    ranked[dist2].push_back(c);
  }
  return ranked.begin()->second[dns_random(ranked.begin()->second.size())];
}

static std::vector<DNSZoneRecord> lookup(const DNSName& name, uint16_t qtype, domainid_t zoneid)
{
  static LockGuarded<UeberBackend> s_ub;

  DNSZoneRecord dr;
  vector<DNSZoneRecord> ret;
  {
    auto ub = s_ub.lock();
    ub->lookup(QType(qtype), name, zoneid);
    while (ub->get(dr)) {
      ret.push_back(dr);
    }
  }
  return ret;
}

static bool getAuth(const ZoneName& name, uint16_t qtype, SOAData* soaData, Netmask remote)
{
  static LockGuarded<UeberBackend> s_ub;

  {
    auto ueback = s_ub.lock();
    return ueback->getAuth(name, qtype, soaData, remote);
  }
}

template<typename T>
static T getOptionValue(const boost::optional<opts_t>& options, const std::string &name, const T &defaultValue)
{
  T selector=defaultValue;
  if(options) {
    if(options->count(name))
      selector = std::get<T>(options->find(name)->second);
  }
  return selector;
}

static vector<ComboAddress> useSelector(Logr::log_t slog, const std::string &selector, const ComboAddress& bestwho, const vector<ComboAddress>& candidates)
{
  vector<ComboAddress> ret;

  if(selector=="all")
    return candidates;
  else if(selector=="empty")
    return ret;
  else if(selector=="random")
    ret.emplace_back(pickRandom<ComboAddress>(candidates));
  else if(selector=="pickclosest")
    ret.emplace_back(pickclosest(slog, bestwho, candidates));
  else if(selector=="hashed")
    ret.emplace_back(pickHashed<ComboAddress>(bestwho, candidates));
  else {
    SLOG(g_log<<Logger::Warning<<"Lua record called with unknown selector '"<<selector<<"'"<<endl,
         slog->info(Logr::Warning, "Lua record called with unknown selector", "selector", Logging::Loggable(selector)));
    ret.emplace_back(pickRandom<ComboAddress>(candidates));
  }

  return ret;
}

static vector<string> convComboAddressListToString(const vector<ComboAddress>& items)
{
  vector<string> result;
  result.reserve(items.size());

  for (const auto& item : items) {
    result.emplace_back(item.toString());
  }

  return result;
}

static vector<ComboAddress> convComboAddressList(const iplist_t& items, uint16_t port=0)
{
  vector<ComboAddress> result;
  result.reserve(items.size());

  for(const auto& item : items) {
    result.emplace_back(ComboAddress(item.second, port));
  }

  return result;
}

/**
 * Reads and unify single or multiple sets of ips :
 * - {'192.0.2.1', '192.0.2.2'}
 * - {{'192.0.2.1', '192.0.2.2'}, {'198.51.100.1'}}
 */

static vector<vector<ComboAddress>> convMultiComboAddressList(const boost::variant<iplist_t, ipunitlist_t>& items, uint16_t port = 0)
{
  vector<vector<ComboAddress>> candidates;

  if(auto simple = boost::get<iplist_t>(&items)) {
    vector<ComboAddress> unit = convComboAddressList(*simple, port);
    candidates.push_back(unit);
  } else {
    auto units = boost::get<ipunitlist_t>(items);
    for(const auto& u : units) {
      vector<ComboAddress> unit = convComboAddressList(u.second, port);
      candidates.push_back(std::move(unit));
    }
  }
  return candidates;
}

static vector<string> convStringList(const iplist_t& items)
{
  vector<string> result;
  result.reserve(items.size());

  for(const auto& item : items) {
    result.emplace_back(item.second);
  }

  return result;
}

static vector< pair<unsigned int, string> > convIntStringPairList(const std::unordered_map<int, wiplist_t >& items)
{
  vector<pair<unsigned int,string> > result;
  result.reserve(items.size());

  for(const auto& item : items) {
    result.emplace_back(pdns::checked_stoi_nonzero<unsigned int>(item.second.at(1).c_str()), item.second.at(2));
  }

  return result;
}

bool g_LuaRecordSharedState;

typedef struct AuthLuaRecordContext
{
  ComboAddress          bestwho;
  DNSName               qname;
  DNSZoneRecord         zone_record;
  DNSName               zone;
  Netmask               remote;
} lua_record_ctx_t;

static thread_local unique_ptr<lua_record_ctx_t> s_lua_record_ctx;

/*
 *  Holds computed hashes for a given entry
 */
struct EntryHashesHolder
{
  std::atomic<size_t> weight;
  std::string entry;
  SharedLockGuarded<std::vector<unsigned int>> hashes;
  std::atomic<time_t> lastUsed;

  EntryHashesHolder(size_t weight_, std::string entry_, time_t lastUsed_ = time(nullptr)): weight(weight_), entry(std::move(entry_)), lastUsed(lastUsed_) {
  }

  bool hashesComputed() {
    return weight == hashes.read_lock()->size();
  }
  void hash() {
    auto locked = hashes.write_lock();
    locked->clear();
    locked->reserve(weight);
    size_t count = 0;
    while (count < weight) {
      auto value = boost::str(boost::format("%s-%d") % entry % count);
      // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
      auto whash = burtle(reinterpret_cast<const unsigned char*>(value.data()), value.size(), 0);
      locked->push_back(whash);
      ++count;
    }
    std::sort(locked->begin(), locked->end());
  }
};

using zone_hashes_key_t = std::tuple<int, std::string, std::string>;

static SharedLockGuarded<std::map<
  zone_hashes_key_t, // zoneid qname entry
  std::shared_ptr<EntryHashesHolder> // entry w/ corresponding hashes
  >>
s_zone_hashes;

static std::atomic<time_t> s_lastConsistentHashesCleanup = 0;

/**
 * every ~g_luaConsistentHashesCleanupInterval, do a cleanup to delete entries that haven't been used in the last g_luaConsistentHashesExpireDelay
 */
static void cleanZoneHashes()
{
  auto now = time(nullptr);
  if (s_lastConsistentHashesCleanup > (now - g_luaConsistentHashesCleanupInterval)) {
    return ;
  }
  s_lastConsistentHashesCleanup = now;
  std::vector<zone_hashes_key_t> toDelete{};
  {
    auto locked = s_zone_hashes.read_lock();
    auto someTimeAgo = now - g_luaConsistentHashesExpireDelay;

    for (const auto& [key, entry]: *locked) {
      if (entry->lastUsed > someTimeAgo) {
        toDelete.push_back(key);
      }
    }
  }
  if (!toDelete.empty()) {
    auto wlocked = s_zone_hashes.write_lock();
    for (const auto& key : toDelete) {
      wlocked->erase(key);
    }
  }
}

static std::vector<std::shared_ptr<EntryHashesHolder>> getCHashedEntries(const domainid_t zoneId, const std::string& queryName, const std::vector<std::pair<unsigned int, std::string>>& items)
{
  std::vector<std::shared_ptr<EntryHashesHolder>> result{};
  std::map<zone_hashes_key_t, std::shared_ptr<EntryHashesHolder>> newEntries{};

  {
    time_t now = time(nullptr);
    auto locked = s_zone_hashes.read_lock();

    for (const auto& [weight, entry]: items) {
      auto key = std::make_tuple(zoneId, queryName, entry);
      if (locked->count(key) == 0) {
        newEntries[key] = std::make_shared<EntryHashesHolder>(weight, entry, now);
      } else {
        locked->at(key)->weight = weight;
        locked->at(key)->lastUsed = now;
        result.push_back(locked->at(key));
      }
    }
  }
  if (!newEntries.empty()) {
    auto wlocked = s_zone_hashes.write_lock();

    for (auto& [key, entry]: newEntries) {
      result.push_back(entry);
      (*wlocked)[key] = std::move(entry);
    }
  }

  return result;
}

static std::string pickConsistentWeightedHashed(const ComboAddress& bestwho, const std::vector<std::pair<unsigned int, std::string>>& items)
{
  const auto& zoneId = s_lua_record_ctx->zone_record.domain_id;
  const auto queryName = s_lua_record_ctx->qname.toString();
  unsigned int sel = std::numeric_limits<unsigned int>::max();
  unsigned int min = std::numeric_limits<unsigned int>::max();

  boost::optional<std::string> ret;
  boost::optional<std::string> first;

  cleanZoneHashes();

  auto entries = getCHashedEntries(zoneId, queryName, items);

  ComboAddress::addressOnlyHash addrOnlyHash;
  auto qhash = addrOnlyHash(bestwho);
  for (const auto& entry : entries) {
    if (!entry->hashesComputed()) {
      entry->hash();
    }
    {
      const auto hashes = entry->hashes.read_lock();
      if (!hashes->empty()) {
        if (min > *(hashes->begin())) {
          min = *(hashes->begin());
          first = entry->entry;
        }

        auto hash_it = std::lower_bound(hashes->begin(), hashes->end(), qhash);
        if (hash_it != hashes->end()) {
          if (*hash_it < sel) {
            sel = *hash_it;
            ret = entry->entry;
          }
        }
      }
    }
  }
  if (ret != boost::none) {
    return *ret;
  }
  if (first != boost::none) {
    return *first;
  }
  return {};
}

static vector<string> genericIfUp(Logr::log_t slog, const boost::variant<iplist_t, ipunitlist_t>& ips, boost::optional<opts_t> options, const std::function<int(Logr::log_t, const ComboAddress&, const opts_t&)>& upcheckf, uint16_t port = 0)
{
  vector<vector<ComboAddress> > candidates;
  opts_t opts;
  if (options) {
    opts = *options;
  }

  candidates = convMultiComboAddressList(ips, port);

  bool incompleteCheck{true};
  for(const auto& unit : candidates) {
    vector<ComboAddress> available;
    for(const auto& address : unit) {
      int status = upcheckf(slog, address, opts);
      if (status > 0) {
        available.push_back(address);
      }
      if (status >= 0) {
        incompleteCheck = false;
      }
    }
    if(!available.empty()) {
      vector<ComboAddress> res = useSelector(slog, getOptionValue<string>(options, "selector", "random"), s_lua_record_ctx->bestwho, available);
      return convComboAddressListToString(res);
    }
  }

  // All units down or have not completed their checks yet.
  if (incompleteCheck) {
    throw std::runtime_error("if{url,port}up health check has not completed yet");
  }

  // Apply backupSelector on all candidates in the first non-empty group
  vector<ComboAddress> res;
  for (const auto& unit : candidates) {
    if (!unit.empty()) {
      res = useSelector(slog, getOptionValue<string>(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, unit);
      break;
    }
  }
  return convComboAddressListToString(res);
}

// Lua functions available to the user

static string lua_latlon(Logr::log_t slog)
{
  double lat{0};
  double lon{0};
  getLatLon(slog, s_lua_record_ctx->bestwho.toString(), lat, lon);
  return std::to_string(lat)+" "+std::to_string(lon);
}

static string lua_latlonloc(Logr::log_t slog)
{
  string loc;
  getLatLon(slog, s_lua_record_ctx->bestwho.toString(), loc);
  return loc;
}

static string lua_closestMagic(Logr::log_t slog)
{
  vector<ComboAddress> candidates;
  // Getting something like 192-0-2-1.192-0-2-2.198-51-100-1.example.org
  for (auto label : s_lua_record_ctx->qname.getRawLabels()) {
    std::replace(label.begin(), label.end(), '-', '.');
    try {
      candidates.emplace_back(label);
    } catch (const PDNSException& exc) {
      // no need to continue as we most likely reached the end of the ip list
      break ;
    }
  }
  return pickclosest(slog, s_lua_record_ctx->bestwho, candidates).toString();
}

static string lua_latlonMagic(Logr::log_t slog)
{
  auto labels = s_lua_record_ctx->qname.getRawLabels();
  if (labels.size() < 4) {
    return {"unknown"};
  }
  double lat{0};
  double lon{0};
  getLatLon(slog, labels[3]+"."+labels[2]+"."+labels[1]+"."+labels[0], lat, lon);
  return std::to_string(lat)+" "+std::to_string(lon);
}

static string lua_createReverse(Logr::log_t slog, const string &format, boost::optional<opts_t> exceptions)
{
  try {
    auto labels = s_lua_record_ctx->qname.getRawLabels();
    if (labels.size() < 4) {
      return {"unknown"};
    }

    // so, query comes in for 4.3.2.1.in-addr.arpa, zone is called 2.1.in-addr.arpa
    // exceptions["1.2.3.4"]="bert.powerdns.com" then provides an exception
    if (exceptions) {
      ComboAddress req(labels[3]+"."+labels[2]+"."+labels[1]+"."+labels[0], 0);
      const auto& uom = *exceptions;
      for (const auto& address : uom) {
        if(ComboAddress(address.first, 0) == req) {
          return std::get<string>(address.second);
        }
      }
    }
    boost::format fmt(format);
    fmt.exceptions(boost::io::all_error_bits ^ (boost::io::too_many_args_bit | boost::io::too_few_args_bit));
    fmt % labels[3] % labels[2] % labels[1] % labels[0];

    fmt % (labels[3]+"-"+labels[2]+"-"+labels[1]+"-"+labels[0]);

    boost::format fmt2("%02x%02x%02x%02x");
    for (int i = 3; i >= 0; --i) {
      fmt2 % atoi(labels[i].c_str());
    }

    fmt % (fmt2.str());

    return fmt.str();
  }
  catch(std::exception& ex) {
    SLOG(g_log<<Logger::Error<<"createReverse error: "<<ex.what()<<endl,
         slog->error(Logr::Error, ex.what(), "Lua record exception in createReverse"));
  }
  catch (const PDNSException &e) {
    SLOG(g_log<<Logger::Error<<"createReverse error: "<<e.reason<<endl,
         slog->error(Logr::Error, e.reason, "Lua record exception in createReverse"));
  }
  return {"error"};
}

static string allZerosIP{"0.0.0.0"};
static string allZerosIP6{"::"};

static string lua_createForward()
{
  try {
    DNSName record_name{s_lua_record_ctx->zone_record.dr.d_name};
    if (!record_name.isWildcard()) {
      return allZerosIP;
    }
    record_name.chopOff();
    DNSName rel{s_lua_record_ctx->qname.makeRelative(record_name)};

    // parts is something like ["1", "2", "3", "4", "static"] or
    // ["1", "2", "3", "4"] or ["ip40414243", "ip-addresses", ...]
    auto parts = rel.getRawLabels();
    // Yes, this still breaks if an 1-2-3-4.XXXX is nested too deeply...
    if (parts.size() >= 4) {
      ComboAddress address(parts[0]+"."+parts[1]+"."+parts[2]+"."+parts[3]);
      return address.toString();
    }
    if (!parts.empty()) {
      auto& input = parts.at(0);

      // allow a word without - in front, as long as it does not contain anything that could be a number
      size_t nonhexprefix = strcspn(input.c_str(), "0123456789abcdefABCDEF");
      if (nonhexprefix > 0) {
        input = input.substr(nonhexprefix);
      }

      // either hex string, or 12-13-14-15
      vector<string> ip_parts;

      stringtok(ip_parts, input, "-");
      if (ip_parts.size() >= 4) {
        // 1-2-3-4 with any prefix (e.g. ip-foo-bar-1-2-3-4)
        string ret;
        for (size_t index=4; index > 0; index--) {
          const auto octet = ip_parts.at(ip_parts.size() - index);
          size_t octetLength{0};
          auto octetVal = pdns::checked_stoi<uint8_t>(octet, &octetLength); // may throw

          if (octetLength != octet.length()) { // trailing chars after number
            return allZerosIP;
          }
          ret += std::to_string(octetVal) + ".";
        }
        ret.resize(ret.size() - 1); // remove trailing dot after last octet
        return ret;
      }
      if (input.length() >= 8) {
        auto last8 = input.substr(input.length()-8);
        unsigned int part1{0};
        unsigned int part2{0};
        unsigned int part3{0};
        unsigned int part4{0};
        if (sscanf(last8.c_str(), "%02x%02x%02x%02x", &part1, &part2, &part3, &part4) == 4) {
          ComboAddress address(std::to_string(part1) + "." + std::to_string(part2) + "." + std::to_string(part3) + "." + std::to_string(part4));
          return address.toString();
        }
      }
    }
    return allZerosIP;
  } catch (const PDNSException &) {
    return allZerosIP;
  } catch (const std::exception &) { // thrown by pdns::checked_stoi
    return allZerosIP;
  }
}

static string lua_createForward6()
{
   try {
     DNSName record_name{s_lua_record_ctx->zone_record.dr.d_name};
     if (!record_name.isWildcard()) {
       return allZerosIP6;
     }
     record_name.chopOff();
     DNSName rel{s_lua_record_ctx->qname.makeRelative(record_name)};

     auto parts = rel.getRawLabels();
     if (parts.size() == 8) {
       string tot;
       for (int chunk = 0; chunk < 8; ++chunk) {
         if (chunk != 0) {
           tot.append(1, ':');
         }
        tot += parts.at(chunk);
      }
      ComboAddress address(tot);
      return address.toString();
    }
    if (parts.size() == 1) {
      if (parts[0].find('-') != std::string::npos) {
        std::replace(parts[0].begin(), parts[0].end(), '-', ':');
        ComboAddress address(parts[0]);
        return address.toString();
      }
      if (parts[0].size() >= 32) {
        auto ippart = parts[0].substr(parts[0].size()-32);
        auto fulladdress =
          ippart.substr(0, 4) + ":" +
          ippart.substr(4, 4) + ":" +
          ippart.substr(8, 4) + ":" +
          ippart.substr(12, 4) + ":" +
          ippart.substr(16, 4) + ":" +
          ippart.substr(20, 4) + ":" +
          ippart.substr(24, 4) + ":" +
          ippart.substr(28, 4);

        ComboAddress address(fulladdress);
        return address.toString();
      }
    }
    return allZerosIP6;
  } catch (const PDNSException &e) {
    return allZerosIP6;
  }
}

static string lua_createReverse6(Logr::log_t slog, const string &format, boost::optional<opts_t> exceptions)
{
  try {
    auto labels= s_lua_record_ctx->qname.getRawLabels();
    if (labels.size()<32) {
      return {"unknown"};
    }

    boost::format fmt(format);
    fmt.exceptions(boost::io::all_error_bits ^ (boost::io::too_many_args_bit | boost::io::too_few_args_bit));

    string together;
    vector<string> quads;
    for (int chunk = 0; chunk < 8; ++chunk) {
      if (chunk != 0) {
        together += ":";
      }
      string lquad;
      for (int quartet = 0; quartet < 4; ++quartet) {
        lquad.append(1, labels[31 - chunk * 4 - quartet][0]);
        together += labels[31 - chunk * 4 - quartet][0];
      }
      quads.push_back(std::move(lquad));
    }
    ComboAddress ip6(together,0);

    if (exceptions) {
      auto& addrs=*exceptions;
      for(const auto& addr: addrs) {
        // this makes sure we catch all forms of the address
        if (ComboAddress(addr.first, 0) == ip6) {
          return std::get<string>(addr.second);
        }
      }
    }

    string dashed=ip6.toString();
    std::replace(dashed.begin(), dashed.end(), ':', '-');

    // https://github.com/PowerDNS/pdns/issues/7524
    if (boost::ends_with(dashed, "-")) {
      // "a--a-" -> "a--a-0"
      dashed.push_back('0');
    }
    if (boost::starts_with(dashed, "-") || dashed.compare(2, 2, "--") == 0) {
      // "-a--a" -> "0-a--a"               "aa--a" -> "0aa--a"
      dashed.insert(0, "0");
    }

    for (int byte = 31; byte >= 0; --byte) {
      fmt % labels[byte];
    }
    fmt % dashed;

    for(const auto& lquad : quads) {
      fmt % lquad;
    }

    return fmt.str();
  }
  catch(std::exception& ex) {
    SLOG(g_log<<Logger::Error<<"createReverse6 exception: "<<ex.what()<<endl,
         slog->error(Logr::Error, ex.what(), "Lua record exception in createReverse6"));
  }
  catch(PDNSException& ex) {
    SLOG(g_log<<Logger::Error<<"createReverse6 exception: "<<ex.reason<<endl,
         slog->error(Logr::Error, ex.reason, "Lua record exception in createReverse6"));
  }
  return {"error"};
}

static vector<string> lua_filterForward(const string& address, NetmaskGroup& nmg, boost::optional<string> fallback)
{
  ComboAddress caddr(address);

  if (nmg.match(caddr)) {
    return {address};
  }
  if (fallback) {
    if (fallback->empty()) {
      // if fallback is an empty string, return an empty array
      return {};
    }
    return {*fallback};
  }

  if (caddr.isIPv4()) {
    return {allZerosIP};
  }
  return {allZerosIP6};
}

/*
 * Simplistic test to see if an IP address listens on a certain port
 * Will return a single IP address from the set of available IP addresses. If
 * no IP address is available, will return a random element of the set of
 * addresses supplied for testing.
 *
 * @example ifportup(443, { '1.2.3.4', '5.4.3.2' })"
 */
static vector<string> lua_ifportup(Logr::log_t slog, int port, const boost::variant<iplist_t, ipunitlist_t>& ips, boost::optional<opts_t> options)
{
  port = std::max(port, 0);
  port = std::min(port, static_cast<int>(std::numeric_limits<uint16_t>::max()));

  auto checker = [](Logr::log_t log, const ComboAddress& addr, const opts_t& opts) -> int {
    return g_up.isUp(log, addr, opts);
  };
  return genericIfUp(slog, ips, std::move(options), checker, port);
}

static vector<string> lua_ifurlextup(Logr::log_t slog, const vector<pair<int, opts_t> >& ipurls, boost::optional<opts_t> options)
{
  vector<ComboAddress> candidates;
  opts_t opts;
  if (options) {
    opts = *options;
  }

  ComboAddress ca_unspec;
  ca_unspec.sin4.sin_family=AF_UNSPEC;

  // ipurls: { { ["192.0.2.1"] = "https://example.com", ["192.0.2.2"] = "https://example.com/404" } }
  bool incompleteCheck{true};
  for (const auto& [count, unitmap] : ipurls) {
    // unitmap: 1 = { ["192.0.2.1"] = "https://example.com", ["192.0.2.2"] = "https://example.com/404" }
    vector<ComboAddress> available;

    for (const auto& [ipStr, url] : unitmap) {
      // unit: ["192.0.2.1"] = "https://example.com"
      ComboAddress address(ipStr);
      candidates.push_back(address);
      int status = g_up.isUp(slog, ca_unspec, std::get<string>(url), opts);
      if (status > 0) {
        available.push_back(address);
      }
      if (status >= 0) {
        incompleteCheck = false;
      }
    }
    if(!available.empty()) {
      vector<ComboAddress> res = useSelector(slog, getOptionValue<string>(options, "selector", "random"), s_lua_record_ctx->bestwho, available);
      return convComboAddressListToString(res);
    }
  }

  // All units down or have not completed their checks yet.
  if (incompleteCheck) {
    throw std::runtime_error("ifexturlup health check has not completed yet");
  }

  // Apply backupSelector on all candidates in the first non-empty group
  vector<ComboAddress> first;
  for (const auto& [count, unitmap] : ipurls) {
    if (!unitmap.empty()) {
      first.reserve(unitmap.size());
      for (const auto& [ipStr, url] : unitmap) {
        first.emplace_back(ipStr);
      }
      break;
    }
  }
  vector<ComboAddress> res = useSelector(slog, getOptionValue<string>(options, "backupSelector", "random"), s_lua_record_ctx->bestwho, first);
  return convComboAddressListToString(res);
}

static vector<string> lua_ifurlup(Logr::log_t slog, const std::string& url, const boost::variant<iplist_t, ipunitlist_t>& ips, boost::optional<opts_t> options)
{
  auto checker = [&url](Logr::log_t log, const ComboAddress& addr, const opts_t& opts) -> int {
    return g_up.isUp(log, addr, url, opts);
  };
  return genericIfUp(slog, ips, std::move(options), checker);
}

/*
 * Returns a random IP address from the supplied list
 * @example pickrandom({ '1.2.3.4', '5.4.3.2' })"
 */
static string lua_pickrandom(const iplist_t& ips)
{
  vector<string> items = convStringList(ips);
  return pickRandom<string>(items);
}

/*
 * Based on the hash of `bestwho`, returns an IP address from the list
 * supplied, weighted according to the results of isUp calls.
 * @example pickselfweighted('http://example.com/weight', { "192.0.2.20", "203.0.113.4", "203.0.113.2" })
 */
static string lua_pickselfweighted(Logr::log_t slog, const std::string& url, const iplist_t& ips, boost::optional<opts_t> options)
{
  vector< pair<unsigned int, ComboAddress> > items;
  opts_t opts;
  if(options) {
    opts = *options;
  }

  items.reserve(ips.capacity());
  bool available = false;

  vector<ComboAddress> conv = convComboAddressList(ips);
  for (auto& entry : conv) {
    int weight = 0;
    weight = g_up.isUp(slog, entry, url, opts);
    if(weight>0) {
      available = true;
    }
    items.emplace_back(weight, entry);
  }
  if(available) {
    return pickWeightedHashed<ComboAddress>(s_lua_record_ctx->bestwho, items).toString();
  }

  // All units down, apply backupSelector on all candidates
  return pickWeightedRandom<ComboAddress>(items).toString();
}

static vector<string> lua_pickrandomsample(int n, const iplist_t& ips)
{
  vector<string> items = convStringList(ips);
  return pickRandomSample<string>(n, items);
}

static string lua_pickhashed(const iplist_t& ips)
{
  vector<string> items = convStringList(ips);
  return pickHashed<string>(s_lua_record_ctx->bestwho, items);
}

/*
 * Returns a random IP address from the supplied list, as weighted by the
 * various ``weight`` parameters
 * @example pickwrandom({ {100, '1.2.3.4'}, {50, '5.4.3.2'}, {1, '192.168.1.0'} })
 */
static string lua_pickwrandom(const std::unordered_map<int, wiplist_t>& ips)
{
  vector< pair<unsigned int, string> > items = convIntStringPairList(ips);
  return pickWeightedRandom<string>(items);
}

/*
 * Based on the hash of `bestwho`, returns an IP address from the list
 * supplied, as weighted by the various `weight` parameters
 * @example pickwhashed({ {15, '1.2.3.4'}, {50, '5.4.3.2'} })
 */
static string lua_pickwhashed(std::unordered_map<int, wiplist_t> ips)
{
  vector< pair<unsigned int, string> > items;

  items.reserve(ips.size());
  for (auto& entry : ips) {
    items.emplace_back(pdns::checked_stoi_nonzero<unsigned int>(entry.second[1].c_str()), entry.second[2]);
  }

  return pickWeightedHashed<string>(s_lua_record_ctx->bestwho, items);
}

/*
 * Based on the hash of the record name, return an IP address from the list
 * supplied, as weighted by the various `weight` parameters
 * @example picknamehashed({ {15, '1.2.3.4'}, {50, '5.4.3.2'} })
 */
static string lua_picknamehashed(std::unordered_map<int, wiplist_t> ips)
{
  vector< pair<unsigned int, string> > items;

  items.reserve(ips.size());
  for (auto& address : ips) {
    items.emplace_back(pdns::checked_stoi_nonzero<unsigned int>(address.second[1].c_str()), address.second[2]);
  }

  return pickWeightedNameHashed<string>(s_lua_record_ctx->qname, items);
}

/*
 * Based on the hash of `bestwho`, returns an IP address from the list
 * supplied, as weighted by the various `weight` parameters and distributed consistently
 * @example pickchashed({ {15, '1.2.3.4'}, {50, '5.4.3.2'} })
 */
static string lua_pickchashed(const std::unordered_map<int, wiplist_t>& ips)
{
  std::vector<std::pair<unsigned int, std::string>> items;

  items.reserve(ips.size());
  for (const auto& entry : ips) {
    items.emplace_back(pdns::checked_stoi_nonzero<unsigned int>(entry.second.at(1).c_str()), entry.second.at(2));
  }

  return pickConsistentWeightedHashed(s_lua_record_ctx->bestwho, items);
}

static string lua_pickclosest(Logr::log_t slog, const iplist_t& ips)
{
  vector<ComboAddress> conv = convComboAddressList(ips);

  return pickclosest(slog, s_lua_record_ctx->bestwho, conv).toString();
}

static void lua_report(const string& /* event */, const boost::optional<string>& /* line */)
{
  throw std::runtime_error("Script took too long");
}

static string lua_geoiplookup(Logr::log_t slog, const string &address, const GeoIPInterface::GeoIPQueryAttribute attr)
{
  return getGeo(slog, address, attr);
}

using combovar_t = const boost::variant<string,vector<pair<int,string> > >;

static bool lua_asnum(Logr::log_t slog, const combovar_t& asns)
{
  string res=getGeo(slog, s_lua_record_ctx->bestwho.toString(), GeoIPInterface::ASn);
  return doCompare(asns, res, [](const std::string& arg1, const std::string& arg2) -> bool {
      return strcasecmp(arg1.c_str(), arg2.c_str()) == 0;
    });
}

static bool lua_continent(Logr::log_t slog, const combovar_t& continent)
{
  string res=getGeo(slog, s_lua_record_ctx->bestwho.toString(), GeoIPInterface::Continent);
  return doCompare(continent, res, [](const std::string& arg1, const std::string& arg2) -> bool {
      return strcasecmp(arg1.c_str(), arg2.c_str()) == 0;
    });
}

static string lua_continentCode(Logr::log_t slog)
{
  string unknown("unknown");
  string res = getGeo(slog, s_lua_record_ctx->bestwho.toString(), GeoIPInterface::Continent);
  if ( res == unknown ) {
   return {"--"};
  }
  return res;
}

static bool lua_country(Logr::log_t slog, const combovar_t& var)
{
  string res = getGeo(slog, s_lua_record_ctx->bestwho.toString(), GeoIPInterface::Country2);
  return doCompare(var, res, [](const std::string& arg1, const std::string& arg2) -> bool {
      return strcasecmp(arg1.c_str(), arg2.c_str()) == 0;
    });

}

static string lua_countryCode(Logr::log_t slog)
{
  string unknown("unknown");
  string res = getGeo(slog, s_lua_record_ctx->bestwho.toString(), GeoIPInterface::Country2);
  if (res == unknown) {
   return {"--"};
  }
  return res;
}

static bool lua_region(Logr::log_t slog, const combovar_t& var)
{
  string res = getGeo(slog, s_lua_record_ctx->bestwho.toString(), GeoIPInterface::Region);
  return doCompare(var, res, [](const std::string& arg1, const std::string& arg2) -> bool {
      return strcasecmp(arg1.c_str(), arg2.c_str()) == 0;
    });

}

static string lua_regionCode(Logr::log_t slog)
{
  string unknown("unknown");
  string res = getGeo(slog, s_lua_record_ctx->bestwho.toString(), GeoIPInterface::Region);
  if ( res == unknown ) {
   return {"--"};
  }
  return res;
}

static bool lua_netmask(const iplist_t& ips)
{
  for (const auto& addr : ips) {
    Netmask netmask(addr.second);
    if (netmask.match(s_lua_record_ctx->bestwho)) {
      return true;
    }
  }
  return false;
}

/* {
     {
      {'192.168.0.0/16', '10.0.0.0/8'},
      {'192.168.20.20', '192.168.20.21'}
     },
     {
      {'0.0.0.0/0'}, {'192.0.2.1'}
     }
   }
*/
static string lua_view(const vector<pair<int, vector<pair<int, iplist_t> > > >& pairs)
{
  for(const auto& rule : pairs) {
    const auto& netmasks=rule.second[0].second;
    const auto& destinations=rule.second[1].second;
    for(const auto& nmpair : netmasks) {
      Netmask netmask(nmpair.second);
      if (netmask.match(s_lua_record_ctx->bestwho)) {
        if (destinations.empty()) {
          throw std::invalid_argument("The IP list cannot be empty (for netmask " + netmask.toString() + ")");
        }
        return destinations[dns_random(destinations.size())].second;
      }
    }
  }
  return {};
}

static vector<string> lua_all(const vector< pair<int,string> >& ips)
{
  vector<string> result;
  result.reserve(ips.size());

  for (const auto& address : ips) {
      result.emplace_back(address.second);
  }
  if(result.empty()) {
    throw std::invalid_argument("The IP list cannot be empty");
  }
  return result;
}

static vector<string> lua_dblookup(Logr::log_t slog, const string& record, uint16_t qtype)
{
  ZoneName rec;
  vector<string> ret;
  try {
    rec = ZoneName(record);
  }
  catch (const std::exception& e) {
    SLOG(g_log << Logger::Error << "DB lookup cannot be performed, the name (" << record << ") is malformed: " << e.what() << endl,
         slog->error(Logr::Error, e.what(), "DB lookup cannot be performed", "record", Logging::Loggable(record)));
    return ret;
  }
  try {
    SOAData soaData;

    if (!getAuth(rec, qtype, &soaData, s_lua_record_ctx->remote)) {
      return ret;
    }

    vector<DNSZoneRecord> drs = lookup(rec.operator const DNSName&(), qtype, soaData.domain_id);
    for (const auto& drec : drs) {
      ret.push_back(drec.dr.getContent()->getZoneRepresentation());
    }
  }
  catch (std::exception& e) {
    SLOG(g_log << Logger::Error << "Failed to do DB lookup for " << rec << "/" << qtype << ": " << e.what() << endl,
         slog->error(Logr::Error, e.what(), "DB lookup failed", "record", Logging::Loggable(rec), "type", Logging::Loggable(qtype)));
  }
  return ret;
}

static void lua_include(Logr::log_t slog, LuaContext& lua, const string& record)
{
  DNSName rec;
  try {
    rec = DNSName(record) + s_lua_record_ctx->zone;
  } catch (const std::exception &e){
    SLOG(g_log<<Logger::Error<<"Included record cannot be loaded, the name ("<<record<<") is malformed: "<<e.what()<<endl,
         slog->error(Logr::Error, e.what(), "included record cannot be loaded", "record", Logging::Loggable(record)));
    return;
  }
  try {
    vector<DNSZoneRecord> drs = lookup(rec, QType::LUA, s_lua_record_ctx->zone_record.domain_id);
    for(const auto& zonerecord : drs) {
      auto luarecord = getRR<LUARecordContent>(zonerecord.dr);
      lua.executeCode(luarecord->getCode());
    }
  }
  catch(std::exception& e) {
    SLOG(g_log<<Logger::Error<<"Failed to load include record for Lua record "<<rec<<": "<<e.what()<<endl,
         slog->error(Logr::Error, e.what(), "Failed to load included record", "record", Logging::Loggable(rec)));
  }
}

// Lua variables available to the user

static std::unordered_map<std::string, int> lua_variables{
  {"ASn", GeoIPInterface::GeoIPQueryAttribute::ASn},
  {"City", GeoIPInterface::GeoIPQueryAttribute::City},
  {"Continent", GeoIPInterface::GeoIPQueryAttribute::Continent},
  {"Country", GeoIPInterface::GeoIPQueryAttribute::Country},
  {"Country2", GeoIPInterface::GeoIPQueryAttribute::Country2},
  {"Name", GeoIPInterface::GeoIPQueryAttribute::Name},
  {"Region", GeoIPInterface::GeoIPQueryAttribute::Region},
  {"Location", GeoIPInterface::GeoIPQueryAttribute::Location}
};

static void setupLuaRecords(Logr::log_t slog, LuaContext& lua)
{
  lua.writeFunction("report", [](const string& event, const boost::optional<string>& line) -> void {
      lua_report(event, line);
    });

  lua.writeFunction("latlon", [slog]() -> string {
      return lua_latlon(slog);
    });
  lua.writeFunction("latlonloc", [slog]() -> string {
      return lua_latlonloc(slog);
    });
  lua.writeFunction("closestMagic", [slog]() -> string {
      return lua_closestMagic(slog);
    });
  lua.writeFunction("latlonMagic", [slog]()-> string {
      return lua_latlonMagic(slog);
    });

  lua.writeFunction("createForward", []() -> string {
      return lua_createForward();
    });
  lua.writeFunction("createForward6", []() -> string {
      return lua_createForward6();
    });

  lua.writeFunction("createReverse", [slog](const string &format, boost::optional<opts_t> exceptions) -> string {
      return lua_createReverse(slog, format, std::move(exceptions));
    });
  lua.writeFunction("createReverse6", [slog](const string &format, boost::optional<opts_t> exceptions) -> string {
      return lua_createReverse6(slog, format, std::move(exceptions));
    });

  lua.writeFunction("filterForward", [](const string& address, NetmaskGroup& nmg, boost::optional<string> fallback) -> vector<string> {
      return lua_filterForward(address, nmg, std::move(fallback));
    });

  lua.writeFunction("ifportup", [slog](int port, const boost::variant<iplist_t, ipunitlist_t>& ips, boost::optional<opts_t> options) -> vector<string> {
      return lua_ifportup(slog, port, ips, std::move(options));
    });

  lua.writeFunction("ifurlextup", [slog](const vector<pair<int, opts_t> >& ipurls, boost::optional<opts_t> options) -> vector<string> {
      return lua_ifurlextup(slog, ipurls, std::move(options));
    });

  lua.writeFunction("ifurlup", [slog](const std::string& url, const boost::variant<iplist_t, ipunitlist_t>& ips, boost::optional<opts_t> options) -> vector<string> {
      return lua_ifurlup(slog, url, ips, std::move(options));
    });

  lua.writeFunction("pickrandom", [](const iplist_t& ips) -> string {
      return lua_pickrandom(ips);
    });

  lua.writeFunction("pickselfweighted", [slog](const std::string& url, const iplist_t& ips, boost::optional<opts_t> options) -> string {
      return lua_pickselfweighted(slog, url, ips, std::move(options));
    });

  lua.writeFunction("pickrandomsample", [](int n, const iplist_t& ips) -> vector<string> {
      return lua_pickrandomsample(n, ips);
    });

  lua.writeFunction("pickhashed", [](const iplist_t& ips) -> string {
      return lua_pickhashed(ips);
    });
  lua.writeFunction("pickwrandom", [](const std::unordered_map<int, wiplist_t>& ips) -> string {
      return lua_pickwrandom(ips);
    });

  lua.writeFunction("pickwhashed", [](std::unordered_map<int, wiplist_t> ips) -> string {
      return lua_pickwhashed(std::move(ips));
    });

  lua.writeFunction("picknamehashed", [](std::unordered_map<int, wiplist_t> ips) -> string {
      return lua_picknamehashed(std::move(ips));
    });
  lua.writeFunction("pickchashed", [](const std::unordered_map<int, wiplist_t>& ips) -> string {
      return lua_pickchashed(ips);
    });

  lua.writeFunction("pickclosest", [slog](const iplist_t& ips) -> string {
      return lua_pickclosest(slog, ips);
    });

  lua.writeFunction("geoiplookup", [slog](const string &address, const GeoIPInterface::GeoIPQueryAttribute attr) -> string {
      return lua_geoiplookup(slog, address, attr);
    });

  lua.writeFunction("asnum", [slog](const combovar_t& asns) -> bool {
      return lua_asnum(slog, asns);
    });
  lua.writeFunction("continent", [slog](const combovar_t& continent) -> bool {
      return lua_continent(slog, continent);
    });
  lua.writeFunction("continentCode", [slog]() -> string {
      return lua_continentCode(slog);
    });
  lua.writeFunction("country", [slog](const combovar_t& var) -> bool {
      return lua_country(slog, var);
    });
  lua.writeFunction("countryCode", [slog]() -> string {
      return lua_countryCode(slog);
    });
  lua.writeFunction("region", [slog](const combovar_t& var) -> bool {
      return lua_region(slog, var);
    });
  lua.writeFunction("regionCode", [slog]() -> string {
      return lua_regionCode(slog);
    });
  lua.writeFunction("netmask", [](const iplist_t& ips) -> bool {
      return lua_netmask(ips);
    });
  lua.writeFunction("view", [](const vector<pair<int, vector<pair<int, iplist_t> > > >& pairs) -> string {
      return lua_view(pairs);
    });

  lua.writeFunction("all", [](const vector< pair<int,string> >& ips) -> vector<string> {
      return lua_all(ips);
    });

  lua.writeFunction("dblookup", [slog](const string& record, uint16_t qtype) -> vector<string> {
      return lua_dblookup(slog, record, qtype);
    });

  lua.writeFunction("include", [slog, &lua](const string& record) -> void {
      lua_include(slog, lua, record);
    });

  lua.writeVariable("GeoIPQueryAttribute", lua_variables);
}

std::vector<shared_ptr<DNSRecordContent>> luaSynth(Logr::log_t slog, const std::string& code, const DNSName& query, const DNSZoneRecord& zone_record, const DNSName& zone, const DNSPacket& dnsp, uint16_t qtype, unique_ptr<AuthLua4>& LUA)
{
  std::vector<shared_ptr<DNSRecordContent>> ret;

  try {
    if(!LUA ||                  // we don't have a Lua state yet
       !g_LuaRecordSharedState) { // or we want a new one even if we had one
      LUA = make_unique<AuthLua4>(::arg()["lua-global-include-dir"]);
      setupLuaRecords(slog, *LUA->getLua());
    }

    LuaContext& lua = *LUA->getLua();

    s_lua_record_ctx = std::make_unique<lua_record_ctx_t>();
    s_lua_record_ctx->qname = query;
    s_lua_record_ctx->zone_record = zone_record;
    s_lua_record_ctx->zone = zone;
    s_lua_record_ctx->remote = dnsp.getRealRemote();

    lua.writeVariable("qname", query);
    lua.writeVariable("zone", zone);
    lua.writeVariable("zoneid", zone_record.domain_id);
    lua.writeVariable("who", dnsp.getInnerRemote());
    lua.writeVariable("localwho", dnsp.getLocal());
    lua.writeVariable("dh", static_cast<const dnsheader*>(&dnsp.d));
    lua.writeVariable("dnssecOK", dnsp.d_dnssecOk);
    lua.writeVariable("tcp", dnsp.d_tcp);
    lua.writeVariable("ednsPKTSize", dnsp.d_ednsRawPacketSizeLimit);
    if(dnsp.hasEDNSSubnet()) {
      lua.writeVariable("ecswho", dnsp.getRealRemote());
      s_lua_record_ctx->bestwho = dnsp.getRealRemote().getNetwork();
    }
    else {
      lua.writeVariable("ecswho", nullptr);
      s_lua_record_ctx->bestwho = dnsp.getInnerRemote();
    }
    lua.writeVariable("bestwho", s_lua_record_ctx->bestwho);

    if (g_luaRecordExecLimit > 0) {
      lua.executeCode(boost::str(boost::format("debug.sethook(report, '', %d)") % g_luaRecordExecLimit));
    }

    string actual;
    if(!code.empty() && code[0]!=';')
      actual = "return " + code;
    else
      actual = code.substr(1);

    auto content=lua.executeCode<boost::variant<string, vector<pair<int, string> > > >(actual);

    vector<string> contents;
    if(auto str = boost::get<string>(&content))
      contents.push_back(*str);
    else
      for(const auto& c : boost::get<vector<pair<int,string>>>(content))
        contents.push_back(c.second);

    for(const auto& content_it: contents) {
      if(qtype==QType::TXT)
        ret.push_back(DNSRecordContent::make(qtype, QClass::IN, '"' + content_it + '"'));
      else
        ret.push_back(DNSRecordContent::make(qtype, QClass::IN, content_it));
    }
  } catch(std::exception &e) {
    if (!g_slogStructured) {
      g_log << Logger::Info << "Lua record ("<<query<<"|"<<QType(qtype).toString()<<") reported: " << e.what();
    }
    try {
      std::rethrow_if_nested(e);
      if (!g_slogStructured) {
        g_log<<endl;
      }
      else {
        slog->info(Logr::Info, "exception raised by Lua record", "query", Logging::Loggable(query), "type", Logging::Loggable(qtype));
      }
    } catch(const std::exception& ne) {
      SLOG(g_log << ": " << ne.what() << std::endl,
           slog->error(Logr::Info, ne.what(), "exception raised by Lua record", "query", Logging::Loggable(query), "type", Logging::Loggable(qtype)));
    }
    catch(const PDNSException& ne) {
      SLOG(g_log << ": " << ne.reason << std::endl,
           slog->error(Logr::Info, ne.reason, "exception raised by Lua record", "query", Logging::Loggable(query), "type", Logging::Loggable(qtype)));
    }
    throw ;
  }

  return ret;
}
