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

#if defined(__ANDROID__) && defined(CARES_THREADS)

ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg,
                                        ares_event_thread_t     *e)
{
  (void)configchg;
  (void)e;
  /* No ability */
  return ARES_ENOTIMP;
}

void ares_event_configchg_destroy(ares_event_configchg_t *configchg)
{
  /* No-op */
  (void)configchg;
}

#elif defined(__linux__) && defined(CARES_THREADS)

#  include <sys/inotify.h>

struct ares_event_configchg {
  int                  inotify_fd;
  ares_event_thread_t *e;
};

void ares_event_configchg_destroy(ares_event_configchg_t *configchg)
{
  if (configchg == NULL) {
    return; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  /* Tell event system to stop monitoring for changes.  This will cause the
   * cleanup to be called */
  ares_event_update(NULL, configchg->e, ARES_EVENT_FLAG_NONE, NULL,
                    configchg->inotify_fd, NULL, NULL, NULL);
}

static void ares_event_configchg_free(void *data)
{
  ares_event_configchg_t *configchg = data;
  if (configchg == NULL) {
    return; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  if (configchg->inotify_fd >= 0) {
    close(configchg->inotify_fd);
    configchg->inotify_fd = -1;
  }

  ares_free(configchg);
}

static void ares_event_configchg_cb(ares_event_thread_t *e, ares_socket_t fd,
                                    void *data, ares_event_flags_t flags)
{
  const ares_event_configchg_t *configchg = data;

  /* Some systems cannot read integer variables if they are not
   * properly aligned. On other systems, incorrect alignment may
   * decrease performance. Hence, the buffer used for reading from
   * the inotify file descriptor should have the same alignment as
   * struct inotify_event. */
  unsigned char                 buf[4096]
    __attribute__((aligned(__alignof__(struct inotify_event))));
  const struct inotify_event *event;
  ssize_t                     len;
  ares_bool_t                 triggered = ARES_FALSE;

  (void)fd;
  (void)flags;

  while (1) {
    const unsigned char *ptr;

    len = read(configchg->inotify_fd, buf, sizeof(buf));
    if (len <= 0) {
      break;
    }

    /* Loop over all events in the buffer. Says kernel will check the buffer
     * size provided, so I assume it won't ever return partial events. */
    for (ptr  = buf; ptr < buf + len;
         ptr += sizeof(struct inotify_event) + event->len) {
      event = (const struct inotify_event *)((const void *)ptr);

      if (event->len == 0 || ares_strlen(event->name) == 0) {
        continue;
      }

      if (ares_strcaseeq(event->name, "resolv.conf") ||
          ares_strcaseeq(event->name, "nsswitch.conf")) {
        triggered = ARES_TRUE;
      }
    }
  }

  /* Only process after all events are read.  No need to process more often as
   * we don't want to reload the config back to back */
  if (triggered) {
    ares_reinit(e->channel);
  }
}

ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg,
                                        ares_event_thread_t     *e)
{
  ares_status_t           status = ARES_SUCCESS;
  ares_event_configchg_t *c;

  (void)e;

  /* Not used by this implementation */
  *configchg = NULL;

  c = ares_malloc_zero(sizeof(*c));
  if (c == NULL) {
    return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  c->e          = e;
  c->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
  if (c->inotify_fd == -1) {
    status = ARES_ESERVFAIL; /* LCOV_EXCL_LINE: UntestablePath */
    goto done;               /* LCOV_EXCL_LINE: UntestablePath */
  }

  /* We need to monitor /etc/resolv.conf, /etc/nsswitch.conf */
  if (inotify_add_watch(c->inotify_fd, "/etc",
                        IN_CREATE | IN_MODIFY | IN_MOVED_TO | IN_ONLYDIR) ==
      -1) {
    status = ARES_ESERVFAIL; /* LCOV_EXCL_LINE: UntestablePath */
    goto done;               /* LCOV_EXCL_LINE: UntestablePath */
  }

  status =
    ares_event_update(NULL, e, ARES_EVENT_FLAG_READ, ares_event_configchg_cb,
                      c->inotify_fd, c, ares_event_configchg_free, NULL);

done:
  if (status != ARES_SUCCESS) {
    ares_event_configchg_free(c);
  } else {
    *configchg = c;
  }
  return status;
}

#elif defined(USE_WINSOCK) && defined(CARES_THREADS)

#  include <winsock2.h>
#  include <iphlpapi.h>
#  include <stdio.h>
#  include <windows.h>

struct ares_event_configchg {
  HANDLE               ifchg_hnd;
  HKEY                 regip4;
  HANDLE               regip4_event;
  HANDLE               regip4_wait;
  HKEY                 regip6;
  HANDLE               regip6_event;
  HANDLE               regip6_wait;
  ares_event_thread_t *e;
};

void ares_event_configchg_destroy(ares_event_configchg_t *configchg)
{
  if (configchg == NULL) {
    return;
  }

#  ifdef HAVE_NOTIFYIPINTERFACECHANGE
  if (configchg->ifchg_hnd != NULL) {
    CancelMibChangeNotify2(configchg->ifchg_hnd);
    configchg->ifchg_hnd = NULL;
  }
#  endif

#  ifdef HAVE_REGISTERWAITFORSINGLEOBJECT
  if (configchg->regip4_wait != NULL) {
    UnregisterWait(configchg->regip4_wait);
    configchg->regip4_wait = NULL;
  }

  if (configchg->regip6_wait != NULL) {
    UnregisterWait(configchg->regip6_wait);
    configchg->regip6_wait = NULL;
  }

  if (configchg->regip4 != NULL) {
    RegCloseKey(configchg->regip4);
    configchg->regip4 = NULL;
  }

  if (configchg->regip6 != NULL) {
    RegCloseKey(configchg->regip6);
    configchg->regip6 = NULL;
  }

  if (configchg->regip4_event != NULL) {
    CloseHandle(configchg->regip4_event);
    configchg->regip4_event = NULL;
  }

  if (configchg->regip6_event != NULL) {
    CloseHandle(configchg->regip6_event);
    configchg->regip6_event = NULL;
  }
#  endif

  ares_free(configchg);
}


#  ifdef HAVE_NOTIFYIPINTERFACECHANGE
static void NETIOAPI_API_
  ares_event_configchg_ip_cb(PVOID CallerContext, PMIB_IPINTERFACE_ROW Row,
                             MIB_NOTIFICATION_TYPE NotificationType)
{
  ares_event_configchg_t *configchg = CallerContext;
  (void)Row;
  (void)NotificationType;
  ares_reinit(configchg->e->channel);
}
#  endif

static ares_bool_t
  ares_event_configchg_regnotify(ares_event_configchg_t *configchg)
{
#  ifdef HAVE_REGISTERWAITFORSINGLEOBJECT
#    if defined(__WATCOMC__) && !defined(REG_NOTIFY_THREAD_AGNOSTIC)
#      define REG_NOTIFY_THREAD_AGNOSTIC 0x10000000L
#    endif
  DWORD flags = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET |
                REG_NOTIFY_THREAD_AGNOSTIC;

  if (RegNotifyChangeKeyValue(configchg->regip4, TRUE, flags,
                              configchg->regip4_event, TRUE) != ERROR_SUCCESS) {
    return ARES_FALSE;
  }

  if (RegNotifyChangeKeyValue(configchg->regip6, TRUE, flags,
                              configchg->regip6_event, TRUE) != ERROR_SUCCESS) {
    return ARES_FALSE;
  }
#  else
  (void)configchg;
#  endif
  return ARES_TRUE;
}

static VOID CALLBACK ares_event_configchg_reg_cb(PVOID   lpParameter,
                                                 BOOLEAN TimerOrWaitFired)
{
  ares_event_configchg_t *configchg = lpParameter;
  (void)TimerOrWaitFired;

  ares_reinit(configchg->e->channel);

  /* Re-arm, as its single-shot.  However, we don't know which one needs to
   * be re-armed, so we just do both */
  ares_event_configchg_regnotify(configchg);
}

ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg,
                                        ares_event_thread_t     *e)
{
  ares_status_t           status = ARES_SUCCESS;
  ares_event_configchg_t *c      = NULL;

  c = ares_malloc_zero(sizeof(**configchg));
  if (c == NULL) {
    return ARES_ENOMEM;
  }

  c->e = e;

#  ifdef HAVE_NOTIFYIPINTERFACECHANGE
  /* NOTE: If a user goes into the control panel and changes the network
   *       adapter DNS addresses manually, this will NOT trigger a notification.
   *       We've also tried listening on NotifyUnicastIpAddressChange(), but
   *       that didn't get triggered either.
   */
  if (NotifyIpInterfaceChange(AF_UNSPEC, ares_event_configchg_ip_cb, c, FALSE,
                              &c->ifchg_hnd) != NO_ERROR) {
    status = ARES_ESERVFAIL;
    goto done;
  }
#  endif

#  ifdef HAVE_REGISTERWAITFORSINGLEOBJECT
  /* Monitor HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces
   * and HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces
   * for changes via RegNotifyChangeKeyValue() */
  if (RegOpenKeyExW(
        HKEY_LOCAL_MACHINE,
        L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces",
        0, KEY_NOTIFY, &c->regip4) != ERROR_SUCCESS) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  if (RegOpenKeyExW(
        HKEY_LOCAL_MACHINE,
        L"SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters\\Interfaces",
        0, KEY_NOTIFY, &c->regip6) != ERROR_SUCCESS) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  c->regip4_event = CreateEvent(NULL, TRUE, FALSE, NULL);
  if (c->regip4_event == NULL) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  c->regip6_event = CreateEvent(NULL, TRUE, FALSE, NULL);
  if (c->regip6_event == NULL) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  if (!RegisterWaitForSingleObject(&c->regip4_wait, c->regip4_event,
                                   ares_event_configchg_reg_cb, c, INFINITE,
                                   WT_EXECUTEDEFAULT)) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  if (!RegisterWaitForSingleObject(&c->regip6_wait, c->regip6_event,
                                   ares_event_configchg_reg_cb, c, INFINITE,
                                   WT_EXECUTEDEFAULT)) {
    status = ARES_ESERVFAIL;
    goto done;
  }
#  endif

  if (!ares_event_configchg_regnotify(c)) {
    status = ARES_ESERVFAIL;
    goto done;
  }

done:
  if (status != ARES_SUCCESS) {
    ares_event_configchg_destroy(c);
  } else {
    *configchg = c;
  }

  return status;
}

#elif defined(__APPLE__) && defined(CARES_THREADS)

#  include <sys/types.h>
#  include <unistd.h>
#  include <stdbool.h>
#  include <notify.h>
#  include <dlfcn.h>
#  include <fcntl.h>

struct ares_event_configchg {
  int fd;
  int token;
};

void ares_event_configchg_destroy(ares_event_configchg_t *configchg)
{
  (void)configchg;

  /* Cleanup happens automatically */
}

static void ares_event_configchg_free(void *data)
{
  ares_event_configchg_t *configchg = data;
  if (configchg == NULL) {
    return;
  }

  if (configchg->fd >= 0) {
    notify_cancel(configchg->token);
    /* automatically closes fd */
    configchg->fd = -1;
  }

  ares_free(configchg);
}

static void ares_event_configchg_cb(ares_event_thread_t *e, ares_socket_t fd,
                                    void *data, ares_event_flags_t flags)
{
  ares_event_configchg_t *configchg = data;
  ares_bool_t             triggered = ARES_FALSE;

  (void)fd;
  (void)flags;

  while (1) {
    int     t = 0;
    ssize_t len;

    len = read(configchg->fd, &t, sizeof(t));

    if (len < (ssize_t)sizeof(t)) {
      break;
    }

    /* Token is read in network byte order (yeah, docs don't mention this) */
    t = (int)ntohl(t);

    if (t != configchg->token) {
      continue;
    }

    triggered = ARES_TRUE;
  }

  /* Only process after all events are read.  No need to process more often as
   * we don't want to reload the config back to back */
  if (triggered) {
    ares_reinit(e->channel);
  }
}

ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg,
                                        ares_event_thread_t     *e)
{
  ares_status_t status                               = ARES_SUCCESS;
  void         *handle                               = NULL;
  const char *(*pdns_configuration_notify_key)(void) = NULL;
  const char *notify_key                             = NULL;
  int         flags;
  size_t      i;
  const char *searchlibs[] = {
    "/usr/lib/libSystem.dylib",
    "/System/Library/Frameworks/SystemConfiguration.framework/"
    "SystemConfiguration",
    NULL
  };

  *configchg = ares_malloc_zero(sizeof(**configchg));
  if (*configchg == NULL) {
    return ARES_ENOMEM;
  }

  /* Load symbol as it isn't normally public */
  for (i = 0; searchlibs[i] != NULL; i++) {
    handle = dlopen(searchlibs[i], RTLD_LAZY);
    if (handle == NULL) {
      /* Fail, loop! */
      continue;
    }

    pdns_configuration_notify_key =
      (const char *(*)(void))dlsym(handle, "dns_configuration_notify_key");
    if (pdns_configuration_notify_key != NULL) {
      break;
    }

    /* Fail, loop! */
    dlclose(handle);
    handle = NULL;
  }

  if (pdns_configuration_notify_key == NULL) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  notify_key = pdns_configuration_notify_key();
  if (notify_key == NULL) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  if (notify_register_file_descriptor(notify_key, &(*configchg)->fd, 0,
                                      &(*configchg)->token) !=
      NOTIFY_STATUS_OK) {
    status = ARES_ESERVFAIL;
    goto done;
  }

  /* Set file descriptor to non-blocking */
  flags = fcntl((*configchg)->fd, F_GETFL, 0);
  fcntl((*configchg)->fd, F_SETFL, flags | O_NONBLOCK);

  /* Register file descriptor with event subsystem */
  status = ares_event_update(NULL, e, ARES_EVENT_FLAG_READ,
                             ares_event_configchg_cb, (*configchg)->fd,
                             *configchg, ares_event_configchg_free, NULL);

done:
  if (status != ARES_SUCCESS) {
    ares_event_configchg_free(*configchg);
    *configchg = NULL;
  }

  if (handle) {
    dlclose(handle);
  }

  return status;
}

