/****************************************************************************
 * apps/netutils/ntpclient/ntpclient.c
 *
 * 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.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <stdbool.h>
#include <stdint.h>

#include <sys/socket.h>
#include <sys/time.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sched.h>
#include <assert.h>
#include <errno.h>
#include <debug.h>

#include <netinet/in.h>

#ifdef CONFIG_LIBC_NETDB
#  include <netdb.h>
#  include <arpa/inet.h>
#endif

#include <nuttx/clock.h>

#include "netutils/ntpclient.h"

#include "ntpv3.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

/* Configuration ************************************************************/

#ifndef CONFIG_HAVE_LONG_LONG
#  error "64-bit integer support required for NTP client"
#endif

#if defined(CONFIG_LIBC_NETDB) && !defined(CONFIG_NETUTILS_NTPCLIENT_SERVER)
#  error "CONFIG_NETUTILS_NTPCLIENT_SERVER must be provided"
#endif

#if !defined(CONFIG_LIBC_NETDB) && !defined(CONFIG_NETUTILS_NTPCLIENT_SERVERIP)
#  error "CONFIG_NETUTILS_NTPCLIENT_SERVERIP must be provided"
#endif

#ifndef CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES
#  define CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES 5
#elif CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES < 1
#  error "NTP sample number below 1, invalid configuration"
#endif

#ifndef CONFIG_NETUTILS_NTPCLIENT_SERVER
#  ifdef CONFIG_NETUTILS_NTPCLIENT_SERVERIP
/* Old config support */
#    warning "NTP server hostname not defined, using deprecated server IP address setting"
#    define CONFIG_NETUTILS_NTPCLIENT_SERVER \
          inet_ntoa(CONFIG_NETUTILS_NTPCLIENT_SERVERIP)
#  else
#    error "NTP server hostname not defined"
#  endif
#endif

/* NTP Time is seconds since 1900. Convert to Unix time which is seconds
 * since 1970
 */

#define NTP2UNIX_TRANSLATION 2208988800u

#define NTP_VERSION_V3       3
#define NTP_VERSION_V4       4
#define NTP_VERSION          NTP_VERSION_V4

#define MAX_SERVER_SELECTION_RETRIES 3

#ifndef ARRAY_SIZE
#  define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#endif

#ifndef STR
#  define STR2(x) #x
#  define STR(x) STR2(x)
#endif

/****************************************************************************
 * Private Types
 ****************************************************************************/

/* This enumeration describes the state of the NTP daemon */

enum ntpc_daemon_e
{
  NTP_NOT_RUNNING = 0,
  NTP_STARTED,
  NTP_RUNNING,
  NTP_STOP_REQUESTED,
  NTP_STOPPED
};

/* This type describes the state of the NTP client daemon.  Only one
 * instance of the NTP daemon is permitted in this implementation.
 */

struct ntpc_daemon_s
{
  uint8_t state;       /* See enum ntpc_daemon_e */
  sem_t lock;          /* Used to protect the whole structure */
  sem_t sync;          /* Used to synchronize start and stop events */
  pid_t pid;           /* Task ID of the NTP daemon */
  sq_queue_t kod_list; /* KoD excluded server addresses */
  int family;          /* Allowed address family */
};

union ntp_addr_u
{
  struct sockaddr sa;
#ifdef CONFIG_NET_IPv4
  struct sockaddr_in in4;
#endif
#ifdef CONFIG_NET_IPv6
  struct sockaddr_in6 in6;
#endif
  struct sockaddr_storage ss;
};

/* NTP offset. */

struct ntp_sample_s
{
  int64_t offset;
  int64_t delay;
  union ntp_addr_u srv_addr;
} packet_struct;

/* Server address list. */

struct ntp_servers_s
{
  union ntp_addr_u list[CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES];
  size_t num;
  size_t pos;
  FAR char *hostlist_str;
  FAR char *hostlist_saveptr;
  FAR char *hostnext;
  FAR const char *ntp_servers;
};

/* KoD exclusion list. */

struct ntp_kod_exclude_s
{
  sq_entry_t node;
  union ntp_addr_u addr;
};

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* This type describes the state of the NTP client daemon.  Only one
 * instance of the NTP daemon is permitted in this implementation.  This
 * limitation is due only to this global data structure.
 */

static struct ntpc_daemon_s g_ntpc_daemon =
{
  NTP_NOT_RUNNING,
  SEM_INITIALIZER(1),
  SEM_INITIALIZER(0),
  -1,
  { NULL, NULL },
  AF_UNSPEC,         /* Default is both IPv4 and IPv6 */
};

static struct ntp_sample_s g_last_samples
    [CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES];
unsigned int g_last_nsamples = 0;

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: sample_cmp
 ****************************************************************************/

static int sample_cmp(FAR const void *_a, FAR const void *_b)
{
  FAR const struct ntp_sample_s *a = _a;
  FAR const struct ntp_sample_s *b = _b;
  int64_t diff = a->offset - b->offset;

  if (diff < 0)
    {
      return -1;
    }
  else if (diff > 0)
    {
      return 1;
    }
  else
    {
      return 0;
    }
}

/****************************************************************************
 * Name: int64abs
 ****************************************************************************/

static inline int64_t int64abs(int64_t value)
{
  return value >= 0 ? value : -value;
}

