/*
 * Copyright (C) 2021 Purism SPC
 *
 * This file is part of Calls.
 *
 * Calls is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Calls is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Calls. If not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Evangelos Ribeiro Tzaras <evangelos.tzaras@puri.sm>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 */

#define G_LOG_DOMAIN "CallsNetworkWatch"

#include "calls-network-watch.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <NetworkManager.h>

/**
 * SECTION:calls-network-watch
 * @short_description: Watches network interfaces via NetworkManager
 * @Title: CallsNetworkWatch
 *
 * The #CallsNetworkWatch uses the NetworkManager D-Bus API to keep
 * track of network interfaces.
 *
 * This allows the sofia SIP stack to bind to the
 * correct interface and can later help in deciding which codec to
 * use for media (e.g. lower bandwidth on a metered connection).
 */


enum {
  PROP_0,
  PROP_IPV4,
  PROP_IPV6,
  PROP_LAST_PROP
};
static GParamSpec *props[PROP_LAST_PROP];

enum {
  NETWORK_CHANGED,
  N_SIGNALS
};
static guint signals[N_SIGNALS];


struct _CallsNetworkWatch {
  GObject             parent;

  GCancellable       *cancellable;

  NMClient           *nm_client;
  NMActiveConnection *active;

  char               *ipv4;
  char               *ipv6;

  unsigned long       client_notify_id;

  unsigned long       active_notify_id;
};

G_DEFINE_TYPE (CallsNetworkWatch, calls_network_watch, G_TYPE_OBJECT)

static char *
get_best_ipv4 (NMIPConfig *cfg) {
  const GPtrArray *addrs;
  char *best_ipv4 = NULL;
  uint best_priority = 0;

  if (!cfg)
    return NULL;

  addrs = nm_ip_config_get_addresses (cfg);
  if (!addrs)
    return NULL;

  for (uint i = 0; i < addrs->len; i++) {
    uint priority;
    uint ip_int;
    struct in_addr addr;
    NMIPAddress *a = g_ptr_array_index (addrs, i);
    const char *ip = nm_ip_address_get_address (a);
    if (!ip)
      continue;

    if (inet_pton (AF_INET, ip, &addr) != 1)
      continue;
    ip_int = ntohl (addr.s_addr);

    /* Skip loopback 127.0.0.0/8 */
    if ((ip_int & 0xFF000000) == 0x7F000000)
      continue;

    /* Skip link-local 169.254.0.0/16 */
    if ((ip_int & 0xFFFF0000) == 0xA9FE0000)
      continue;

    /* Determine priority: global > private */
    if ((ip_int & 0xFF000000) != 0x0A000000 && /* 10.0.0.0/8 */
        (ip_int & 0xFFF00000) != 0xAC100000 && /* 172.16.0.0/12 */
        (ip_int & 0xFFFF0000) != 0xC0A80000)   /* 192.168.0.0/16 */
      priority = 2; /* Public */
    else
      priority = 1; /* Private */

    if (!best_ipv4 || priority > best_priority) {
      g_free (best_ipv4);
      best_ipv4 = g_strdup (ip);
      best_priority = priority;
    }
  }

  return best_ipv4;
}

static char *
get_best_ipv6 (NMIPConfig *cfg) {
  const GPtrArray *addrs;
  char *best_ipv6 = NULL;
  uint best_priority = 0;

  if (!cfg)
    return NULL;

  addrs = nm_ip_config_get_addresses (cfg);
  if (!addrs)
    return NULL;

  for (uint i = 0; i < addrs->len; i++) {
    uint priority;
    NMIPAddress *a = g_ptr_array_index (addrs, i);
    const char *ip = nm_ip_address_get_address (a);
    if (!ip)
      continue;

    /* Skip loopback */
    if (g_str_equal (ip, "::1"))
      continue;

    /* Skip link-local */
    if (g_str_has_prefix (ip, "fe80:"))
      continue;

    /* Determine priority: global > private */
    if (!g_str_has_prefix (ip, "fe80:") &&
        !g_str_has_prefix (ip, "fc00:") &&
        !g_str_has_prefix (ip, "fd00:"))
      priority = 2; /* Global */
    else
      priority = 1; /* Private/ULA */

    if (!best_ipv6 || priority > best_priority) {
      g_free (best_ipv6);
      best_ipv6 = g_strdup (ip);
      best_priority = priority;
    }
  }

  return best_ipv6;
}

static void
refresh_ips_from_active (CallsNetworkWatch *self)
{
  const GPtrArray *devs;
  NMDevice *dev = NULL;
  NMIPConfig *ip4 = NULL;
  NMIPConfig *ip6 = NULL;
  char *new4 = NULL;
  char *new6 = NULL;
  gboolean changed = FALSE;

  g_debug ("Refreshing IP addresses from active connection");

  if (self->active) {
    devs = nm_active_connection_get_devices (self->active);
    if (devs) {
      for (uint i = 0; i < devs->len; i++) {
        dev = g_ptr_array_index ((GPtrArray *)devs, i);
        if (!dev)
          continue;

        if (!new4) {
          ip4 = nm_device_get_ip4_config (dev);
          if (ip4)
            new4 = get_best_ipv4 (ip4);
        }

        if (!new6) {
          ip6 = nm_device_get_ip6_config (dev);
          if (ip6)
            new6 = get_best_ipv6 (ip6);
        }

        if (new4 && new6)
          break;
      }
    }
  }

  if (g_strcmp0 (self->ipv4, new4) != 0) {
    g_debug ("IPv4 address changed from '%s' to '%s'",
             self->ipv4, new4);

    g_set_str (&self->ipv4, new4);
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IPV4]);
    changed = TRUE;
  }
  if (g_strcmp0 (self->ipv6, new6) != 0) {
    g_debug ("IPv6 address changed from '%s' to '%s'",
             self->ipv6, new6);

    g_set_str (&self->ipv6, new6);
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IPV6]);
    changed = TRUE;
  }

  g_free (new4);
  g_free (new6);

  if (changed) {
    g_debug ("Emitting 'network-changed' signal due to IP change");
    g_signal_emit (self, signals[NETWORK_CHANGED], 0);
  }
}

