/*
  This file is part of Anastasis
  Copyright (C) 2020, 2021, 2022 Anastasis SARL

  Anastasis 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, or (at your option) any later version.

  Anastasis 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
  Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file reducer/anastasis_api_providers.c
 * @brief anastasis provider synchronization logic
 * @author Christian Grothoff
 */

#include <platform.h>
#include <jansson.h>
#include "anastasis_redux.h"
#include "anastasis_error_codes.h"
#include "anastasis_api_redux.h"


/**
 * Main data structure for sync_providers().
 */
struct MasterSync;


/**
 * Data structure for one provider we are syncing /config with.
 */
struct SyncEntry
{
  /**
   * Kept in a DLL.
   */
  struct SyncEntry *next;

  /**
   * Kept in a DLL.
   */
  struct SyncEntry *prev;

  /**
   * Sync operation we are part of.
   */
  struct MasterSync *ms;

  /**
   * Redux action for this provider.
   */
  struct ANASTASIS_ReduxAction *ra;
};


/**
 * Main data structure for sync_providers().
 */
struct MasterSync
{
  /**
   * Our own sync action we expose externally.
   */
  struct ANASTASIS_ReduxAction ra;
  /**
   * Head of DLL with entries per provider.
   */
  struct SyncEntry *se_head;
  /**
   * Tail of DLL with entries per provider.
   */
  struct SyncEntry *se_tail;

  /**
   * Function to call with the result.
   */
  ANASTASIS_ActionCallback cb;

  /**
   * Closure for @e cb.
   */
  void *cb_cls;

};


/**
 * Free @a cls data structure.
 *
 * @param[in] cls data structure to free, must be a `struct MasterSync *`
 */
static void
clean_sync (void *cls)
{
  struct MasterSync *ms = cls;
  struct SyncEntry *se;

  while (NULL != (se = ms->se_head))
  {
    GNUNET_CONTAINER_DLL_remove (ms->se_head,
                                 ms->se_tail,
                                 se);
    se->ra->cleanup (se->ra->cleanup_cls);
    GNUNET_free (se);
  }
  GNUNET_free (ms);
}


/**
 * Function called when we have made progress on any of the
 * providers we are trying to sync with.
 *
 * @param cls closure
 * @param error error code, #TALER_EC_NONE if @a new_bs is the new successful state
 * @param new_state the new state of the operation (client should json_incref() to keep an alias)
 */
static void
sync_progress (void *cls,
               enum TALER_ErrorCode error,
               json_t *new_state)
{
  struct SyncEntry *se = cls;
  struct MasterSync *ms = se->ms;

  GNUNET_CONTAINER_DLL_remove (ms->se_head,
                               ms->se_tail,
                               se);
  GNUNET_free (se);
  ms->cb (ms->cb_cls,
          error,
          new_state);
  clean_sync (ms);
}


struct ANASTASIS_ReduxAction *
ANASTASIS_REDUX_sync_providers_ (json_t *state,
                                 const json_t *arguments,
                                 ANASTASIS_ActionCallback cb,
                                 void *cb_cls)
{
  json_t *rd;
  json_t *cs_arr;
  struct MasterSync *ms;

  rd = json_object_get (state,
                        "recovery_document");
  if (NULL == rd)
  {
    GNUNET_break (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_document' missing");
    return NULL;
  }
  cs_arr = json_object_get (rd,
                            "challenges");
  if (! json_is_array (cs_arr))
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_document' must be an array");
    return NULL;
  }
  ms = GNUNET_new (struct MasterSync);
  ms->cb = cb;
  ms->cb_cls = cb_cls;
  {
    json_t *cs;
    unsigned int n_index;

    json_array_foreach (cs_arr, n_index, cs)
    {
      const char *provider_url;
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_string ("url",
                                 &provider_url),
        GNUNET_JSON_spec_end ()
      };
      struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
      struct SyncEntry *se;

      if (GNUNET_OK !=
          GNUNET_JSON_parse (cs,
                             spec,
                             NULL, NULL))
      {
        GNUNET_break_op (0);
        ANASTASIS_redux_fail_ (cb,
                               cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                               "'recovery_document' missing");
        clean_sync (ms);
        return NULL;
      }
      if (GNUNET_OK ==
          ANASTASIS_reducer_lookup_salt (state,
                                         provider_url,
                                         &provider_salt))
        continue; /* provider already ready */
      se = GNUNET_new (struct SyncEntry);
      se->ms = ms;
      GNUNET_CONTAINER_DLL_insert (ms->se_head,
                                   ms->se_tail,
                                   se);
      se->ra = ANASTASIS_REDUX_add_provider_to_state_ (provider_url,
                                                       state,
                                                       &sync_progress,
                                                       se);
    }
  }
  if (NULL == ms->se_head)
  {
    /* everything already synced */
    clean_sync (ms);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED,
                           "already in sync");
    return NULL;
  }
  ms->ra.cleanup = &clean_sync;
  ms->ra.cleanup_cls = ms;
  return &ms->ra;
}


struct ANASTASIS_ReduxAction *
ANASTASIS_REDUX_poll_providers_ (json_t *state,
                                 const json_t *arguments,
                                 ANASTASIS_ActionCallback cb,
                                 void *cb_cls)
{
  json_t *ap;
  const char *url;
  json_t *obj;
  struct MasterSync *ms;

  ap = json_object_get (state,
                        "authentication_providers");
  if (NULL == ap)
  {
    GNUNET_break (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'authentication_providers' missing");
    return NULL;
  }
  ms = GNUNET_new (struct MasterSync);
  ms->cb = cb;
  ms->cb_cls = cb_cls;
  json_object_foreach (ap, url, obj)
  {
    struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
    struct SyncEntry *se;
    struct ANASTASIS_ReduxAction *ra;

    if (GNUNET_OK ==
        ANASTASIS_reducer_lookup_salt (state,
                                       url,
                                       &provider_salt))
      continue;
    se = GNUNET_new (struct SyncEntry);
    se->ms = ms;
    GNUNET_CONTAINER_DLL_insert (ms->se_head,
                                 ms->se_tail,
                                 se);
    ra = ANASTASIS_REDUX_add_provider_to_state_ (url,
                                                 state,
                                                 &sync_progress,
                                                 se);
    if (NULL == ra)
      return NULL; /* sync_progress already called! */
    se->ra = ra;
  }
  if (NULL == ms->se_head)
  {
    /* everything already synced */
    clean_sync (ms);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID,
                           "already in sync");
    return NULL;
  }
  ms->ra.cleanup = &clean_sync;
  ms->ra.cleanup_cls = ms;
  return &ms->ra;
}