/****************************************************************************
 * Name: ntpc_get_compile_timestamp
 ****************************************************************************/

static time_t ntpc_get_compile_timestamp(void)
{
  struct tm tm;
  int year;
  int day;
  int month;
  bool unknown = true;
  time_t tim;
#ifdef __DATE__
  const char *pmonth;
  const char *pyear;
  const char *pday;

  /* Compile date. Format: "MMM DD YYYY", where MMM is month in three letter
   * format, DD is day of month (left padded with space if less than ten) and
   * YYYY is year. "??? ?? ????" if unknown.
   */

  pmonth = __DATE__;
  pday = pmonth + 4;
  pyear = pmonth + 7;

  year = (pyear[0] - '0') * 1000
         + (pyear[1] - '0') * 100
         + (pyear[2] - '0') * 10
         + (pyear[3] - '0') * 1;

  day = (pday[1] - '0');
  if (pday[0] != ' ')
    {
      day += (pday[0] - '0') * 10;
    }

  unknown = false;
  switch (pmonth[0])
    {
    default:
      unknown = true;
      break;
    case 'J':
      if (pmonth[1] == 'a') /* Jan */
        month = 1;
      else if (pmonth[2] == 'n') /* Jun */
        month = 6;
      else /* Jul */
        month = 7;
      break;
    case 'F': /* Feb */
      month = 2;
      break;
    case 'M':
      if (pmonth[2] == 'r') /* Mar */
        month = 3;
      else /* May */
        month = 5;
      break;
    case 'A':
      if (pmonth[1] == 'p') /* Apr */
        month = 4;
      else /* Aug */
        month = 8;
      break;
    case 'S': /* Sep */
      month = 9;
      break;
    case 'O': /* Oct */
      month = 10;
      break;
    case 'N': /* Nov */
      month = 11;
      break;
    case 'D': /* Dec */
      month = 12;
      break;
    }
#endif

  if (unknown)
    {
      month = 8;
      day = 18;
      year = 2015;
    }

  /* Convert date to timestamp. */

  memset(&tm, 0, sizeof(tm));
  tm.tm_hour = 0;
  tm.tm_min = 0;
  tm.tm_sec = 0;
  tm.tm_mday = day;
  tm.tm_mon = month - 1;
  tm.tm_year = year - 1900;

  tim = mktime(&tm);

  /* Reduce by one day to discount timezones. */

  tim -= 24 * 60 * 60;

  return tim;
}

/****************************************************************************
 * Name: ntpc_getuint32
 *
 * Description:
 *   Return the big-endian, 4-byte value in network (big-endian) order.
 *
 ****************************************************************************/

static inline uint32_t ntpc_getuint32(FAR const uint8_t *ptr)
{
  /* Network order is big-endian; host order is irrelevant */

  return (uint32_t)ptr[3] |          /* MS byte appears first in data stream */
         ((uint32_t)ptr[2] << 8) |
         ((uint32_t)ptr[1] << 16) |
         ((uint32_t)ptr[0] << 24);
}

/****************************************************************************
 * Name: ntpc_getuint64
 *
 * Description:
 *   Return the big-endian, 8-byte value in network (big-endian) order.
 *
 ****************************************************************************/

static inline uint64_t ntpc_getuint64(FAR const uint8_t *ptr)
{
  return ((uint64_t)ntpc_getuint32(ptr) << 32) | ntpc_getuint32(&ptr[4]);
}

/****************************************************************************
 * Name: ntpc_setuint32
 *
 * Description:
 *   Write 4-byte value to buffer in network (big-endian) order.
 *
 ****************************************************************************/

static inline void ntpc_setuint32(FAR uint8_t *ptr, uint32_t value)
{
  ptr[3] = (uint8_t)value;
  ptr[2] = (uint8_t)(value >> 8);
  ptr[1] = (uint8_t)(value >> 16);
  ptr[0] = (uint8_t)(value >> 24);
}

/****************************************************************************
 * Name: ntpc_setuint64
 *
 * Description:
 *   Write 8-byte value to buffer in network (big-endian) order.
 *
 ****************************************************************************/

static inline void ntpc_setuint64(FAR uint8_t *ptr, uint64_t value)
{
  ntpc_setuint32(ptr + 0, (uint32_t)(value >> 32));
  ntpc_setuint32(ptr + 4, (uint32_t)value);
}

/****************************************************************************
 * Name: ntp_secpart
 ****************************************************************************/

static uint32_t ntp_secpart(uint64_t time)
{
  /* NTP timestamps are represented as a 64-bit fixed-point number, in
   * seconds relative to 0000 UT on 1 January 1900.  The integer part is
   * in the first 32 bits and the fraction part in the last 32 bits, as
   * shown in the following diagram.
   *
   *    0                   1                   2                   3
   *    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                         Integer Part                          |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *   |                         Fraction Part                         |
   *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   */

  /* Get seconds part. */

  return time >> 32;
}

/****************************************************************************
 * Name: ntp_nsecpart
 ****************************************************************************/

static uint32_t ntp_nsecpart(uint64_t time)
{
  /* Get fraction part converted to nanoseconds. */

  return ((time & 0xffffffffu) * NSEC_PER_SEC) >> 32;
}

