/*
 * Copyright (C) 2025 The Phosh.mobi e.V.
 *
 * SPDX-License-Identifier: GPL-3.0+
 *
 * Author: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
 */

#define G_LOG_DOMAIN "cbd-mm-manager"

#include "cbd-config.h"

#include "cbd-channel-manager.h"
#include "cbd-mm-manager.h"
#include "cbd-modem-3gpp.h"
#include "cbd-modem-cell-broadcast.h"

#include <libmm-glib.h>


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

enum {
  NEW_CBM,
  LAST_SIGNAL
};
static guint signals[LAST_SIGNAL];

typedef struct _CbdMmManager {
  GObject parent;

  guint           watch_id;
  GCancellable   *cancellable;
  MMManager      *mm;
  char           *mm_version;

  CbdChannelManager *channel_manager;
  GListModel        *modems;
  GListModel        *modems_3gpp;
  GListModel        *modems_cell_broadcast;
  gboolean           has_cell_broadcast;
} CbdMmManager;

G_DEFINE_FINAL_TYPE (CbdMmManager, cbd_mm_manager, G_TYPE_OBJECT);


static gboolean
is_cbm_support_blocklisted (CbdMmManager *self)
{
  if (!self->mm)
    return TRUE;

  /* MM 1.24 has CBM support but doesn't support SetChannel and QMI.
   * Since older versions will never expose a CellBroadcast interface
   * and 1.25 already supports both SetChannel and QMI we only need to
   * blocklist that single version in order to make sure users don't
   * think they're all set */
  if (g_str_has_prefix (self->mm_version, "1.24."))
    return TRUE;

  return FALSE;
}


static void
on_cell_broadcast_n_items_changed (CbdMmManager *self)
{
  gboolean has_cell_broadcast = !!g_list_model_get_n_items (self->modems_cell_broadcast);

  /* Only indicate usable Cell Broadcast support to consumers if the implementation is
   * reasonably complete */
  if (is_cbm_support_blocklisted (self))
    has_cell_broadcast = FALSE;

  if (self->has_cell_broadcast == has_cell_broadcast)
    return;

  self->has_cell_broadcast = has_cell_broadcast;
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_CELLBROADCAST]);
}


static void
add_cbm (CbdMmManager         *self,
         MMModemCellBroadcast *cell_broadcast,
         MMCbm                *cbm)
{
  const char *operator_code = NULL;

  g_assert (CBD_IS_MM_MANAGER (self));
  g_assert (MM_IS_CBM (cbm));
  g_assert (mm_cbm_get_state (cbm) == MM_CBM_STATE_RECEIVED);
  g_assert (MM_IS_MODEM_CELL_BROADCAST (cell_broadcast));

  g_signal_handlers_disconnect_by_data (cbm, self);

  for (int i = 0; i < g_list_model_get_n_items (self->modems_3gpp); i++) {
    g_autoptr (CbdModem3gpp) modem_3gpp = g_list_model_get_item (self->modems_3gpp, i);

    operator_code = cbd_modem_3gpp_get_operator_code (modem_3gpp);
    if (operator_code)
      break;
  }
  g_debug ("Adding new cell broadcast message '%s', operator: '%s'",
           mm_cbm_get_path (cbm),
           operator_code);

  g_signal_emit (self, signals[NEW_CBM],
                 0,
                 mm_cbm_get_text (cbm),
                 mm_cbm_get_channel (cbm),
                 mm_cbm_get_message_code (cbm),
                 mm_cbm_get_update (cbm),
                 operator_code);
}


static void
on_cbm_state_changed (CbdMmManager *self, MMCbm *cbm)
{
  MMModemCellBroadcast *cell_broadcast;

  if (mm_cbm_get_state (cbm) != MM_CBM_STATE_RECEIVED)
    return;

  cell_broadcast = g_object_get_data (G_OBJECT (cbm), "modem-cell-broadcast");
  add_cbm (self, cell_broadcast, cbm);
}


static void
track_cbm (CbdMmManager         *self,
           MMModemCellBroadcast *modem_cell_broadcast,
           MMCbm                *cbm)
{
  g_debug ("Tracking '%s'", mm_cbm_get_path (cbm));

  if (mm_cbm_get_state (cbm) != MM_CBM_STATE_RECEIVED) {
    g_object_set_data (G_OBJECT (cbm), "modem-cell-broadcast", modem_cell_broadcast);
    g_signal_connect_object (cbm, "notify::state", G_CALLBACK (on_cbm_state_changed), self, G_CONNECT_DEFAULT);
    return;
  }

  add_cbm (self, modem_cell_broadcast, cbm);
}

typedef struct {
  CbdMmManager *self;
  char *cbm_path;
} CbmListData;

