/* foundry-forge-manager.c
 *
 * Copyright 2025 Christian Hergert <chergert@redhat.com>
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include <libpeas.h>

#include "foundry-debug.h"
#include "foundry-forge-project.h"
#include "foundry-intent-manager.h"
#include "foundry-model-manager.h"
#include "foundry-forge-manager.h"
#include "foundry-forge-private.h"
#include "foundry-service-private.h"
#include "foundry-settings.h"
#include "foundry-util-private.h"
#include "foundry-web-intent.h"

/**
 * FoundryForgeManager:
 *
 * Manages plugins providing [class@Foundry.Forge] implementations.
 *
 * Since: 1.1
 */

struct _FoundryForgeManager
{
  FoundryService    parent_instance;
  PeasExtensionSet *addins;
  FoundrySettings  *settings;
  char             *id;
};

struct _FoundryForgeManagerClass
{
  FoundryServiceClass parent_class;
};

static void list_model_iface_init (GListModelInterface *iface);

G_DEFINE_FINAL_TYPE_WITH_CODE (FoundryForgeManager, foundry_forge_manager, FOUNDRY_TYPE_SERVICE,
                               G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))

enum {
  PROP_0,
  PROP_FORGE,
  N_PROPS
};

static GParamSpec *properties[N_PROPS];

static void
foundry_forge_manager_added (PeasExtensionSet *set,
                             PeasPluginInfo   *plugin_info,
                             GObject          *addin,
                             gpointer          user_data)
{
  FoundryForgeManager *self = user_data;

  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (PEAS_IS_PLUGIN_INFO (plugin_info));
  g_assert (FOUNDRY_IS_FORGE (addin));
  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));

  g_debug ("Adding FoundryForge of type `%s`", G_OBJECT_TYPE_NAME (addin));

  dex_future_disown (_foundry_forge_load (FOUNDRY_FORGE (addin)));
}

static void
foundry_forge_manager_removed (PeasExtensionSet *set,
                               PeasPluginInfo   *plugin_info,
                               GObject          *addin,
                               gpointer          user_data)
{
  FoundryForgeManager *self = user_data;

  g_assert (PEAS_IS_EXTENSION_SET (set));
  g_assert (PEAS_IS_PLUGIN_INFO (plugin_info));
  g_assert (FOUNDRY_IS_FORGE (addin));
  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));

  g_debug ("Removing FoundryForge of type `%s`", G_OBJECT_TYPE_NAME (addin));

  dex_future_disown (_foundry_forge_unload (FOUNDRY_FORGE (addin)));
}

static void
foundry_forge_manager_update_action_states (FoundryForgeManager *self)
{
  g_autoptr(FoundryForge) forge = NULL;
  gboolean enabled;

  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));

  forge = foundry_forge_manager_dup_forge (self);
  enabled = (forge != NULL);

  foundry_service_action_set_enabled (FOUNDRY_SERVICE (self), "navigate-to-issues", enabled);
  foundry_service_action_set_enabled (FOUNDRY_SERVICE (self), "navigate-to-merge-requests", enabled);
  foundry_service_action_set_enabled (FOUNDRY_SERVICE (self), "navigate-to-project", enabled);
}

static DexFuture *
foundry_forge_manager_start_fiber (gpointer user_data)
{
  FoundryForgeManager *self = user_data;
  g_autoptr(GPtrArray) futures = NULL;
  guint n_items;

  g_assert (FOUNDRY_IS_MAIN_THREAD ());
  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));
  g_assert (PEAS_IS_EXTENSION_SET (self->addins));

  g_signal_connect_object (self->addins,
                           "extension-added",
                           G_CALLBACK (foundry_forge_manager_added),
                           self,
                           0);
  g_signal_connect_object (self->addins,
                           "extension-removed",
                           G_CALLBACK (foundry_forge_manager_removed),
                           self,
                           0);
  g_signal_connect_object (self->addins,
                           "items-changed",
                           G_CALLBACK (g_list_model_items_changed),
                           self,
                           G_CONNECT_SWAPPED);

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->addins));
  futures = g_ptr_array_new_with_free_func (dex_unref);

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(FoundryForge) forge = g_list_model_get_item (G_LIST_MODEL (self->addins), i);
      g_ptr_array_add (futures, _foundry_forge_load (forge));
    }

  g_list_model_items_changed (G_LIST_MODEL (self), 0, 0, n_items);

  if (futures->len > 0)
    dex_await (foundry_future_all (futures), NULL);

  foundry_forge_manager_update_action_states (self);

  return dex_future_new_true ();
}