/****************************************************************************
 * Name: timespec2ntp
 *
 * Convert UNIX timespec timestamp to NTP 64-bit fixed point timestamp.
 *
 ****************************************************************************/

static uint64_t timespec2ntp(FAR const struct timespec *ts)
{
  uint64_t ntp_time;

  /* Set fraction part. */

  ntp_time = ((uint64_t)(ts->tv_nsec) << 32) / NSEC_PER_SEC;

  DEBUGASSERT((ntp_time >> 32) == 0);

  /* Set seconds part. */

  ntp_time += (uint64_t)(ts->tv_sec) << 32;

  return ntp_time;
}

/****************************************************************************
 * Name: ntp_gettime
 *
 * Get time in NTP epoch, stored in fixed point uint64_t format (upper 32-bit
 * seconds, lower 32-bit fraction)
 ****************************************************************************/

static uint64_t ntp_localtime(void)
{
  int err = errno;
  struct timespec currts;

  /* Get current clock in NTP epoch. */

  clock_gettime(CLOCK_REALTIME, &currts);
  currts.tv_sec += NTP2UNIX_TRANSLATION;

  /* Restore errno. */

  errno = err;

  return timespec2ntp(&currts);
}

/****************************************************************************
 * Name: ntpc_calculate_offset
 *
 * Description:
 *   Calculate NTP time offset and round-trip delay
 *
 ****************************************************************************/

static void ntpc_calculate_offset(FAR int64_t *offset, FAR int64_t *delay,
                                  uint64_t local_xmittime,
                                  uint64_t local_recvtime,
                                  FAR const uint8_t *remote_recv,
                                  FAR const uint8_t *remote_xmit)
{
  uint64_t remote_recvtime;
  uint64_t remote_xmittime;

  /* Two timestamps from server, when request was received, and response
   * send.
   */

  remote_recvtime = ntpc_getuint64(remote_recv);
  remote_xmittime = ntpc_getuint64(remote_xmit);

  /* Calculate offset of local time compared to remote time.
   * See: https://www.eecis.udel.edu/~mills/time.html
   *      http://nicolas.aimon.fr/2014/12/05/timesync/
   */

  *offset = (int64_t)((remote_recvtime / 2 - local_xmittime / 2) +
                     (remote_xmittime / 2 - local_recvtime / 2));

  /* Calculate roundtrip delay. */

  *delay = (local_recvtime - local_xmittime) -
          (remote_xmittime - remote_recvtime);
}

/****************************************************************************
 * Name: ntpc_settime
 *
 * Description:
 *   Given the NTP time offset, adjust the system time
 *
 ****************************************************************************/

static void ntpc_settime(int64_t offset, FAR struct timespec *start_realtime,
                         FAR struct timespec *start_monotonic)
{
  struct timespec tp;
  struct timespec curr_realtime;
  struct timespec curr_monotonic;
  int64_t diffms_real;
  int64_t diffms_mono;
  int64_t diff_diff_ms;

  /* Get the system times */

  clock_gettime(CLOCK_REALTIME, &curr_realtime);
  clock_gettime(CLOCK_MONOTONIC, &curr_monotonic);

  /* Check differences between monotonic and realtime. */

  diffms_real = curr_realtime.tv_sec - start_realtime->tv_sec;
  diffms_real *= 1000;
  diffms_real += (int64_t)(curr_realtime.tv_nsec -
                           start_realtime->tv_nsec) / (1000 * 1000);

  diffms_mono = curr_monotonic.tv_sec - start_monotonic->tv_sec;
  diffms_mono *= 1000;
  diffms_mono += (int64_t)(curr_monotonic.tv_nsec -
                           start_monotonic->tv_nsec) / (1000 * 1000);

  /* Detect if real-time has been altered by other task. */

  diff_diff_ms = diffms_real - diffms_mono;
  if (diff_diff_ms < 0)
    {
      diff_diff_ms = -diff_diff_ms;
    }

  if (diff_diff_ms >= 1000)
    {
      nwarn("System time altered by other task by %ju msecs, "
            "do not apply offset.\n", (intmax_t)diff_diff_ms);

      return;
    }

  /* Apply offset */

  tp = curr_realtime;
  tp.tv_sec  += ntp_secpart(offset);
  tp.tv_nsec += ntp_nsecpart(offset);
  while (tp.tv_nsec >= NSEC_PER_SEC)
    {
      tp.tv_nsec -= NSEC_PER_SEC;
      tp.tv_sec++;
    }

  /* Set the system time */

  clock_settime(CLOCK_REALTIME, &tp);

  ninfo("Set time to %ju.%03ld seconds (offset: %s%lu.%03lu).\n",
        (intmax_t)tp.tv_sec, tp.tv_nsec / NSEC_PER_MSEC,
        offset < 0 ? "-" : "",
        (unsigned long)ntp_secpart(int64abs(offset)),
        ntp_nsecpart(int64abs(offset)) / NSEC_PER_MSEC);
}

/****************************************************************************
 * Name: ntp_address_in_kod_list
 *
 * Description: Check if address is in KoD KoD exclusion list.
 *
 ****************************************************************************/