static void on_cb_list (GObject      *object,
                        GAsyncResult *res,
                        gpointer      user_data)
{
  CbmListData *data = user_data;
  GList *list;
  g_autoptr (GError) error = NULL;

  list = mm_modem_cell_broadcast_list_finish (MM_MODEM_CELL_BROADCAST (object),
                                              res,
                                              &error);
  if (error) {
    g_warning ("Could not list cell broadcast messages from '%s': %s",
               mm_modem_cell_broadcast_get_path (MM_MODEM_CELL_BROADCAST (object)),
                                                 error->message);
    return;
  }

  for (GList *node = list; node; node = node->next) {
    MMCbm *cbm = node->data;

    if (!data->cbm_path || g_str_equal (data->cbm_path, mm_cbm_get_path (cbm)))
      track_cbm (data->self, MM_MODEM_CELL_BROADCAST (object), g_object_ref (cbm));
  }

  g_free (data->cbm_path);
  g_free (data);

  g_list_free_full (list, g_object_unref);
}


static void
on_cbm_added (CbdMmManager         *self,
              const char           *cbm_path,
              MMModemCellBroadcast *mm_cb)
{
  CbmListData *data;

  g_debug ("cbm added: %s", cbm_path);
  data = g_new0 (CbmListData, 1);
  data->self = self;
  data->cbm_path = g_strdup (cbm_path);

  mm_modem_cell_broadcast_list (mm_cb,
                                self->cancellable,
                                on_cb_list,
                                data);
}


static void
update_channels (CbdMmManager *self)
{
  GArray *channels;

  channels = cbd_channel_manager_get_channels (self->channel_manager);
  if (!channels) {
    g_critical ("Channel list for '%s' is empty",
                cbd_channel_manager_get_country (self->channel_manager));
    return;
  }

  /* TODO: Should we only do that for the modem that flipped country ? */
  for (int i = 0; i < g_list_model_get_n_items (self->modems_cell_broadcast); i++) {
    g_autoptr (CbdModemCellBroadcast) modem = NULL;

    modem = g_list_model_get_item (self->modems_cell_broadcast, i);
    cbd_modem_cell_broadcast_set_channels (modem, channels);
  }
}


static void
on_country_changed (CbdMmManager *self, GParamSpec *pspec, CbdModem3gpp *modem_3gpp)
{
  const char *new_country, *country;
  LcbChannelMode mode;

  country = cbd_channel_manager_get_country (self->channel_manager);
  new_country = cbd_modem_3gpp_get_country (modem_3gpp);

  if (country && !new_country)
    return;

  if (country && !g_strcmp0 (country, new_country))
    return;

  cbd_channel_manager_set_country (self->channel_manager, new_country);
  mode = cbd_channel_manager_get_mode (self->channel_manager);
  if (mode != LCB_CHANNEL_MODE_COUNTRY)
    return;

  update_channels (self);
}


static void
modem_init_3gpp (CbdMmManager *self, MMObject *object)
{
  CbdModem3gpp *modem;
  MMModem3gpp *modem_3gpp = mm_object_peek_modem_3gpp (MM_OBJECT (object));

  if (!modem_3gpp)
    return;

  g_debug ("New 3gpp interface '%s'", mm_modem_3gpp_get_path (modem_3gpp));
  modem = cbd_modem_3gpp_new (modem_3gpp);
  g_signal_connect_object (modem,
                           "notify::country",
                           G_CALLBACK (on_country_changed),
                           self,
                           G_CONNECT_SWAPPED);
  on_country_changed (self, NULL, modem);

  g_list_store_append (G_LIST_STORE (self->modems_3gpp), modem);
}


static void
modem_init_cellbroadcast (CbdMmManager *self, MMObject *object)
{
  CbmListData *data;
  CbdModemCellBroadcast *modem;
  MMModemCellBroadcast *modem_cell_broadcast;
  MMModem3gpp *modem_3gpp;

  modem_cell_broadcast = mm_object_peek_modem_cell_broadcast (MM_OBJECT (object));
  if (!modem_cell_broadcast)
    return;

  g_debug ("New cell broadcast interface '%s'",
           mm_modem_cell_broadcast_get_path (modem_cell_broadcast));
  modem = cbd_modem_cell_broadcast_new (modem_cell_broadcast);
  g_list_store_append (G_LIST_STORE (self->modems_cell_broadcast), modem);

  /* We already have a 3gpp interface so update the channels */
  modem_3gpp = mm_object_peek_modem_3gpp (MM_OBJECT (object));
  if (modem_3gpp)
    update_channels (self);

  g_signal_connect_object (modem_cell_broadcast,
                           "added",
                           G_CALLBACK (on_cbm_added), self,
                           G_CONNECT_SWAPPED);
  g_debug ("Listing cell broadcast messages");

  data = g_new0 (CbmListData, 1);
  data->self = self;

  mm_modem_cell_broadcast_list (modem_cell_broadcast,
                                self->cancellable,
                                on_cb_list,
                                data);
}


