/* foundry-secret-service.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 <libdex.h>
#include <libpeas.h>
#include <libsecret/secret.h>

#include "foundry-key-rotator.h"
#include "foundry-secret-private.h"
#include "foundry-secret-service.h"
#include "foundry-service-private.h"
#include "foundry-util.h"

/**
 * FoundrySecretService:
 *
 * Manages secure storage and retrieval of API keys and sensitive data.
 *
 * FoundrySecretService provides a secure interface for storing and retrieving
 * API keys, authentication tokens, and other sensitive information. It integrates
 * with the system's secret storage backend and provides a unified API for
 * managing credentials across different services and platforms.
 */

typedef struct {
  char *host;
  char *service;
} FoundrySecretHostService;

#define FOUNDRY_SECRET_SCHEMA_NAME       "org.gnome.foundry.secret.api-key"
#define FOUNDRY_SECRET_ATTRIBUTE_HOST    "host"
#define FOUNDRY_SECRET_ATTRIBUTE_SERVICE "service"

static SecretSchema foundry_api_key_schema = {
  .name = FOUNDRY_SECRET_SCHEMA_NAME,
  .flags = SECRET_SCHEMA_DONT_MATCH_NAME,
  .attributes = {
    { FOUNDRY_SECRET_ATTRIBUTE_HOST, SECRET_SCHEMA_ATTRIBUTE_STRING },
    { FOUNDRY_SECRET_ATTRIBUTE_SERVICE, SECRET_SCHEMA_ATTRIBUTE_STRING },
    { NULL, 0 }
  }
};

struct _FoundrySecretService
{
  FoundryService parent_instance;
};

struct _FoundrySecretServiceClass
{
  FoundryServiceClass parent_class;
};

G_DEFINE_FINAL_TYPE (FoundrySecretService, foundry_secret_service, FOUNDRY_TYPE_SERVICE)

static void
foundry_secret_service_class_init (FoundrySecretServiceClass *klass)
{
}

static void
foundry_secret_service_init (FoundrySecretService *self)
{
}

static GHashTable *
create_attributes (const char *host,
                   const char *service)
{
  GHashTable *attributes;

  attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
  g_hash_table_replace (attributes,
                        g_strdup (FOUNDRY_SECRET_ATTRIBUTE_HOST),
                        g_strdup (host));
  g_hash_table_replace (attributes,
                        g_strdup (FOUNDRY_SECRET_ATTRIBUTE_SERVICE),
                        g_strdup (service));

  return attributes;
}

/**
 * foundry_secret_service_store_api_key:
 * @self: a [class@Foundry.SecretService]
 * @host: the hostname of the api server
 * @service: the name of the service such as "gitlab"
 * @api_key: the actual API key
 *
 * Stores an API key in secret storage.
 *
 * Returns: (transfer full): a [class@Dex.Future] that resolves to
 *   any value on success or rejects with error.
 *
 * Since: 1.1
 */
DexFuture *
foundry_secret_service_store_api_key (FoundrySecretService *self,
                                      const char           *host,
                                      const char           *service,
                                      const char           *api_key)
{
  g_return_val_if_fail (FOUNDRY_IS_SECRET_SERVICE (self), NULL);
  g_return_val_if_fail (host != NULL, NULL);
  g_return_val_if_fail (service != NULL, NULL);
  g_return_val_if_fail (api_key != NULL, NULL);

  return foundry_secret_password_storev (&foundry_api_key_schema,
                                         create_attributes (host, service),
                                         SECRET_COLLECTION_DEFAULT,
                                         "Foundry API Key",
                                         api_key);
}

/**
 * foundry_secret_service_lookup_api_key:
 * @self: a [class@Foundry.SecretService]
 * @host: the hostname of the api server
 * @service: the name of the service such as "gitlab"
 *
 * Retrieves an API key from secret storage.
 *
 * Returns: (transfer full): a [class@Dex.Future] that resolves to
 *   a string on success or rejects with error.
 *
 * Since: 1.1
 */