static bool ntp_address_in_kod_list(FAR const union ntp_addr_u *server_addr)
{
  FAR struct ntp_kod_exclude_s *entry;

  entry = (FAR void *)sq_peek(&g_ntpc_daemon.kod_list);
  while (entry)
    {
      if (memcmp(&entry->addr, server_addr, sizeof(*server_addr)) == 0)
        {
          return true;
        }

      entry = (FAR void *)sq_next(&entry->node);
    }

  return false;
}

/****************************************************************************
 * Name: ntp_is_kiss_of_death
 *
 * Description: Check if this is KoD response from the server. If it is,
 * add server to KoD exclusion list and return 'true'.
 *
 ****************************************************************************/

static bool ntp_is_kiss_of_death(FAR const struct ntp_datagram_s *recv,
                                 FAR const union ntp_addr_u *server_addr)
{
  /* KoD only specified for v4. */

  if (GETVN(recv->lvm) != NTP_VERSION_V4)
    {
      if (recv->stratum == 0)
        {
          /* Stratum 0 is unspecified on v3, so ignore packet. */

          return true;
        }
      else
        {
          return false;
        }
    }

  /* KoD message if stratum == 0. */

  if (recv->stratum != 0)
    {
      return false;
    }

  /* KoD message received. */

  /* Check if we need to add server to access exclusion list. */

  if (strncmp((char *)recv->refid, "DENY", 4) == 0 ||
      strncmp((char *)recv->refid, "RSTR", 4) == 0 ||
      strncmp((char *)recv->refid, "RATE", 4) == 0)
    {
      struct ntp_kod_exclude_s *entry;

      entry = calloc(1, sizeof(*entry));
      if (entry)
        {
          entry->addr = *server_addr;
          sq_addlast(&entry->node, &g_ntpc_daemon.kod_list);
        }
    }

  return true;
}

/****************************************************************************
 * Name: ntpc_verify_recvd_ntp_datagram
 ****************************************************************************/

static bool ntpc_verify_recvd_ntp_datagram(
                FAR const struct ntp_datagram_s *xmit,
                FAR const struct ntp_datagram_s *recv,
                size_t nbytes,
                FAR const union ntp_addr_u *xmitaddr,
                FAR const union ntp_addr_u *recvaddr,
                size_t recvaddrlen)
{
  time_t buildtime;
  time_t seconds;

  if (recvaddr->sa.sa_family != xmitaddr->sa.sa_family)
    {
      ninfo("wrong address family\n");

      return false;
    }

#ifdef CONFIG_NET_IPv4
  if (recvaddr->sa.sa_family == AF_INET)
    {
      if (recvaddrlen != sizeof(struct sockaddr_in) ||
          xmitaddr->in4.sin_addr.s_addr != recvaddr->in4.sin_addr.s_addr ||
          xmitaddr->in4.sin_port != recvaddr->in4.sin_port)
        {
          ninfo("response from wrong peer\n");

          return false;
        }
    }
#endif

#ifdef CONFIG_NET_IPv6
  if (recvaddr->sa.sa_family == AF_INET6)
    {
      if (recvaddrlen != sizeof(struct sockaddr_in6) ||
          memcmp(&xmitaddr->in6.sin6_addr,
                 &recvaddr->in6.sin6_addr, sizeof(struct in6_addr)) != 0 ||
          xmitaddr->in6.sin6_port != recvaddr->in6.sin6_port)
        {
          ninfo("response from wrong peer\n");

          return false;
        }
    }
#endif /* CONFIG_NET_IPv6 */

  if (nbytes < NTP_DATAGRAM_MINSIZE)
    {
      /* Too short. */

      ninfo("too short response\n");

      return false;
    }

  if (GETVN(recv->lvm) != NTP_VERSION_V3 &&
      GETVN(recv->lvm) != NTP_VERSION_V4)
    {
      /* Wrong version. */

      ninfo("wrong version: %d\n", GETVN(recv->lvm));

      return false;
    }

  if (GETMODE(recv->lvm) != 4)
    {
      /* Response not in server mode. */

      ninfo("wrong mode: %d\n", GETMODE(recv->lvm));

      return false;
    }

  if (ntp_is_kiss_of_death(recv, xmitaddr))
    {
      /* KoD, Kiss-o'-Death. Ignore response. */

      ninfo("kiss-of-death response\n");

      return false;
    }

  if (GETLI(recv->lvm) == 3)
    {
      /* Clock not synchronized. */

      ninfo("LI: not synchronized\n");

      return false;
    }

  if (memcmp(recv->origtimestamp, xmit->xmittimestamp, 8) != 0)
    {
      /* "The Originate Timestamp in the server reply should match the
       * Transmit Timestamp used in the client request."
       */

      ninfo("xmittimestamp mismatch\n");

      return false;
    }

  if (ntpc_getuint32(recv->reftimestamp) == 0)
    {
      /* Invalid timestamp. */

      ninfo("invalid reftimestamp, 0x%08lx\n",
            (unsigned long)ntpc_getuint32(recv->reftimestamp));

      return false;
    }

  if (ntpc_getuint32(recv->recvtimestamp) == 0)
    {
      /* Invalid timestamp. */

      ninfo("invalid recvtimestamp, 0x%08lx\n",
            (unsigned long)ntpc_getuint32(recv->recvtimestamp));

      return false;
    }

  if (ntpc_getuint32(recv->xmittimestamp) == 0)
    {
      /* Invalid timestamp. */

      ninfo("invalid xmittimestamp, 0x%08lx\n",
            (unsigned long)ntpc_getuint32(recv->xmittimestamp));

      return false;
    }

  if ((int64_t)(ntpc_getuint64(recv->xmittimestamp) -
                ntpc_getuint64(recv->recvtimestamp)) < 0)
    {
      /* Remote received our request after sending response? */

      ninfo("invalid xmittimestamp & recvtimestamp pair\n");

      return false;
    }

  buildtime = ntpc_get_compile_timestamp();
  seconds = ntpc_getuint32(recv->recvtimestamp);
  if (seconds > NTP2UNIX_TRANSLATION)
    {
      seconds -= NTP2UNIX_TRANSLATION;
    }

  if (seconds < buildtime)
    {
      /* Invalid timestamp. */

      ninfo("invalid recvtimestamp, 0x%08lx\n",
            (unsigned long)ntpc_getuint32(recv->recvtimestamp));

      return false;
    }

  return true;
}

