/** @file

  A brief file description

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#pragma once

#include <cstdint>
#include <cstring>

#include "iocore/dns/DNSProcessor.h"
#include "P_DNSConnection.h"

#include "iocore/eventsystem/Action.h"
#include "iocore/eventsystem/Continuation.h"
#include "iocore/eventsystem/EThread.h"
#include "iocore/eventsystem/Event.h"

#include "tscore/ink_apidefs.h"
#include "tscore/ink_hrtime.h"
#include "tscore/ink_inet.h"
#include "tscore/ink_platform.h"
#include "tscore/ink_rand.h"
#include "tscore/ink_resolver.h"
#include "tscore/List.h"
#include "tscore/PendingAction.h"
#include "tscore/Ptr.h"

#include "tsutil/DbgCtl.h"

#include <swoc/IPEndpoint.h>

#include "tsutil/Metrics.h"

using ts::Metrics;

#define MAX_NAMED                       32
#define DEFAULT_DNS_RETRIES             5
#define MAX_DNS_RETRIES                 9
#define DEFAULT_DNS_TIMEOUT             30
#define MAX_DNS_IN_FLIGHT               2048
#define MAX_DNS_TCP_CONTINUOUS_FAILURES 10
#define DEFAULT_FAILOVER_NUMBER         (DEFAULT_DNS_RETRIES + 1)
#define DEFAULT_FAILOVER_PERIOD         (DEFAULT_DNS_TIMEOUT + 30)
// how many seconds before FAILOVER_PERIOD to try the primary with
// a well known address
#define DEFAULT_FAILOVER_TRY_PERIOD (DEFAULT_DNS_TIMEOUT + 1)
#define DEFAULT_DNS_SEARCH          1
#define FAILOVER_SOON_RETRY         5
#define NO_NAMESERVER_SELECTED      -1

//
// Config
//
extern int          dns_timeout;
extern int          dns_retries;
extern int          dns_search;
extern int          dns_failover_number;
extern int          dns_failover_period;
extern int          dns_failover_try_period;
extern int          dns_max_dns_in_flight;
extern int          dns_max_tcp_continuous_failures;
extern unsigned int dns_sequence_number;

//
// Constants
//

#define DNS_PERIOD                         HRTIME_MSECONDS(100)
#define DNS_DELAY_PERIOD                   HRTIME_MSECONDS(10)
#define DNS_SEQUENCE_NUMBER_RESTART_OFFSET 4000
#define DNS_PRIMARY_RETRY_PERIOD           HRTIME_SECONDS(5)
#define DNS_PRIMARY_REOPEN_PERIOD          HRTIME_SECONDS(60)
#define BAD_DNS_RESULT                     (reinterpret_cast<HostEnt *>((uintptr_t) - 1))
#define DEFAULT_NUM_TRY_SERVER             8

// these are from nameser.h
#ifndef HFIXEDSZ
#define HFIXEDSZ 12
#endif
#ifndef QFIXEDSZ
#define QFIXEDSZ 4
#endif

// Stats
struct DNSStatsBlock {
  Metrics::Counter::AtomicType *fail_time;
  Metrics::Gauge::AtomicType   *in_flight;
  Metrics::Counter::AtomicType *lookup_fail;
  Metrics::Counter::AtomicType *lookup_success;
  Metrics::Counter::AtomicType *max_retries_exceeded;
  Metrics::Counter::AtomicType *response_time;
  Metrics::Counter::AtomicType *retries;
  Metrics::Counter::AtomicType *success_time;
  Metrics::Counter::AtomicType *tcp_reset;
  Metrics::Counter::AtomicType *tcp_retries;
  Metrics::Counter::AtomicType *total_lookups;
};

struct HostEnt;
struct DNSHandler;

extern DNSStatsBlock dns_rsb;

/**
  One DNSEntry is allocated per outstanding request. This continuation
  handles TIMEOUT events for the request as well as storing all
  information about the request and its status.

*/
struct DNSEntry : public Continuation {
  int          id[MAX_DNS_RETRIES];
  int          qtype          = 0;             ///< Type of query to send.
  HostResStyle host_res_style = HOST_RES_NONE; ///< Preferred IP address family.
  int          retries        = DEFAULT_DNS_RETRIES;
  int          which_ns       = NO_NAMESERVER_SELECTED;
  ink_hrtime   submit_time    = 0;
  ink_hrtime   send_time      = 0;
  char         qname[MAXDNAME + 1];
  int          qname_len      = 0;
  int          orig_qname_len = 0;
  char       **domains        = nullptr;
  EThread     *submit_thread  = nullptr;
  Action       action;
  Event       *timeout = nullptr;
  Ptr<HostEnt> result_ent;
  DNSHandler  *dnsH              = nullptr;
  bool         written_flag      = false;
  bool         once_written_flag = false;
  bool         last              = false;
  LINK(DNSEntry, dup_link);
  Que(DNSEntry, dup_link) dups;