static void
modem_finalize_cell_broadcast (CbdMmManager *self, MMObject *object)
{
  MMModemCellBroadcast *modem_cell_broadcast;

  modem_cell_broadcast = mm_object_peek_modem_cell_broadcast (MM_OBJECT (object));
  if (!modem_cell_broadcast)
    return;

  for (int i = 0; i < g_list_model_get_n_items (self->modems_cell_broadcast); i++) {
    g_autoptr (CbdModemCellBroadcast) modem = NULL;

    modem = g_list_model_get_item (self->modems_cell_broadcast, i);
    if (cbd_modem_cell_broadcast_match (modem, modem_cell_broadcast)) {
      g_list_store_remove (G_LIST_STORE (self->modems_cell_broadcast), i);
      return;
    }
  }
}


static void
modem_finalize_3gpp (CbdMmManager *self, MMObject *object)
{
  MMModem3gpp *modem_3gpp;

  modem_3gpp = mm_object_peek_modem_3gpp (MM_OBJECT (object));
  for (int i = 0; i < g_list_model_get_n_items (self->modems_3gpp); i++) {
    g_autoptr (CbdModem3gpp) modem = g_list_model_get_item (self->modems_3gpp, i);

    if (cbd_modem_3gpp_match (modem, modem_3gpp)) {
      g_list_store_remove (G_LIST_STORE (self->modems_3gpp), i);
      return;
    }
  }
}


static void
on_mm_object_interface_added (CbdMmManager *self, GDBusInterface *interface, MMObject *object)
{
  g_assert_true (CBD_IS_MM_MANAGER (self));

  if (MM_IS_MODEM_3GPP (interface)) {
    modem_init_3gpp (self, object);
    return;
  } else if (MM_IS_MODEM_CELL_BROADCAST (interface)) {
    modem_init_cellbroadcast (self, object);
    return;
  }
}


static void
on_mm_object_interface_removed (CbdMmManager *self, GDBusInterface *interface, MMObject *object)
{
  g_assert_true (CBD_IS_MM_MANAGER (self));

  if (MM_IS_MODEM_3GPP (interface)) {
    modem_finalize_3gpp (self, object);
    return;
  } else if (MM_IS_MODEM_CELL_BROADCAST (interface)) {
    modem_finalize_cell_broadcast (self, object);
    return;
  }
}


static void
on_mm_object_added (CbdMmManager *self, GDBusObject *object)
{
  g_assert (CBD_IS_MM_MANAGER (self));
  g_assert (MM_IS_OBJECT (object));

  g_debug ("Tracking modem at: %s", mm_object_get_path (MM_OBJECT (object)));

  g_list_store_append (G_LIST_STORE (self->modems), MM_OBJECT (object));
  g_signal_connect_swapped (object,
                            "interface-added",
                            G_CALLBACK (on_mm_object_interface_added),
                            self);
  g_signal_connect_swapped (object,
                            "interface-removed",
                            G_CALLBACK (on_mm_object_interface_removed),
                            self);
  /* Coldplug interfaces */
  modem_init_3gpp (self, MM_OBJECT (object));
  modem_init_cellbroadcast (self, MM_OBJECT (object));
}


static void
on_mm_object_removed (CbdMmManager *self, GDBusObject *object)
{
  const char *path;

  g_assert (CBD_IS_MM_MANAGER (self));
  g_assert (MM_IS_OBJECT (object));

  path = g_dbus_object_get_object_path (object);
  g_debug ("Modem '%s' removed", path);

  modem_finalize_3gpp (self, MM_OBJECT (object));
  modem_finalize_cell_broadcast (self, MM_OBJECT (object));
  for (int i = 0; i < g_list_model_get_n_items (self->modems); i++) {
    MMObject *modem = g_list_model_get_item (self->modems, i);

    if (modem == MM_OBJECT (object)) {
      g_list_store_remove (G_LIST_STORE (self->modems), i);
      return;
    }
  }
}

static void
on_new_mm_manager (GDBusConnection *connection,
                   GAsyncResult    *res,
                   gpointer         user_data)
{
  CbdMmManager *self = CBD_MM_MANAGER (user_data);
  g_autoptr (GError) error = NULL;
  g_autolist (GObject) objects = NULL;

  self->mm = mm_manager_new_finish (res, &error);
  if (!self->mm) {
    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
      g_warning ("Error creating MmManager: %s", error->message);

    return;
  }

  self->mm_version = g_strdup (mm_manager_get_version (self->mm));

  g_signal_connect_swapped (self->mm,
                            "object-added",
                            G_CALLBACK (on_mm_object_added), self);
  g_signal_connect_swapped (self->mm,
                            "object-removed",
                            G_CALLBACK (on_mm_object_removed), self);
  /* Coldplug modems */
  objects = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (self->mm));
  for (GList *node = objects; node; node = node->next)
    on_mm_object_added (self, node->data);
}