static DexFuture *
foundry_forge_manager_start (FoundryService *service)
{
  FoundryForgeManager *self = (FoundryForgeManager *)service;
  g_autoptr(FoundryContext) context = NULL;
  g_autofree char *id = NULL;

  g_assert (FOUNDRY_IS_MAIN_THREAD ());
  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));
  g_assert (PEAS_IS_EXTENSION_SET (self->addins));

  context = foundry_contextual_dup_context (FOUNDRY_CONTEXTUAL (self));

  self->settings = foundry_context_load_settings (context, "org.gnome.foundry.forge", NULL);

  id = foundry_settings_get_string (self->settings, "forge");
  if (!foundry_str_empty0 (id))
    g_set_str (&self->id, id);

  return dex_scheduler_spawn (NULL, 0,
                              foundry_forge_manager_start_fiber,
                              g_object_ref (self),
                              g_object_unref);
}

static DexFuture *
foundry_forge_manager_stop (FoundryService *service)
{
  FoundryForgeManager *self = (FoundryForgeManager *)service;
  g_autoptr(GPtrArray) futures = NULL;
  guint n_items;

  g_assert (FOUNDRY_IS_MAIN_THREAD ());
  g_assert (FOUNDRY_IS_SERVICE (service));

  g_signal_handlers_disconnect_by_func (self->addins,
                                        G_CALLBACK (foundry_forge_manager_added),
                                        self);
  g_signal_handlers_disconnect_by_func (self->addins,
                                        G_CALLBACK (foundry_forge_manager_removed),
                                        self);

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->addins));
  futures = g_ptr_array_new_with_free_func (dex_unref);

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(FoundryForge) forge = g_list_model_get_item (G_LIST_MODEL (self->addins), i);
      g_ptr_array_add (futures, _foundry_forge_unload (forge));
    }

  g_list_model_items_changed (G_LIST_MODEL (self), 0, n_items, 0);

  g_clear_object (&self->addins);
  g_clear_object (&self->settings);

  if (futures->len > 0)
    return foundry_future_all (futures);

  return dex_future_new_true ();
}

static void
foundry_forge_manager_constructed (GObject *object)
{
  FoundryForgeManager *self = (FoundryForgeManager *)object;
  g_autoptr(FoundryContext) context = NULL;

  G_OBJECT_CLASS (foundry_forge_manager_parent_class)->constructed (object);

  context = foundry_contextual_dup_context (FOUNDRY_CONTEXTUAL (self));

  self->addins = peas_extension_set_new (NULL,
                                         FOUNDRY_TYPE_FORGE,
                                         "context", context,
                                         NULL);
}