/****************************************************************************
 * Name: ntpc_create_dgram_socket
 ****************************************************************************/

static int ntpc_create_dgram_socket(int domain)
{
  struct timeval tv;
  int ret;
  int sd;
  int err;

  /* Create a datagram socket  */

  sd = socket(domain, SOCK_DGRAM, 0);
  if (sd < 0)
    {
      err = errno;
      nerr("ERROR: socket failed: %d\n", err);
      errno = err;
      return sd;
    }

  /* Setup a send timeout on the socket */

  tv.tv_sec  = 5;
  tv.tv_usec = 0;

  ret = setsockopt(sd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(struct timeval));
  if (ret < 0)
    {
      err = errno;
      nerr("ERROR: setsockopt(SO_SNDTIMEO) failed: %d\n", errno);
      goto err_close;
    }

  /* Setup a receive timeout on the socket */

  tv.tv_sec  = 5;
  tv.tv_usec = 0;

  ret = setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(struct timeval));
  if (ret < 0)
    {
      err = errno;
      nerr("ERROR: setsockopt(SO_RCVTIMEO) failed: %d\n", errno);
      goto err_close;
    }

  return sd;

err_close:
  close(sd);
  errno = err;
  return ret;
}

/****************************************************************************
 * Name: ntp_gethostip_multi
 ****************************************************************************/

static int ntp_gethostip_multi(FAR const char *hostname,
                               FAR union ntp_addr_u *ipaddr, size_t nipaddr)
{
  struct addrinfo hints;
  FAR struct addrinfo *info;
  FAR struct addrinfo *next;
  int ret;

  memset(&hints, 0, sizeof(struct addrinfo));
  hints.ai_family   = g_ntpc_daemon.family;
  hints.ai_socktype = SOCK_DGRAM;
  hints.ai_protocol = IPPROTO_UDP;
  hints.ai_flags    = AI_NUMERICSERV;

  ret = getaddrinfo(hostname, STR(CONFIG_NETUTILS_NTPCLIENT_PORTNO),
                    &hints, &info);
  if (ret != OK)
    {
      nerr("ERROR: getaddrinfo(%s): %d: %s\n", hostname,
           ret, gai_strerror(ret));
      return ERROR;
    }

  ret = 0;
  for (next = info; next != NULL; next = next->ai_next)
    {
      if (ret >= nipaddr)
        {
          break;
        }

      memcpy(ipaddr, next->ai_addr, next->ai_addrlen);
      ret++;
      ipaddr++;
    }

  freeaddrinfo(info);
  return ret;
}

/****************************************************************************
 * Name: ntp_get_next_hostip
 ****************************************************************************/

static int ntp_get_next_hostip(FAR struct ntp_servers_s *srvs,
                               FAR union ntp_addr_u *addr)
{
  int ret;

  if (srvs->pos >= srvs->num)
    {
      FAR char *hostname;

      srvs->pos = 0;
      srvs->num = 0;

      /* Get next hostname. */

      if (srvs->hostnext == NULL)
        {
          if (srvs->hostlist_str)
            {
              free(srvs->hostlist_str);
              srvs->hostlist_str = NULL;
            }

          /* Allocate hostname list buffer */

          srvs->hostlist_str = strdup(srvs->ntp_servers);
          if (!srvs->hostlist_str)
            {
              return ERROR;
            }

          srvs->hostlist_saveptr = NULL;
#ifndef __clang_analyzer__ /* Silence false 'possible memory leak'. */
          srvs->hostnext =
              strtok_r(srvs->hostlist_str, ";", &srvs->hostlist_saveptr);
#endif
        }

      hostname = srvs->hostnext;
      srvs->hostnext = strtok_r(NULL, ";", &srvs->hostlist_saveptr);

      if (!hostname)
        {
          /* Invalid configuration. */

          errno = EINVAL;
          return ERROR;
        }

      /* Refresh DNS for new IP-addresses. */

      ret = ntp_gethostip_multi(hostname, srvs->list,
                                ARRAY_SIZE(srvs->list));
      if (ret <= 0)
        {
          return ERROR;
        }

      srvs->num = ret;
    }

  *addr = srvs->list[srvs->pos++];

  return OK;
}