  int  mainEvent(int event, Event *e);
  int  delayEvent(int event, Event *e);
  int  postAllEvent(int event, Event *e);
  int  post(DNSHandler *h, HostEnt *ent);
  int  postOneEvent(int event, Event *e);
  void init(DNSQueryData target, int qtype_arg, Continuation *acont, DNSProcessor::Options const &opt);

  DNSEntry()
  {
    for (int &i : id)
      i = -1;
    memset(qname, 0, MAXDNAME);
  }
};

using DNSEntryHandler = int (DNSEntry::*)(int, void *);

struct DNSEntry;

/**
  One DNSHandler is allocated to handle all DNS traffic by polling a
  UDP port.

*/
struct DNSHandler : public Continuation {
  /// This is used as the target if round robin isn't set.
  IpEndpoint           ip;
  IpEndpoint           local_ipv6; ///< Local V6 address if set.
  IpEndpoint           local_ipv4; ///< Local V4 address if set.
  int                  ifd[MAX_NAMED];
  int                  n_con = 0;
  DNSConnection        tcpcon[MAX_NAMED];
  DNSConnection        udpcon[MAX_NAMED];
  Queue<DNSEntry>      entries;
  Queue<DNSConnection> triggered;
  int                  in_flight    = 0;
  int                  name_server  = 0;
  int                  in_write_dns = 0;

  HostEnt *hostent_cache = nullptr;

  int        ns_down[MAX_NAMED];
  int        failover_number[MAX_NAMED];
  int        failover_soon_number[MAX_NAMED];
  int        tcp_continuous_failures[MAX_NAMED];
  ink_hrtime crossed_failover_number[MAX_NAMED];
  ink_hrtime last_primary_retry  = 0;
  ink_hrtime last_primary_reopen = 0;

  ink_res_state m_res              = nullptr;
  int           txn_lookup_timeout = 0;

  InkRand generator;
  // bitmap of query ids in use
  uint64_t qid_in_flight[(USHRT_MAX + 1) / 64];

  void
  received_one(int i)
  {
    failover_number[i] = failover_soon_number[i] = crossed_failover_number[i] = 0;
  }

  void
  sent_one()
  {
    ++failover_number[name_server];
    Dbg(_dbg_ctl_dns, "sent_one: failover_number for resolver %d is %d", name_server, failover_number[name_server]);
    if (failover_number[name_server] >= dns_failover_number && !crossed_failover_number[name_server])
      crossed_failover_number[name_server] = ink_get_hrtime();
  }

  bool
  failover_now(int i)
  {
    if (_dbg_ctl_dns.on()) {
      DbgPrint(_dbg_ctl_dns, "failover_now: Considering immediate failover, target time is %" PRId64 "",
               (ink_hrtime)HRTIME_SECONDS(dns_failover_period));
      DbgPrint(_dbg_ctl_dns, "\tdelta time is %" PRId64 "", (ink_get_hrtime() - crossed_failover_number[i]));
    }
    return ns_down[i] ||
           (crossed_failover_number[i] && ((ink_get_hrtime() - crossed_failover_number[i]) > HRTIME_SECONDS(dns_failover_period)));
  }