DexFuture *
foundry_secret_service_lookup_api_key (FoundrySecretService *self,
                                       const char           *host,
                                       const char           *service)
{
  g_return_val_if_fail (FOUNDRY_IS_SECRET_SERVICE (self), NULL);
  g_return_val_if_fail (host != NULL, NULL);
  g_return_val_if_fail (service != NULL, NULL);

  return foundry_secret_password_lookupv (&foundry_api_key_schema,
                                          create_attributes (host, service));
}

/**
 * foundry_secret_service_delete_api_key:
 * @self: a [class@Foundry.SecretService]
 * @host: the hostname of the api server
 * @service: the name of the service such as "gitlab"
 *
 * Removes an API key from secret storage.
 *
 * Returns: (transfer full): a [class@Dex.Future] that resolves to
 *   any value on success or rejects with error.
 *
 * Since: 1.1
 */
DexFuture *
foundry_secret_service_delete_api_key (FoundrySecretService *self,
                                       const char           *host,
                                       const char           *service)
{
  g_return_val_if_fail (FOUNDRY_IS_SECRET_SERVICE (self), NULL);
  g_return_val_if_fail (host != NULL, NULL);
  g_return_val_if_fail (service != NULL, NULL);

  return foundry_secret_password_clearv (&foundry_api_key_schema,
                                         create_attributes (host, service));
}

static DexFuture *
foundry_secret_service_rotate_api_key_fiber (FoundrySecretService *self,
                                             const char           *host,
                                             const char           *service,
                                             GDateTime            *expire_at)
{
  g_autoptr(PeasExtensionSet) addins = NULL;
  g_autoptr(FoundryContext) context = NULL;
  g_autoptr(GError) error = NULL;
  g_autofree char *secret = NULL;
  guint n_items;

  g_assert (FOUNDRY_IS_SECRET_SERVICE (self));
  g_assert (host != NULL);
  g_assert (service != NULL);

  if (!(context = foundry_contextual_acquire (FOUNDRY_CONTEXTUAL (self), &error)))
    return dex_future_new_for_error (g_steal_pointer (&error));

  secret = dex_await_string (foundry_secret_service_lookup_api_key (self, host, service), &error);

  if (error != NULL)
    return dex_future_new_for_error (g_steal_pointer (&error));

  if (secret == NULL)
    return dex_future_new_reject (G_IO_ERROR,
                                  G_IO_ERROR_NOT_FOUND,
                                  "No API key found for %s on %s",
                                  service, host);

  addins = peas_extension_set_new (peas_engine_get_default (),
                                   FOUNDRY_TYPE_KEY_ROTATOR,
                                   "context", context,
                                   NULL);

  n_items = g_list_model_get_n_items (G_LIST_MODEL (addins));

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(FoundryKeyRotator) rotator = g_list_model_get_item (G_LIST_MODEL (addins), i);
      g_autofree char *new_secret = NULL;

      if (!foundry_key_rotator_can_rotate (rotator, host, service, secret))
        continue;

      if (!(new_secret = dex_await_string (foundry_key_rotator_rotate (rotator, host, service, secret, expire_at), &error)))
        return dex_future_new_for_error (g_steal_pointer (&error));

      if (!dex_await (foundry_secret_service_store_api_key (self, host, service, new_secret), &error))
        return dex_future_new_for_error (g_steal_pointer (&error));

      return dex_future_new_take_string (g_steal_pointer (&new_secret));
    }

  return foundry_future_new_not_supported ();
}

/**
 * foundry_secret_service_rotate_api_key:
 * @self: a [class@Foundry.SecretService]
 * @host: the hostname of the api server
 * @service: the name of the service such as "gitlab"
 * @expire_at: (nullable): when the new key should expire, or %NULL
 *
 * Rotates the API key for the host/service pair.
 *
 * Returns: (transfer full): a [class@Dex.Future] that resolves to
 *   the new API key as a string on success; or rejects with error.
 *
 * Since: 1.1
 */