/****************************************************************************
 * Name: ntpc_get_ntp_sample
 ****************************************************************************/

static int ntpc_get_ntp_sample(FAR struct ntp_servers_s *srvs,
                               FAR struct ntp_sample_s *samples,
                               int curr_idx)
{
  FAR struct ntp_sample_s *sample = &samples[curr_idx];
  uint64_t xmit_time, recv_time;
  union ntp_addr_u server;
  union ntp_addr_u recvaddr;
  struct ntp_datagram_s xmit;
  struct ntp_datagram_s recv;
  socklen_t socklen;
  ssize_t nbytes;
  int errval;
  int retry = 0;
  int nsamples = curr_idx;
  bool addr_ok;
  int ret;
  int sd = -1;
  int i;

  /* Setup an ntp_addr_u with information about the server we are
   * going to ask the time from.
   */

  memset(&server, 0, sizeof(server));

  do
    {
      addr_ok = true;

      ret = ntp_get_next_hostip(srvs, &server);
      if (ret < 0)
        {
          errval = errno;

          nerr("ERROR: ntp_get_next_hostip() failed: %d\n", errval);

          goto sock_error;
        }

      /* Make sure that server not in exclusion list. */

      if (ntp_address_in_kod_list(&server))
        {
          if (retry < MAX_SERVER_SELECTION_RETRIES)
            {
              ninfo("on KoD list. retry DNS.\n");

              retry++;
              addr_ok = false;
              continue;
            }
          else
            {
              errval = -EALREADY;
              goto sock_error;
            }
        }

      /* Make sure that this sample is from new server. */

      for (i = 0; i < nsamples; i++)
        {
          if (memcmp(&server, &samples[i].srv_addr, sizeof(server)) == 0)
            {
              /* Already have sample from this server, retry DNS. */

              ninfo("retry DNS\n");

              if (retry < MAX_SERVER_SELECTION_RETRIES)
                {
                  retry++;
                  addr_ok = false;
                  break;
                }
              else
                {
                  /* Accept same server if cannot get DNS for other server. */

                  break;
                }
            }
        }
    }
  while (!addr_ok);

  /* Open socket. */

  sd = ntpc_create_dgram_socket(server.sa.sa_family);
  if (sd < 0)
    {
      errval = errno;

      nerr("ERROR: ntpc_create_dgram_socket() failed: %d\n", errval);

      goto sock_error;
    }

  /* Format the transmit datagram */

  memset(&xmit, 0, sizeof(xmit));
  xmit.lvm = MKLVM(0, NTP_VERSION, 3);

  ninfo("Sending a NTPv%d packet\n", NTP_VERSION);

  xmit_time = ntp_localtime();
  ntpc_setuint64(xmit.xmittimestamp, xmit_time);

  socklen = (server.sa.sa_family == AF_INET) ? sizeof(struct sockaddr_in)
                                             : sizeof(struct sockaddr_in6);
  ret = sendto(sd, &xmit, sizeof(struct ntp_datagram_s),
               0, &server.sa, socklen);
  if (ret < 0)
    {
      errval = errno;

      nerr("ERROR: sendto() failed: %d\n", errval);

      goto sock_error;
    }

  /* Attempt to receive a packet (with a timeout that was set up via
   * setsockopt() above)
   */

  nbytes = recvfrom(sd, (void *)&recv, sizeof(struct ntp_datagram_s),
                    0, &recvaddr.sa, &socklen);
  recv_time = ntp_localtime();

  /* Check if the received message was long enough to be a valid NTP
   * datagram.
   */

  if (nbytes > 0 && ntpc_verify_recvd_ntp_datagram(
                          &xmit, &recv, nbytes, &server, &recvaddr, socklen))
    {
      close(sd);
      sd = -1;

      ninfo("Calculate offset\n");

      memset(sample, 0, sizeof(struct ntp_sample_s));

      sample->srv_addr = server;

      ntpc_calculate_offset(&sample->offset, &sample->delay,
                            xmit_time, recv_time, recv.recvtimestamp,
                            recv.xmittimestamp);

      return OK;
    }
  else
    {
      /* Check for errors.  Short datagrams are handled as error. */

      errval = errno;

      if (nbytes >= 0)
        {
          errval = EMSGSIZE;
        }
      else
        {
          nerr("ERROR: recvfrom() failed: %d\n", errval);
        }

      goto sock_error;
    }

sock_error:
  if (sd >= 0)
    {
      close(sd);
      sd = -1;
    }

  errno = errval;
  return ERROR;
}

/****************************************************************************
 * Name: ntpc_daemon
 *
 * Description:
 *   This the the NTP client daemon.  This is a *very* minimal
 *   implementation! An NTP request is and the system clock is set when the
 *   response is received
 *
 ****************************************************************************/