  bool
  failover_soon(int i)
  {
    return ns_down[i] || (crossed_failover_number[i] &&
                          ((ink_get_hrtime() - crossed_failover_number[i]) >
                           (HRTIME_SECONDS(dns_failover_try_period + failover_soon_number[i] * FAILOVER_SOON_RETRY))));
  }

  void recv_dns(int event, Event *e);
  int  startEvent(int event, Event *e);
  int  startEvent_sdns(int event, Event *e);
  int  mainEvent(int event, Event *e);

  void     open_cons(sockaddr const *addr, bool failed = false, int icon = 0);
  bool     open_con(sockaddr const *addr, bool failed = false, int icon = 0, bool over_tcp = false);
  void     failover();
  void     rr_failure(int ndx);
  void     recover();
  void     retry_named(int ndx, ink_hrtime t, bool reopen = true);
  void     try_primary_named(bool reopen = true);
  void     switch_named(int ndx);
  uint16_t get_query_id();

  void
  release_query_id(uint16_t qid)
  {
    qid_in_flight[qid >> 6] &= (uint64_t) ~(0x1ULL << (qid & 0x3F));
  };

  void
  set_query_id_in_use(uint16_t qid)
  {
    qid_in_flight[qid >> 6] |= (uint64_t)(0x1ULL << (qid & 0x3F));
  };

  bool
  query_id_in_use(uint16_t qid)
  {
    return (qid_in_flight[(uint16_t)(qid) >> 6] & (uint64_t)(0x1ULL << ((uint16_t)(qid) & 0x3F))) != 0;
  };

  DNSHandler();

private:
  // Check the IP address and switch to default if needed.
  void validate_ip();
  // Check tcp connection for TCP_RETRY mode
  void check_and_reset_tcp_conn();
  bool reset_tcp_conn(int ndx);

  /** The event used for the periodic retry of connectivity to any down name
   * servers. */
  PendingAction _dns_retry_event;

private:
  inline static DbgCtl _dbg_ctl_dns{"dns"}, _dbg_ctl_net_epoll{"net_epoll"};
};

/* --------------------------------------------------------------
   **                struct DNSServer

   A record for an single server
   -------------------------------------------------------------- */
struct DNSServer {
  IpEndpoint x_server_ip[MAXNS];
  char       x_dns_ip_line[MAXDNAME * 2];

  char x_def_domain[MAXDNAME];
  char x_domain_srch_list[MAXDNAME];

  DNSHandler *x_dnsH = nullptr;

  DNSServer()
  {
    memset(x_server_ip, 0, sizeof(x_server_ip));

    memset(x_def_domain, 0, MAXDNAME);
    memset(x_domain_srch_list, 0, MAXDNAME);
    memset(x_dns_ip_line, 0, MAXDNAME * 2);
  }
};

TS_INLINE
DNSHandler::DNSHandler()
  : Continuation(nullptr),

    generator((uint32_t)((uintptr_t)time(nullptr) ^ (uintptr_t)this))
{
  ats_ip_invalidate(&ip);
  for (int i = 0; i < MAX_NAMED; i++) {
    ifd[i]                     = -1;
    failover_number[i]         = 0;
    failover_soon_number[i]    = 0;
    crossed_failover_number[i] = 0;
    tcp_continuous_failures[i] = 0;
    ns_down[i]                 = 1;
    tcpcon[i].handler          = this;
    udpcon[i].handler          = this;
  }
  memset(&qid_in_flight, 0, sizeof(qid_in_flight));
  SET_HANDLER(&DNSHandler::startEvent);
  Dbg(_dbg_ctl_net_epoll, "inline DNSHandler::DNSHandler()");
}
