// -*- mode: cpp; mode: fold -*-
// Description								/*{{{*/
/* ######################################################################

   SRV record support

   ##################################################################### */
									/*}}}*/
#include <config.h>

#include <netdb.h>

#include <arpa/nameser.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <resolv.h>
#include <time.h>

#include <algorithm>
#include <memory>
#include <tuple>

#include <apt-pkg/configuration.h>
#include <apt-pkg/error.h>
#include <apt-pkg/strutl.h>

#include "srvrec.h"

bool SrvRec::operator==(SrvRec const &other) const
{
   return (std::tie(target, priority, weight, port) ==
           std::tie(other.target, other.priority, other.weight, other.port));
}

bool GetSrvRecords(std::string host, int port, std::vector<SrvRec> &Result)
{
   // try SRV only for hostnames, not for IP addresses
   {
      struct in_addr addr4;
      struct in6_addr addr6;
      if (inet_pton(AF_INET, host.c_str(), &addr4) == 1 ||
	  inet_pton(AF_INET6, host.c_str(), &addr6) == 1)
	 return true;
   }

   std::string target;
   int res;
   struct servent s_ent_buf;
   struct servent *s_ent = nullptr;
   std::vector<char> buf(1024);

#ifdef SOLARIS_GETSERVBYPORT_R
   s_ent = getservbyport_r(htons(port), "tcp", &s_ent_buf, buf.data(), buf.size());
   if (s_ent == nullptr)
      return false;
#else
   res = getservbyport_r(htons(port), "tcp", &s_ent_buf, buf.data(), buf.size(), &s_ent);
   if (res != 0 || s_ent == nullptr)
      return false;
#endif
   strprintf(target, "_%s._tcp.%s", s_ent->s_name, host.c_str());
   return GetSrvRecords(target, Result);
}

bool GetSrvRecords(std::string name, std::vector<SrvRec> &Result)
{
   unsigned char answer[PACKETSZ];
   int answer_len, compressed_name_len;
   int answer_count;
   struct __res_state res;

   if (res_ninit(&res) != 0)
      return _error->Errno("res_init", "Failed to init resolver");

   // Close on return
   std::shared_ptr<void> guard(&res, res_nclose);

   answer_len = res_nquery(&res, name.c_str(), C_IN, T_SRV, answer, sizeof(answer));
   if (answer_len == -1)
      return false;
   if (answer_len < (int)sizeof(HEADER))
      return _error->Warning("Not enough data from res_query (%i)", answer_len);

   // check the header
   HEADER *header = (HEADER*)answer;
   if (header->rcode != NOERROR)
      return _error->Warning("res_query returned rcode %i", header->rcode);
   answer_count = ntohs(header->ancount);
   if (answer_count <= 0)
      return _error->Warning("res_query returned no answers (%i) ", answer_count);

   // skip the header
   compressed_name_len = dn_skipname(answer+sizeof(HEADER), answer+answer_len);
   if(compressed_name_len < 0)
      return _error->Warning("dn_skipname failed %i", compressed_name_len);

   // pt points to the first answer record, go over all of them now
   unsigned char *pt = answer+sizeof(HEADER)+compressed_name_len+QFIXEDSZ;
   while ((int)Result.size() < answer_count && pt < answer+answer_len)
   {
      u_int16_t type, klass, priority, weight, port, dlen;
      char buf[MAXDNAME];

      compressed_name_len = dn_skipname(pt, answer+answer_len);
      if (compressed_name_len < 0)
         return _error->Warning("dn_skipname failed (2): %i",
                                compressed_name_len);
      pt += compressed_name_len;
      if (((answer+answer_len) - pt) < 16)
         return _error->Warning("packet too short");

      // extract the data out of the result buffer
      #define extract_u16(target, p) target = *p++ << 8; target |= *p++;

      extract_u16(type, pt);
      if(type != T_SRV)
         return _error->Warning("Unexpected type excepted %x != %x",
                                T_SRV, type);
      extract_u16(klass, pt);
      if(klass != C_IN)
         return _error->Warning("Unexpected class excepted %x != %x",
                                C_IN, klass);
      pt += 4;  // ttl
      extract_u16(dlen, pt);
      extract_u16(priority, pt);
      extract_u16(weight, pt);
      extract_u16(port, pt);

      #undef extract_u16

      compressed_name_len = dn_expand(answer, answer+answer_len, pt, buf, sizeof(buf));
      if(compressed_name_len < 0)
         return _error->Warning("dn_expand failed %i", compressed_name_len);
      pt += compressed_name_len;

      // add it to our class
      Result.emplace_back(buf, priority, weight, port);
   }

   // implement load balancing as specified in RFC-2782

   // sort them by priority
   std::stable_sort(Result.begin(), Result.end());

   for(std::vector<SrvRec>::iterator I = Result.begin();
      I != Result.end(); ++I)
   {
      if (_config->FindB("Debug::Acquire::SrvRecs", false) == true)
      {
         std::cerr << "SrvRecs: got " << I->target
                   << " prio: " << I->priority
                   << " weight: " << I->weight
                   << std::endl;
      }
   }

   return true;
}

SrvRec PopFromSrvRecs(std::vector<SrvRec> &Recs)
{
   // FIXME: instead of the simplistic shuffle below use the algorithm
   //        described in rfc2782 (with weights)
   //        and figure out how the weights need to be adjusted if
   //        a host refuses connections

#if 0  // all code below is only needed for the weight adjusted selection 
   // assign random number ranges
   int prev_weight = 0;
   int prev_priority = 0;
   for(std::vector<SrvRec>::iterator I = Result.begin();
      I != Result.end(); ++I)
   {
      if(prev_priority != I->priority)
         prev_weight = 0;
      I->random_number_range_start = prev_weight;
      I->random_number_range_end = prev_weight + I->weight;
      prev_weight = I->random_number_range_end;
      prev_priority = I->priority;

      if (_config->FindB("Debug::Acquire::SrvRecs", false) == true)
         std::cerr << "SrvRecs: got " << I->target
                   << " prio: " << I->priority
                   << " weight: " << I->weight
                   << std::endl;
   }

   // go over the code in reverse order and note the max random range
   int max = 0;
   prev_priority = 0;
   for(std::vector<SrvRec>::iterator I = Result.end();
      I != Result.begin(); --I)
   {
      if(prev_priority != I->priority)
         max = I->random_number_range_end;
      I->random_number_range_max = max;
   }
#endif

   // shuffle in a very simplistic way for now (equal weights)
   std::vector<SrvRec>::iterator I = Recs.begin();
   std::vector<SrvRec>::iterator const J = std::find_if(Recs.begin(), Recs.end(),
	 [&I](SrvRec const &J) { return I->priority != J.priority; });

   // clock seems random enough.
   I += std::max(static_cast<clock_t>(0), clock()) % std::distance(I, J);
   SrvRec const selected = std::move(*I);
   Recs.erase(I);

   if (_config->FindB("Debug::Acquire::SrvRecs", false) == true)
      std::cerr << "PopFromSrvRecs: selecting " << selected.target << std::endl;

   return selected;
}