static void
on_mm_appeared (GDBusConnection *connection,
                const char      *name,
                const char      *name_owner,
                gpointer         user_data)
{
  CbdMmManager *self = CBD_MM_MANAGER (user_data);

  g_debug ("ModemMmManager appeared on D-Bus");

  g_cancellable_cancel (self->cancellable);
  g_clear_object (&self->cancellable);
  self->cancellable = g_cancellable_new ();

  mm_manager_new (connection,
                  G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
                  self->cancellable,
                  (GAsyncReadyCallback) on_new_mm_manager,
                  self);
}


static void
on_mm_vanished (GDBusConnection *connection,
                const char      *name,
                gpointer         user_data)
{
  CbdMmManager *self = CBD_MM_MANAGER (user_data);

  g_debug ("ModemMmManager vanished from D-Bus");

  //g_list_free_full (self->cb_objects, g_object_unref);
  g_clear_object (&self->mm);
}


static void
cbd_mm_manager_get_property (GObject    *object,
                             guint       property_id,
                             GValue     *value,
                             GParamSpec *pspec)
{
  CbdMmManager *self = CBD_MM_MANAGER (object);

  switch (property_id) {
  case PROP_HAS_CELLBROADCAST:
    g_value_set_boolean (value, self->has_cell_broadcast);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
cbd_mm_manager_dispose (GObject *object)
{
  CbdMmManager *self = CBD_MM_MANAGER (object);

  g_clear_object (&self->modems);
  g_clear_object (&self->modems_3gpp);
  g_clear_object (&self->modems_cell_broadcast);
  g_clear_object (&self->channel_manager);

  g_clear_handle_id (&self->watch_id, g_bus_unwatch_name);
  g_clear_object (&self->cancellable);
  g_clear_object (&self->mm);

  g_clear_pointer (&self->mm_version, g_free);

  G_OBJECT_CLASS (cbd_mm_manager_parent_class)->dispose (object);
}


static void
cbd_mm_manager_class_init (CbdMmManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = cbd_mm_manager_dispose;
  object_class->get_property = cbd_mm_manager_get_property;

  /**
   * CbdMmManager:has-cellbroadcast:
   *
   * Whether we have at least one cell broadcast interface
   */
  props[PROP_HAS_CELLBROADCAST] =
    g_param_spec_boolean ("has-cellbroadcast", "", "",
                          FALSE,
                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);

  /**
   * CbdMmManager::new-cbm:
   * @body: The message body
   * @channel: The channel of the CB message
   * @message_code: The message
   * @update: The update number
   * @operator_code: The operator code at the time the CBM was received
   *
   * A new cell broadcast message (e.g. emergency alert) has just been received.
   */
  signals[NEW_CBM] =
    g_signal_new ("new-cbm",
                  CBD_TYPE_MM_MANAGER,
                  G_SIGNAL_RUN_LAST, 0,
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 5,
                  G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_STRING);
}


static void
cbd_mm_manager_init (CbdMmManager *self)
{
  self->watch_id =
    g_bus_watch_name (G_BUS_TYPE_SYSTEM,
                      MM_DBUS_SERVICE,
                      G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
                      (GBusNameAppearedCallback) on_mm_appeared,
                      (GBusNameVanishedCallback) on_mm_vanished,
                      self, NULL);

  self->channel_manager = cbd_channel_manager_new (CBD_SERVICE_PROVIDER_DATABASE);
  self->modems = G_LIST_MODEL (g_list_store_new (MM_TYPE_OBJECT));
  self->modems_3gpp = G_LIST_MODEL (g_list_store_new (CBD_TYPE_MODEM_3GPP));
  self->modems_cell_broadcast = G_LIST_MODEL (g_list_store_new (CBD_TYPE_MODEM_CELL_BROADCAST));
  g_signal_connect_object (self->modems_cell_broadcast,
                           "notify::n-items",
                           G_CALLBACK (on_cell_broadcast_n_items_changed),
                           self,
                           G_CONNECT_SWAPPED);
}


CbdMmManager *
cbd_mm_manager_new (void)
{
  return g_object_new (CBD_TYPE_MM_MANAGER, NULL);
}


CbdChannelManager *
cbd_mm_manager_get_channel_manager (CbdMmManager *self)
{
  g_assert (CBD_IS_MM_MANAGER (self));

  return self->channel_manager;
}
