/*
  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"
#include "anastasis_api_redux_state.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;

  /**
   * State all our entries share.  We own it until the first entry
   * reports back, at which point it is handed to @e cb.
   */
  struct ANASTASIS_ReduxState *rs;

};


/**
 * 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);
  }
  ANASTASIS_REDUX_state_free_ (ms->rs);
  GNUNET_free (ms);
}


/**
 * Function called when we have made progress on any of the
 * providers we are trying to sync with.  The first one to report wins:
 * we return its result and drop the rest.
 *
 * @param cls closure
 * @param error error code, #TALER_EC_NONE if the state is the new successful state
 * @param[in] rs the state, updated for the provider that reported
 */
static void
sync_progress (void *cls,
               enum TALER_ErrorCode error,
               struct ANASTASIS_ReduxState *rs)
{
  struct SyncEntry *se = cls;
  struct MasterSync *ms = se->ms;
  ANASTASIS_ActionCallback cb = ms->cb;
  void *cb_cls = ms->cb_cls;

  GNUNET_assert (rs == ms->rs);
  GNUNET_CONTAINER_DLL_remove (ms->se_head,
                               ms->se_tail,
                               se);
  GNUNET_free (se);
  /* The remaining entries are cancelled below; none of them touches the
     state once it has been unlinked, so handing it over here is safe. */
  ms->rs = NULL;
  clean_sync (ms);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           error);
}


struct ANASTASIS_ReduxAction *
ANASTASIS_REDUX_sync_providers_ (struct ANASTASIS_ReduxState *rs,
                                 const json_t *arguments,
                                 ANASTASIS_ActionCallback cb,
                                 void *cb_cls)
{
  const struct ANASTASIS_RecoveryInformation *ri;
  struct MasterSync *ms;

  if (NULL == rs->details.recovery.r)
  {
    GNUNET_break (0);
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_document' missing");
    return NULL;
  }
  ri = ANASTASIS_recovery_get_info (rs->details.recovery.r);
  ms = GNUNET_new (struct MasterSync);
  ms->cb = cb;
  ms->cb_cls = cb_cls;
  ms->rs = rs;
  for (unsigned int i = 0; i < ri->cs_len; i++)
  {
    const struct ANASTASIS_ChallengeDetails *cd;
    struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
    struct SyncEntry *se;

    cd = ANASTASIS_challenge_get_details (ri->cs[i]);
    if (GNUNET_OK ==
        ANASTASIS_REDUX_lookup_salt_ (&rs->common,
                                      cd->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_ (cd->provider_url,
                                                     rs,
                                                     &sync_progress,
                                                     se);
  }
  if (NULL == ms->se_head)
  {
    /* everything already synced */
    ms->rs = NULL;
    clean_sync (ms);
    ANASTASIS_REDUX_fail_ (rs,
                           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_ (struct ANASTASIS_ReduxState *rs,
                                 const json_t *arguments,
                                 ANASTASIS_ActionCallback cb,
                                 void *cb_cls)
{
  struct MasterSync *ms;

  if (! rs->common.have_providers)
  {
    GNUNET_break (0);
    ANASTASIS_REDUX_fail_ (rs,
                           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;
  ms->rs = rs;
  for (unsigned int i = 0; i < rs->common.providers_len; i++)
  {
    const char *url = rs->common.providers[i].url.url;
    struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
    struct SyncEntry *se;

    if (GNUNET_OK ==
        ANASTASIS_REDUX_lookup_salt_ (&rs->common,
                                      url,
                                      &provider_salt))
      continue;
    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_ (url,
                                                     rs,
                                                     &sync_progress,
                                                     se);
  }
  if (NULL == ms->se_head)
  {
    /* everything already synced */
    ms->rs = NULL;
    clean_sync (ms);
    ANASTASIS_REDUX_fail_ (rs,
                           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;
}