static void
foundry_forge_manager_finalize (GObject *object)
{
  FoundryForgeManager *self = (FoundryForgeManager *)object;

  g_clear_object (&self->addins);
  g_clear_pointer (&self->id, g_free);

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

static DexFuture *
foundry_forge_manager_navigate_fiber (FoundryForgeManager *self,
                                      const char          *property_name)
{
  g_autoptr(FoundryForge) forge = NULL;
  g_autoptr(FoundryForgeProject) project = NULL;
  g_autoptr(FoundryIntentManager) intent_manager = NULL;
  g_autoptr(FoundryContext) context = NULL;
  g_autoptr(FoundryIntent) intent = NULL;
  g_autofree char *url = NULL;
  g_autoptr(GError) error = NULL;

  g_assert (FOUNDRY_IS_MAIN_THREAD ());
  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));

  if (!(forge = foundry_forge_manager_dup_forge (self)))
    return dex_future_new_reject (G_IO_ERROR,
                                  G_IO_ERROR_NOT_FOUND,
                                  "No forge configured");

  if (!(project = dex_await_object (foundry_forge_find_project (forge), &error)))
    return dex_future_new_for_error (g_steal_pointer (&error));

  if (g_strcmp0 (property_name, "issues-url") == 0)
    url = foundry_forge_project_dup_issues_url (project);
  else if (g_strcmp0 (property_name, "merge-requests-url") == 0)
    url = foundry_forge_project_dup_merge_requests_url (project);
  else if (g_strcmp0 (property_name, "online-url") == 0)
    url = foundry_forge_project_dup_online_url (project);

  if (!url)
    return dex_future_new_reject (G_IO_ERROR,
                                  G_IO_ERROR_NOT_SUPPORTED,
                                  "Project does not support `%s`",
                                  property_name);

  context = foundry_contextual_dup_context (FOUNDRY_CONTEXTUAL (self));

  if (!(intent_manager = foundry_context_dup_intent_manager (context)))
    return foundry_future_new_not_supported ();

  intent = foundry_web_intent_new (url);

  return foundry_intent_manager_dispatch (intent_manager, intent);
}

static void
foundry_forge_manager_navigate_to_issues_action (FoundryService *service,
                                                 const char     *action_name,
                                                 GVariant       *param)
{
  FoundryForgeManager *self = (FoundryForgeManager *)service;

  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));

  dex_future_disown (foundry_scheduler_spawn (NULL, 0,
                                              G_CALLBACK (foundry_forge_manager_navigate_fiber),
                                              2,
                                              FOUNDRY_TYPE_FORGE_MANAGER, self,
                                              G_TYPE_STRING, "issues-url"));
}

static void
foundry_forge_manager_navigate_to_project_action (FoundryService *service,
                                                  const char     *action_name,
                                                  GVariant       *param)
{
  FoundryForgeManager *self = (FoundryForgeManager *)service;

  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));

  dex_future_disown (foundry_scheduler_spawn (NULL, 0,
                                              G_CALLBACK (foundry_forge_manager_navigate_fiber),
                                              2,
                                              FOUNDRY_TYPE_FORGE_MANAGER, self,
                                              G_TYPE_STRING, "online-url"));
}

static void
foundry_forge_manager_navigate_to_merge_requests_action (FoundryService *service,
                                                         const char     *action_name,
                                                         GVariant       *param)
{
  FoundryForgeManager *self = (FoundryForgeManager *)service;

  g_assert (FOUNDRY_IS_FORGE_MANAGER (self));

  dex_future_disown (foundry_scheduler_spawn (NULL, 0,
                                              G_CALLBACK (foundry_forge_manager_navigate_fiber),
                                              2,
                                              FOUNDRY_TYPE_FORGE_MANAGER, self,
                                              G_TYPE_STRING, "merge-requests-url"));
}