/* ---------- callbacks ---------- */

static void
on_active_notify (GObject    *obj,
                  GParamSpec *pspec,
                  gpointer    user_data)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (user_data);
  const char *name = pspec->name;

  g_debug ("Received 'notify::%s' signal from active connection", name);

  if (g_str_equal (name, "ip4-config") ||
      g_str_equal (name, "ip6-config") ||
      g_str_equal (name, "state")) {
    g_debug ("IP config or state changed, refreshing IPs");
    refresh_ips_from_active (self);
  }
}

/* Replace active connection, install/remove notify handlers */
static void
update_from_active_connection (CallsNetworkWatch *self)
{
  NMActiveConnection *primary =
    nm_client_get_primary_connection (self->nm_client);

  g_debug ("Updating active connection (primary: %p, current: %p)",
           primary, self->active);

  if (primary == self->active) {
    g_debug ("Primary connection unchanged, refreshing IPs");
    refresh_ips_from_active (self);
    return;
  }

  /* disconnect old */
  if (self->active && self->active_notify_id) {
    g_debug ("Disconnecting notify handler from old active connection");
    g_signal_handler_disconnect (self->active, self->active_notify_id);
    self->active_notify_id = 0;
  }
  g_clear_object (&self->active);

  /* connect new */
  if (primary) {
    g_debug ("Setting new primary connection");
    self->active = g_object_ref (primary);
    self->active_notify_id =
      g_signal_connect (self->active,
                        "notify",
                        G_CALLBACK (on_active_notify),
                        self);
    g_debug ("Installed notify handler on new active connection");
  } else {
    g_debug ("No primary connection available");
  }

  refresh_ips_from_active (self);
}

/* NMClient signal handlers */
static void
on_client_notify (GObject    *obj,
                  GParamSpec *pspec,
                  gpointer    user_data)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (user_data);

  g_debug ("Primary connection changed, updating active connection");
  update_from_active_connection (self);
}

static void
on_new_nm_client (GObject      *object,
                  GAsyncResult *res,
                  gpointer      user_data)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (user_data);
  g_autoptr (GError) error = NULL;

  self->nm_client = nm_client_new_finish (res, &error);
  if (!self->nm_client) {
    g_warning ("Failed to create NMClient: %s", error ? error->message : "(no error)");
    return;
  }

  /* Connect signals and update active connection */
  self->client_notify_id = g_signal_connect (G_OBJECT (self->nm_client), "notify::primary-connection",
                                             G_CALLBACK (on_client_notify), self);

  update_from_active_connection (self);
}

static void
calls_network_watch_get_property (GObject    *object,
                                  guint       property_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (object);

  switch (property_id) {
  case PROP_IPV4:
    g_value_set_string (value, calls_network_watch_get_ipv4 (self));
    break;

  case PROP_IPV6:
    g_value_set_string (value, calls_network_watch_get_ipv6 (self));
    break;

  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
calls_network_watch_finalize (GObject *object)
{
  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (object);

  g_debug ("Finalizing CallsNetworkWatch");

  if (self->client_notify_id && self->nm_client)
    g_signal_handler_disconnect (G_OBJECT (self->nm_client), self->client_notify_id);

  if (self->active && self->active_notify_id)
    g_signal_handler_disconnect (G_OBJECT (self->active), self->active_notify_id);

  g_clear_object (&self->active);
  g_clear_object (&self->nm_client);

  g_free (self->ipv4);
  g_free (self->ipv6);

  G_OBJECT_CLASS (calls_network_watch_parent_class)->finalize (object);
}

static void
calls_network_watch_class_init (CallsNetworkWatchClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->get_property = calls_network_watch_get_property;
  object_class->finalize = calls_network_watch_finalize;

  signals[NETWORK_CHANGED] = g_signal_new ("network-changed",
                                           CALLS_TYPE_NETWORK_WATCH,
                                           G_SIGNAL_RUN_LAST,
                                           0,
                                           NULL, NULL, NULL,
                                           G_TYPE_NONE,
                                           0);

  props[PROP_IPV4] =
    g_param_spec_string ("ipv4",
                         "IPv4",
                         "The preferred source address for IPv4",
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_IPV6] =
    g_param_spec_string ("ipv6",
                         "IPv6",
                         "The preferred source address for IPv6",
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
}


static void
calls_network_watch_init (CallsNetworkWatch *self)
{
  g_debug ("Initializing CallsNetworkWatch");

  self->cancellable = g_cancellable_new ();
  nm_client_new_async (self->cancellable, on_new_nm_client, self);
}


CallsNetworkWatch *
calls_network_watch_new (void)
{
  return g_object_new (CALLS_TYPE_NETWORK_WATCH, NULL);
}


const char *
calls_network_watch_get_ipv4 (CallsNetworkWatch *self)
{
  g_return_val_if_fail (CALLS_IS_NETWORK_WATCH (self), NULL);

  return self->ipv4;
}


const char *
calls_network_watch_get_ipv6 (CallsNetworkWatch *self)
{
  g_return_val_if_fail (CALLS_IS_NETWORK_WATCH (self), NULL);

  return self->ipv6;
}