static int ntpc_daemon(int argc, FAR char **argv)
{
  FAR struct ntp_sample_s *samples;
  FAR struct ntp_servers_s *srvs;
  int exitcode = EXIT_SUCCESS;
  int retries = 0;
  int nsamples;
  int ret;

  /* We must always be invoked with a list of servers. */

  DEBUGASSERT(argc > 1 && argv[1] != NULL && *argv[1] != '\0');

  samples = malloc(sizeof(struct ntp_sample_s) *
                   CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES);
  if (samples == NULL)
    {
      return EXIT_FAILURE;
    }

  srvs = calloc(1, sizeof(*srvs));
  if (srvs == NULL)
    {
      free(samples);
      return EXIT_FAILURE;
    }

  /* Indicate that we have started */

  g_ntpc_daemon.state = NTP_RUNNING;
  sem_post(&g_ntpc_daemon.sync);

  /* Here we do the communication with the NTP server. We collect set of
   * NTP samples (hopefully from different servers when using DNS) and
   * select median time-offset of samples. This is to filter out
   * misconfigured server giving wrong timestamps.
   *
   * NOTE that the scheduler is locked whenever this loop runs.  That
   * assures both:  (1) that there are no asynchronous stop requests and
   * (2) that we are not suspended while in critical moments when we about
   * to set the new time.  This sounds harsh, but this function is suspended
   * most of the time either: (1) sending a datagram, (2) receiving a
   * datagram, or (3) waiting for the next poll cycle.
   *
   * TODO: The first datagram that is sent is usually lost.  That is because
   * the MAC address of the NTP server is not in the ARP table.  This is
   * particularly bad here because the request will not be sent again until
   * the long delay expires leaving the system with bad time for a long time
   * initially.  Solutions:
   *
   * 1. Fix send logic so that it assures that the ARP request has been
   *    sent and the entry is in the ARP table before sending the packet
   *    (best).
   * 2. Add some ad hoc logic here so that there is no delay until at least
   *    one good time is received.
   */

  sched_lock();
  while (g_ntpc_daemon.state != NTP_STOP_REQUESTED)
    {
      struct timespec start_realtime;
      struct timespec start_monotonic;
      int errval = 0;
      int i;

      free(srvs->hostlist_str);
      memset(srvs, 0, sizeof(*srvs));
      srvs->ntp_servers = argv[1];

      memset(samples, 0, sizeof(*samples) *
                         CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES);

      clock_gettime(CLOCK_REALTIME, &start_realtime);
      clock_gettime(CLOCK_MONOTONIC, &start_monotonic);

      /* Collect samples. */

      nsamples = 0;
      for (i = 0; i < CONFIG_NETUTILS_NTPCLIENT_NUM_SAMPLES; i++)
        {
          /* Get next sample. */

          ret = ntpc_get_ntp_sample(srvs, samples, nsamples);
          if (ret < 0)
            {
              errval = errno;
            }
          else
            {
              ++nsamples;
            }
        }

      /* Analyse samples. */

      if (nsamples > 0)
        {
          int64_t offset;

          /* Select median offset of samples. */

          qsort(samples, nsamples, sizeof(*samples), sample_cmp);

          for (i = 0; i < nsamples; i++)
            {
              ninfo("NTP sample[%d]: offset: %s%lu.%03lu sec, "
                    "round-trip delay: %s%lu.%03lu sec\n",
                    i,
                    samples[i].offset < 0 ? "-" : "",
                    (unsigned long)ntp_secpart(int64abs(samples[i].offset)),
                    ntp_nsecpart(int64abs(samples[i].offset))
                      / NSEC_PER_MSEC,
                    samples[i].delay < 0 ? "-" : "",
                    (unsigned long)ntp_secpart(int64abs(samples[i].delay)),
                    ntp_nsecpart(int64abs(samples[i].delay))
                      / NSEC_PER_MSEC);
            }

          if ((nsamples % 2) == 1)
            {
              offset = samples[nsamples / 2].offset;
            }
          else
            {
              int64_t offset1 = samples[nsamples / 2].offset;
              int64_t offset2 = samples[nsamples / 2 - 1].offset;

              /* Average of two middle offsets. */

              if (offset1 > 0 && offset2 > 0)
                {
                  offset = ((uint64_t)offset1 + (uint64_t)offset2) / 2;
                }
              else if (offset1 < 0 && offset2 < 0)
                {
                  offset1 = -offset1;
                  offset2 = -offset2;

                  offset = ((uint64_t)offset1 + (uint64_t)offset2) / 2;

                  offset = -offset;
                }
              else
                {
                  offset = (offset1 + offset2) / 2;
                }
            }

          /* Adjust system time. */

          ntpc_settime(offset, &start_realtime, &start_monotonic);

          /* Save samples for ntpc_status() */

          sem_wait(&g_ntpc_daemon.lock);
          g_last_nsamples = nsamples;
          memcpy(&g_last_samples, samples, nsamples * sizeof(*samples));
          sem_post(&g_ntpc_daemon.lock);

#ifndef CONFIG_NETUTILS_NTPCLIENT_STAY_ON
          /* Configured to exit at success. */

          exitcode = EXIT_SUCCESS;
          break;

#else

          /* A full implementation of an NTP client would require much
           * more.  I think we can skip most of that here.
           */

          if (g_ntpc_daemon.state == NTP_RUNNING)
            {
              ninfo("Waiting for %d seconds\n",
                    CONFIG_NETUTILS_NTPCLIENT_POLLDELAYSEC);

              sleep(CONFIG_NETUTILS_NTPCLIENT_POLLDELAYSEC);
              retries = 0;
            }

          continue;
#endif
        }

      /* Exceeded maximum retries? */

      if (retries++ >= CONFIG_NETUTILS_NTPCLIENT_RETRIES)
        {
          nerr("ERROR: too many retries: %d\n", retries - 1);
          exitcode = EXIT_FAILURE;
          break;
        }

      /* Is this error a signal? If not, sleep before retry. */

      if (errval != EINTR)
        {
          sleep(1);
        }

      /* Keep retrying. */
    }

  /* The NTP client is terminating */

  sched_unlock();

  g_ntpc_daemon.state = NTP_STOPPED;
  sem_post(&g_ntpc_daemon.sync);
  free(srvs->hostlist_str);
  free(srvs);
  free(samples);
  return exitcode;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ntpc_dualstack_family()
 *
 * Description:
 *   Set the protocol family used (AF_INET, AF_INET6 or AF_UNSPEC)
 *
 ****************************************************************************/

void ntpc_dualstack_family(int family)
{
  sem_wait(&g_ntpc_daemon.lock);
  g_ntpc_daemon.family = family;
  sem_post(&g_ntpc_daemon.lock);
}

/****************************************************************************
 * Name: ntpc_start_with_list
 *
 * Description:
 *   Start the NTP daemon
 *
 * Returned Value:
 *   On success, the non-negative task ID of the NTPC daemon is returned;
 *   On failure, a negated errno value is returned.
 *
 ****************************************************************************/

int ntpc_start_with_list(FAR const char *ntp_server_list)
{
  FAR char *task_argv[] =
    {
      (FAR char *)ntp_server_list,
      NULL
    };

  /* Is the NTP in a non-running state? */

  sem_wait(&g_ntpc_daemon.lock);
  if (g_ntpc_daemon.state == NTP_NOT_RUNNING ||
      g_ntpc_daemon.state == NTP_STOPPED)
    {
      /* Start the NTP daemon */

      g_ntpc_daemon.state = NTP_STARTED;
      g_ntpc_daemon.pid =
        task_create("NTP daemon", CONFIG_NETUTILS_NTPCLIENT_SERVERPRIO,
                    CONFIG_NETUTILS_NTPCLIENT_STACKSIZE, ntpc_daemon,
                    task_argv);

      /* Handle failures to start the NTP daemon */

      if (g_ntpc_daemon.pid < 0)
        {
          int errval = errno;
          DEBUGASSERT(errval > 0);

          g_ntpc_daemon.state = NTP_STOPPED;
          nerr("ERROR: Failed to start the NTP daemon: %d\n", errval);
          sem_post(&g_ntpc_daemon.lock);
          return -errval;
        }

      /* Wait for any daemon state change */

      do
        {
          sem_wait(&g_ntpc_daemon.sync);
        }
      while (g_ntpc_daemon.state == NTP_STARTED);
    }

  sem_post(&g_ntpc_daemon.lock);
  return g_ntpc_daemon.pid;
}

/****************************************************************************
 * Name: ntpc_start
 *
 * Description:
 *   Start the NTP daemon
 *
 * Returned Value:
 *   On success, the non-negative task ID of the NTPC daemon is returned;
 *   On failure, a negated errno value is returned.
 *
 ****************************************************************************/

int ntpc_start(void)
{
  return ntpc_start_with_list(CONFIG_NETUTILS_NTPCLIENT_SERVER);
}

/****************************************************************************
 * Name: ntpc_stop
 *
 * Description:
 *   Stop the NTP daemon
 *
 * Returned Value:
 *   Zero on success; a negated errno value on failure.  The current
 *   implementation only returns success.
 *
 ****************************************************************************/

int ntpc_stop(void)
{
  int ret;

  /* Is the NTP in a running state? */

  sem_wait(&g_ntpc_daemon.lock);
  if (g_ntpc_daemon.state == NTP_STARTED ||
      g_ntpc_daemon.state == NTP_RUNNING)
    {
      /* Yes.. request that the daemon stop. */

      g_ntpc_daemon.state = NTP_STOP_REQUESTED;

      /* Wait for any daemon state change */

      do
        {
          /* Signal the NTP client */

          ret = kill(g_ntpc_daemon.pid,
                     CONFIG_NETUTILS_NTPCLIENT_SIGWAKEUP);

          if (ret < 0)
            {
              nerr("ERROR: kill pid %d failed: %d\n",
                   g_ntpc_daemon.pid, errno);
              break;
            }

          /* Wait for the NTP client to respond to the stop request */

          sem_wait(&g_ntpc_daemon.sync);
        }
      while (g_ntpc_daemon.state == NTP_STOP_REQUESTED);
    }

  sem_post(&g_ntpc_daemon.lock);
  return OK;
}

int ntpc_status(struct ntpc_status_s *statusp)
{
  unsigned int i;

  sem_wait(&g_ntpc_daemon.lock);
  statusp->nsamples = g_last_nsamples;
  for (i = 0; i < g_last_nsamples; i++)
    {
      statusp->samples[i].offset = g_last_samples[i].offset;
      statusp->samples[i].delay = g_last_samples[i].delay;
      statusp->samples[i]._srv_addr_store = g_last_samples[i].srv_addr.ss;
      statusp->samples[i].srv_addr = (FAR const struct sockaddr *)
                                     &statusp->samples[i]._srv_addr_store;
    }

  sem_post(&g_ntpc_daemon.lock);
  return OK;
}