#elif defined(HAVE_STAT) && !defined(_WIN32) && defined(CARES_THREADS)
#  ifdef HAVE_SYS_TYPES_H
#    include <sys/types.h>
#  endif
#  ifdef HAVE_SYS_STAT_H
#    include <sys/stat.h>
#  endif

typedef struct {
  size_t size;
  time_t mtime;
} fileinfo_t;

struct ares_event_configchg {
  ares_bool_t          isup;
  ares_thread_t       *thread;
  ares_htable_strvp_t *filestat;
  ares_thread_mutex_t *lock;
  ares_thread_cond_t  *wake;
  const char          *resolvconf_path;
  ares_event_thread_t *e;
};

static ares_status_t config_change_check(ares_htable_strvp_t *filestat,
                                         const char          *resolvconf_path)
{
  size_t      i;
  const char *configfiles[16];
  ares_bool_t changed = ARES_FALSE;
  size_t      cnt = 0;

  memset(configfiles, 0, sizeof(configfiles));

  configfiles[cnt++] = resolvconf_path;
  configfiles[cnt++] = "/etc/nsswitch.conf";
#ifdef _AIX
  configfiles[cnt++] = "/etc/netsvc.conf";
#endif
#ifdef __osf /* Tru64 */
  configfiles[cnt++] = "/etc/svc.conf";
#endif
#ifdef __QNX__
  configfiles[cnt++] = "/etc/net.cfg";
#endif
  configfiles[cnt++] = NULL;

  for (i = 0; configfiles[i] != NULL; i++) {
    fileinfo_t *fi = ares_htable_strvp_get_direct(filestat, configfiles[i]);
    struct stat st;

    if (stat(configfiles[i], &st) == 0) {
      if (fi == NULL) {
        fi = ares_malloc_zero(sizeof(*fi));
        if (fi == NULL) {
          return ARES_ENOMEM;
        }
        if (!ares_htable_strvp_insert(filestat, configfiles[i], fi)) {
          ares_free(fi);
          return ARES_ENOMEM;
        }
      }
      if (fi->size != (size_t)st.st_size || fi->mtime != (time_t)st.st_mtime) {
        changed = ARES_TRUE;
      }
      fi->size  = (size_t)st.st_size;
      fi->mtime = (time_t)st.st_mtime;
    } else if (fi != NULL) {
      /* File no longer exists, remove */
      ares_htable_strvp_remove(filestat, configfiles[i]);
      changed = ARES_TRUE;
    }
  }

  if (changed) {
    return ARES_SUCCESS;
  }
  return ARES_ENOTFOUND;
}