DexFuture *
foundry_secret_service_rotate_api_key (FoundrySecretService *self,
                                       const char           *host,
                                       const char           *service,
                                       GDateTime            *expire_at)
{
  dex_return_error_if_fail (FOUNDRY_IS_SECRET_SERVICE (self));
  dex_return_error_if_fail (host != NULL);
  dex_return_error_if_fail (service != NULL);

  return FOUNDRY_SCHEDULER_SPAWN (NULL, 0,
                                  foundry_secret_service_rotate_api_key_fiber,
                                  4,
                                  FOUNDRY_TYPE_SECRET_SERVICE, self,
                                  G_TYPE_STRING, host,
                                  G_TYPE_STRING, service,
                                  G_TYPE_DATE_TIME, expire_at);
}

static DexFuture *
foundry_secret_service_check_expires_at_fiber (FoundrySecretService *self,
                                               const char           *host,
                                               const char           *service)
{
  g_autoptr(PeasExtensionSet) addins = NULL;
  g_autoptr(FoundryContext) context = NULL;
  g_autoptr(GError) error = NULL;
  g_autofree char *secret = NULL;
  guint n_items;

  g_assert (FOUNDRY_IS_SECRET_SERVICE (self));
  g_assert (host != NULL);
  g_assert (service != NULL);

  if (!(context = foundry_contextual_acquire (FOUNDRY_CONTEXTUAL (self), &error)))
    return dex_future_new_for_error (g_steal_pointer (&error));

  secret = dex_await_string (foundry_secret_service_lookup_api_key (self, host, service), &error);

  if (error != NULL)
    return dex_future_new_for_error (g_steal_pointer (&error));

  if (secret == NULL)
    return dex_future_new_reject (G_IO_ERROR,
                                  G_IO_ERROR_NOT_FOUND,
                                  "No API key found for %s on %s",
                                  service, host);

  addins = peas_extension_set_new (peas_engine_get_default (),
                                   FOUNDRY_TYPE_KEY_ROTATOR,
                                   "context", context,
                                   NULL);

  n_items = g_list_model_get_n_items (G_LIST_MODEL (addins));

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr(FoundryKeyRotator) rotator = g_list_model_get_item (G_LIST_MODEL (addins), i);
      g_autoptr(GDateTime) expires_at = NULL;

      if (!foundry_key_rotator_can_rotate (rotator, host, service, secret))
        continue;

      if (!(expires_at = dex_await_boxed (foundry_key_rotator_check_expires_at (rotator, host, service, secret), &error)))
        return dex_future_new_for_error (g_steal_pointer (&error));

      if (expires_at != NULL)
        return dex_future_new_take_boxed (G_TYPE_DATE_TIME, g_steal_pointer (&expires_at));
    }

  return foundry_future_new_not_supported ();
}

/**
 * foundry_secret_service_check_expires_at:
 * @self: a [class@Foundry.SecretService]
 * @host: the hostname of the api server
 * @service: the name of the service such as "gitlab"
 *
 * Checks when the API key expires for the host/service pair.
 *
 * Returns: (transfer full): a [class@Dex.Future] that resolves to
 *   a GDateTime on success; or rejects with error.
 *
 * Since: 1.1
 */
DexFuture *
foundry_secret_service_check_expires_at (FoundrySecretService *self,
                                         const char           *host,
                                         const char           *service)
{
  dex_return_error_if_fail (FOUNDRY_IS_SECRET_SERVICE (self));
  dex_return_error_if_fail (host != NULL);
  dex_return_error_if_fail (service != NULL);

  return FOUNDRY_SCHEDULER_SPAWN (NULL, 0,
                                  foundry_secret_service_check_expires_at_fiber,
                                  3,
                                  FOUNDRY_TYPE_SECRET_SERVICE, self,
                                  G_TYPE_STRING, host,
                                  G_TYPE_STRING, service);
}