static void
foundry_forge_manager_get_property (GObject    *object,
                                    guint       prop_id,
                                    GValue     *value,
                                    GParamSpec *pspec)
{
  FoundryForgeManager *self = FOUNDRY_FORGE_MANAGER (object);

  switch (prop_id)
    {
    case PROP_FORGE:
      g_value_take_object (value, foundry_forge_manager_dup_forge (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
foundry_forge_manager_class_init (FoundryForgeManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  FoundryServiceClass *service_class = FOUNDRY_SERVICE_CLASS (klass);

  object_class->constructed = foundry_forge_manager_constructed;
  object_class->finalize = foundry_forge_manager_finalize;
  object_class->get_property = foundry_forge_manager_get_property;

  service_class->start = foundry_forge_manager_start;
  service_class->stop = foundry_forge_manager_stop;

  foundry_service_class_set_action_prefix (service_class, "forge");
  foundry_service_class_install_action (service_class, "navigate-to-issues", NULL,
                                        foundry_forge_manager_navigate_to_issues_action);
  foundry_service_class_install_action (service_class, "navigate-to-merge-requests", NULL,
                                        foundry_forge_manager_navigate_to_merge_requests_action);
  foundry_service_class_install_action (service_class, "navigate-to-project", NULL,
                                        foundry_forge_manager_navigate_to_project_action);

  properties[PROP_FORGE] =
    g_param_spec_object ("forge", NULL, NULL,
                         FOUNDRY_TYPE_FORGE,
                         (G_PARAM_READABLE |
                          G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
foundry_forge_manager_init (FoundryForgeManager *self)
{
}

static GType
foundry_forge_manager_get_item_type (GListModel *model)
{
  return FOUNDRY_TYPE_FORGE;
}

static guint
foundry_forge_manager_get_n_items (GListModel *model)
{
  FoundryForgeManager *self = FOUNDRY_FORGE_MANAGER (model);

  if (self->addins == NULL)
    return 0;

  return g_list_model_get_n_items (G_LIST_MODEL (self->addins));
}

static gpointer
foundry_forge_manager_get_item (GListModel *model,
                                guint       position)
{
  FoundryForgeManager *self = FOUNDRY_FORGE_MANAGER (model);

  if (self->addins == NULL)
    return NULL;

  return g_list_model_get_item (G_LIST_MODEL (self->addins), position);
}

static void
list_model_iface_init (GListModelInterface *iface)
{
  iface->get_item_type = foundry_forge_manager_get_item_type;
  iface->get_n_items = foundry_forge_manager_get_n_items;
  iface->get_item = foundry_forge_manager_get_item;
}

/**
 * foundry_forge_manager_find_by_id:
 * @self: a [class@Foundry.ForgeManager]
 * @forge_id: the id of the forge
 *
 * Returns: (transfer full) (nullable):
 *
 * Since: 1.1
 */
FoundryForge *
foundry_forge_manager_find_by_id (FoundryForgeManager *self,
                                  const char          *forge_id)
{
  guint n_items;

  g_return_val_if_fail (FOUNDRY_IS_FORGE_MANAGER (self), NULL);

  if (forge_id == NULL || self->addins == NULL)
    return NULL;

  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->addins));

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(FoundryForge) forge = g_list_model_get_item (G_LIST_MODEL (self->addins), i);
      g_autoptr(PeasPluginInfo) info = foundry_forge_dup_plugin_info (forge);
      const char *module_name = peas_plugin_info_get_module_name (info);

      if (g_strcmp0 (module_name, forge_id) == 0)
        return g_steal_pointer (&forge);
    }

  return NULL;
}

/**
 * foundry_forge_manager_dup_forge:
 * @self: a [class@Foundry.ForgeManager]
 *
 * Returns: (transfer full) (nullable):
 *
 * Since: 1.1
 */
FoundryForge *
foundry_forge_manager_dup_forge (FoundryForgeManager *self)
{
  g_return_val_if_fail (FOUNDRY_IS_FORGE_MANAGER (self), NULL);

  return foundry_forge_manager_find_by_id (self, self->id);
}

/**
 * foundry_forge_manager_set_forge:
 * @self: a [class@Foundry.ForgeManager]
 * @forge: (nullable): a [class@Foundry.Forge]
 *
 * Since: 1.1
 */
void
foundry_forge_manager_set_forge (FoundryForgeManager *self,
                                 FoundryForge        *forge)
{
  g_autofree char *id = NULL;

  g_return_if_fail (FOUNDRY_IS_FORGE_MANAGER (self));
  g_return_if_fail (!forge || FOUNDRY_IS_FORGE (forge));

  if (forge != NULL)
    {
      g_autoptr(PeasPluginInfo) info = foundry_forge_dup_plugin_info (forge);

      if (info != NULL)
        id = g_strdup (peas_plugin_info_get_module_name (info));
    }

  if (g_set_str (&self->id, id))
    {
      foundry_settings_set_string (self->settings, "forge", id ? id : "");
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FORGE]);
      foundry_forge_manager_update_action_states (self);
    }
}