static void *ares_event_configchg_thread(void *arg)
{
  ares_event_configchg_t *c = arg;

  ares_thread_mutex_lock(c->lock);
  while (c->isup) {
    ares_status_t status;

    if (ares_thread_cond_timedwait(c->wake, c->lock, 30000) != ARES_ETIMEOUT) {
      continue;
    }

    /* make sure status didn't change even though we got a timeout */
    if (!c->isup) {
      break;
    }

    status = config_change_check(c->filestat, c->resolvconf_path);
    if (status == ARES_SUCCESS) {
      ares_reinit(c->e->channel);
    }
  }

  ares_thread_mutex_unlock(c->lock);
  return NULL;
}

ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg,
                                        ares_event_thread_t     *e)
{
  ares_status_t           status = ARES_SUCCESS;
  ares_event_configchg_t *c      = NULL;

  *configchg = NULL;

  c = ares_malloc_zero(sizeof(*c));
  if (c == NULL) {
    status = ARES_ENOMEM;
    goto done;
  }

  c->e = e;

  c->filestat = ares_htable_strvp_create(ares_free);
  if (c->filestat == NULL) {
    status = ARES_ENOMEM;
    goto done;
  }

  c->wake = ares_thread_cond_create();
  if (c->wake == NULL) {
    status = ARES_ENOMEM;
    goto done;
  }

  c->lock = ares_thread_mutex_create();
  if (c->lock == NULL) {
    status = ARES_ENOMEM;
    goto done;
  }

  c->resolvconf_path = c->e->channel->resolvconf_path;
  if (c->resolvconf_path == NULL) {
    c->resolvconf_path = PATH_RESOLV_CONF;
  }

  status = config_change_check(c->filestat, c->resolvconf_path);
  if (status == ARES_ENOMEM) {
    goto done;
  }

  c->isup = ARES_TRUE;
  status  = ares_thread_create(&c->thread, ares_event_configchg_thread, c);

done:
  if (status != ARES_SUCCESS) {
    ares_event_configchg_destroy(c);
  } else {
    *configchg = c;
  }
  return status;
}

void ares_event_configchg_destroy(ares_event_configchg_t *configchg)
{
  if (configchg == NULL) {
    return;
  }

  if (configchg->lock) {
    ares_thread_mutex_lock(configchg->lock);
  }

  configchg->isup = ARES_FALSE;
  if (configchg->wake) {
    ares_thread_cond_signal(configchg->wake);
  }

  if (configchg->lock) {
    ares_thread_mutex_unlock(configchg->lock);
  }

  if (configchg->thread) {
    void *rv = NULL;
    ares_thread_join(configchg->thread, &rv);
  }

  ares_thread_mutex_destroy(configchg->lock);
  ares_thread_cond_destroy(configchg->wake);
  ares_htable_strvp_destroy(configchg->filestat);
  ares_free(configchg);
}

#else

ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg,
                                        ares_event_thread_t     *e)
{
  (void)configchg;
  (void)e;
  /* No ability */
  return ARES_ENOTIMP;
}

void ares_event_configchg_destroy(ares_event_configchg_t *configchg)
{
  /* No-op */
  (void)configchg;
}

#endif
