/*
  This file is part of Anastasis
  Copyright (C) 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_discovery.c
 * @brief anastasis recovery policy discovery api
 * @author Christian Grothoff
 */
#include <platform.h>
#include <jansson.h>
#include "anastasis_redux.h"
#include "anastasis_error_codes.h"
#include <taler/taler_json_lib.h>
#include "anastasis_api_redux.h"
#include "anastasis_api_redux_state.h"


/**
 * Maximum number of *optional* identity attributes we are willing to
 * enumerate subsets of in #ANASTASIS_policy_discovery_start().  The mask
 * loop there is 2^n, so this bounds the work at 65536 combinations; it also
 * keeps `1LLU << opt_cnt` well-defined (it is undefined for n >= 64) and the
 * 64-bit loop counter from wrapping.
 */
#define MAX_OPTIONAL_ATTRIBUTES 16


/**
 * Handle for one request we are doing at a specific provider.
 */
struct ProviderOperation
{
  /**
   * Kept in a DLL.
   */
  struct ProviderOperation *next;

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

  /**
   * Base URL of the provider.
   */
  char *provider_url;

  /**
   * Handle to the version check operation we are performing.
   */
  struct ANASTASIS_VersionCheck *vc;

  /**
   * Handle discovery operation we this is a part of.
   */
  struct ANASTASIS_PolicyDiscovery *pd;

  /**
   * Attribute mask applied to the identity attributes
   * for this operation.
   */
  struct ANASTASIS_AttributeMask attribute_mask;
};


/**
 * Handle for a discovery operation.
 */
struct ANASTASIS_PolicyDiscovery
{
  /**
   * Head of HTTP requests, kept in a DLL.
   */
  struct ProviderOperation *po_head;

  /**
   * Tail of HTTP requests, kept in a DLL.
   */
  struct ProviderOperation *po_tail;

  /**
   * Function to call with results.
   */
  ANASTASIS_PolicyDiscoveryCallback cb;

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

  /**
   * Map for duplicate detection, maps hashes of policies we
   * have already seen to a json_array with all providers
   * and versions corresponding to this policy hash.
   */
  struct GNUNET_CONTAINER_MultiHashMap *dd_map;

  /**
   * State we are operating on.
   */
  struct ANASTASIS_ReduxState *rs;

  /**
   * Number of optional fields in our identity attributes.
   */
  unsigned int opt_cnt;
};


/**
 * Return a copy of @a master_id with the optional attributes selected
 * by @a mask removed.  The internal, typed counterpart of
 * #ANASTASIS_mask_id_data().
 *
 * @param common state describing which attributes exist
 * @param master_id the user's identity attributes
 * @param mask which of the optional attributes to drop
 * @return NULL if @a master_id does not match @a common
 */
static json_t *
mask_id_data (const struct ANASTASIS_ReduxCommon *common,
              const json_t *master_id,
              struct ANASTASIS_AttributeMask mask)
{
  json_t *ret = json_deep_copy (master_id);
  unsigned int bit = 0;

  GNUNET_assert (NULL != ret);
  for (unsigned int i = 0; i < common->required_attributes_len; i++)
  {
    const struct ANASTASIS_ReduxAttributeSpec *a
      = &common->required_attributes[i];
    bool present;

    present = (NULL !=
               json_object_get (master_id,
                                a->name));
    if ( (! present) && (! a->optional) )
    {
      GNUNET_break (0);
      json_decref (ret);
      return NULL;
    }
    if (present && a->optional)
    {
      if (0 != ((1LLU << bit) & mask.mask))
      {
        GNUNET_assert (0 ==
                       json_object_del (ret,
                                        a->name));
      }
      bit++;
    }
  }
  return ret;
}


/**
 * Callback which passes back meta data about one of the
 * recovery documents available at the provider.
 *
 * @param cls our `struct ProviderOperation *`
 * @param version version number of the policy document,
 *                0 for the end of the list
 * @param server_time time of the backup at the provider
 * @param recdoc_id hash of the compressed recovery document, uniquely
 *                  identifies the document; NULL for the end of the list
 * @param secret_name name of the secret as chosen by the user,
 *                  or NULL if the user did not provide a name
 */
static void
meta_cb (void *cls,
         uint32_t version,
         struct GNUNET_TIME_Timestamp server_time,
         const struct GNUNET_HashCode *recdoc_id,
         const char *secret_name)
{
  struct ProviderOperation *po = cls;
  struct ANASTASIS_PolicyDiscovery *pd = po->pd;
  json_t *pa;
  json_t *pe;

  if (NULL == recdoc_id)
  {
    po->vc = NULL;
    GNUNET_CONTAINER_DLL_remove (pd->po_head,
                                 pd->po_tail,
                                 po);
    GNUNET_free (po->provider_url);
    GNUNET_free (po);
    return;
  }
  pe = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_uint64 ("version",
                             version),
    GNUNET_JSON_pack_string ("url",
                             po->provider_url));

  pa = GNUNET_CONTAINER_multihashmap_get (pd->dd_map,
                                          recdoc_id);
  if (NULL != pa)
  {
    GNUNET_break (0 ==
                  json_array_append_new (pa,
                                         pe));
    return;
  }
  pa = json_array ();
  GNUNET_assert (NULL != pa);
  GNUNET_break (0 ==
                json_array_append_new (pa,
                                       pe));
  GNUNET_assert (
    GNUNET_OK ==
    GNUNET_CONTAINER_multihashmap_put (
      pd->dd_map,
      recdoc_id,
      pa,
      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
  pd->cb (pd->cb_cls,
          recdoc_id,
          po->provider_url,
          version,
          (json_int_t) po->attribute_mask.mask,
          server_time,
          secret_name,
          pa);
}


/**
 * Start policy operation for @a pd using identity @a id_data
 * at provider @a provider_url.
 *
 * @param pd policy discovery operation
 * @param id_data our identity data, derived using @a mask
 * @param mask the mask describing which optional attributes were removed
 * @param provider_url which provider to query
 * @param cursor cursor telling us from where to query
 */
static void
start_po (struct ANASTASIS_PolicyDiscovery *pd,
          const json_t *id_data,
          struct ANASTASIS_AttributeMask mask,
          const char *provider_url,
          const json_t *cursor)
{
  struct ProviderOperation *po;
  uint32_t max_version = UINT32_MAX;
  struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;

  if (NULL != cursor)
  {
    size_t i;
    json_t *obj;

    json_array_foreach ((json_t *) cursor, i, obj)
    {
      const char *url;
      uint64_t cmask;
      uint32_t mv;
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_string ("provider_url",
                                 &url),
        GNUNET_JSON_spec_uint64 ("mask",
                                 &cmask),
        GNUNET_JSON_spec_uint32 ("max_version",
                                 &mv),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (obj,
                             spec,
                             NULL, NULL))
      {
        /* cursor invalid */
        GNUNET_break (0);
        json_dumpf (obj,
                    stderr,
                    JSON_INDENT (2));
        return;
      }
      if ( (cmask == mask.mask) &&
           (0 == strcmp (url,
                         provider_url)) )
      {
        max_version = mv;
        break;
      }
    }
  }

  if (GNUNET_OK !=
      ANASTASIS_REDUX_lookup_salt_ (&pd->rs->common,
                                    provider_url,
                                    &provider_salt))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "No /config for `%s', skipping provider\n",
                provider_url);
    return;
  }
  po = GNUNET_new (struct ProviderOperation);
  po->pd = pd;
  po->attribute_mask = mask;
  po->provider_url = GNUNET_strdup (provider_url);
  po->vc = ANASTASIS_recovery_get_versions (ANASTASIS_REDUX_ctx_,
                                            id_data,
                                            max_version,
                                            provider_url,
                                            &provider_salt,
                                            &meta_cb,
                                            po);
  if (NULL == po->vc)
  {
    GNUNET_free (po);
  }
  else
  {
    GNUNET_CONTAINER_DLL_insert (pd->po_head,
                                 pd->po_tail,
                                 po);
  }
}


struct ANASTASIS_PolicyDiscovery *
ANASTASIS_policy_discovery_start (const json_t *state,
                                  const json_t *cursor,
                                  ANASTASIS_PolicyDiscoveryCallback cb,
                                  void *cb_cls)
{
  struct ANASTASIS_PolicyDiscovery *pd;
  struct ANASTASIS_ReduxState *rs;
  const json_t *master_id;
  unsigned int opt_cnt;

  {
    enum TALER_ErrorCode ec;
    const char *detail;

    rs = ANASTASIS_REDUX_state_parse_ (state,
                                       &ec,
                                       &detail);
    if (NULL == rs)
    {
      GNUNET_break (0);
      json_dumpf ((json_t *) state,
                  stderr,
                  JSON_INDENT (2));
      return NULL;
    }
  }
  master_id = rs->common.identity_attributes;
  if ( (NULL == master_id) ||
       (! rs->common.have_providers) ||
       (! rs->common.have_required_attributes) )
  {
    GNUNET_break (0);
    json_dumpf ((json_t *) state,
                stderr,
                JSON_INDENT (2));
    ANASTASIS_REDUX_state_free_ (rs);
    return NULL;
  }

  /* count optional attributes present in 'master_id' */
  opt_cnt = 0;
  for (unsigned int i = 0; i < rs->common.required_attributes_len; i++)
  {
    const struct ANASTASIS_ReduxAttributeSpec *a
      = &rs->common.required_attributes[i];
    bool present;

    present = (NULL !=
               json_object_get (master_id,
                                a->name));
    if ((! present) && (! a->optional))
    {
      GNUNET_break (0);
      json_dumpf ((json_t *) master_id,
                  stderr,
                  JSON_INDENT (2));
      ANASTASIS_REDUX_state_free_ (rs);
      return NULL;
    }
    if (present && a->optional)
      opt_cnt++;
  }
  /* `opt_cnt` drives the 2^opt_cnt mask loop below; both it and
     `identity_attributes` come from the untrusted `state`, so a crafted
     state with ~40 optional attributes would queue ~10^12 provider
     downloads.  Fail closed instead. */
  if (opt_cnt > MAX_OPTIONAL_ATTRIBUTES)
  {
    GNUNET_break_op (0);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Refusing policy discovery over %u optional attributes (max %u)\n",
                opt_cnt,
                (unsigned int) MAX_OPTIONAL_ATTRIBUTES);
    ANASTASIS_REDUX_state_free_ (rs);
    return NULL;
  }

  pd = GNUNET_new (struct ANASTASIS_PolicyDiscovery);
  pd->dd_map = GNUNET_CONTAINER_multihashmap_create (128,
                                                     GNUNET_NO);
  pd->cb = cb;
  pd->cb_cls = cb_cls;
  pd->opt_cnt = opt_cnt;
  pd->rs = rs;

  /* Compute 'id_data' for all possible masks, and then
     start downloads at all providers for 'id_data' */
  for (uint64_t m = 0; m < (1LLU << opt_cnt); m++)
  {
    struct ANASTASIS_AttributeMask mask = {
      .mask = m
    };
    json_t *id_data = mask_id_data (&rs->common,
                                    master_id,
                                    mask);

    for (unsigned int i = 0; i < rs->common.providers_len; i++)
      start_po (pd,
                id_data,
                mask,
                rs->common.providers[i].url.url,
                cursor);
    json_decref (id_data);
  }
  return pd;
}


void
ANASTASIS_policy_discovery_more (struct ANASTASIS_PolicyDiscovery *pd,
                                 const char *provider_url,
                                 json_t *provider_state)
{
  const json_t *master_id = pd->rs->common.identity_attributes;
  const char *detail;

  GNUNET_assert (NULL != master_id);
  if (GNUNET_OK !=
      ANASTASIS_REDUX_provider_set_ (&pd->rs->common,
                                     provider_url,
                                     provider_state,
                                     &detail))
  {
    GNUNET_break_op (0);
    return;
  }
  /* Compute 'id_data' for all possible masks, and then
     start downloads at provider for 'id_data' */
  for (uint64_t m = 0; m < (1LLU << pd->opt_cnt); m++)
  {
    struct ANASTASIS_AttributeMask mask = {
      .mask = m
    };
    json_t *id_data = mask_id_data (&pd->rs->common,
                                    master_id,
                                    mask);

    start_po (pd,
              id_data,
              mask,
              provider_url,
              NULL);
    json_decref (id_data);
  }
}


/**
 * Free JSON Arrays from our hash map.
 *
 * @param cls NULL
 * @param key ignored
 * @param value `json_t *` to free
 * @return #GNUNET_OK
 */
static enum GNUNET_GenericReturnValue
free_dd_json (void *cls,
              const struct GNUNET_HashCode *key,
              void *value)
{
  json_t *j = value;

  (void) cls;
  (void) key;
  json_decref (j);
  return GNUNET_OK;
}


void
ANASTASIS_policy_discovery_stop (struct ANASTASIS_PolicyDiscovery *pd)
{
  struct ProviderOperation *po;

  while (NULL != (po = pd->po_head))
  {
    GNUNET_CONTAINER_DLL_remove (pd->po_head,
                                 pd->po_tail,
                                 po);
    ANASTASIS_recovery_get_versions_cancel (po->vc);
    GNUNET_free (po->provider_url);
    GNUNET_free (po);
  }
  GNUNET_CONTAINER_multihashmap_iterate (pd->dd_map,
                                         &free_dd_json,
                                         NULL);
  GNUNET_CONTAINER_multihashmap_destroy (pd->dd_map);
  ANASTASIS_REDUX_state_free_ (pd->rs);
  GNUNET_free (pd);
}


json_t *
ANASTASIS_mask_id_data (const json_t *state,
                        const json_t *master_id,
                        json_int_t mask)
{
  struct ANASTASIS_ReduxState *rs;
  struct ANASTASIS_AttributeMask m = {
    .mask = (uint64_t) mask
  };
  json_t *ret;

  {
    enum TALER_ErrorCode ec;
    const char *detail;

    rs = ANASTASIS_REDUX_state_parse_ (state,
                                       &ec,
                                       &detail);
    if (NULL == rs)
    {
      GNUNET_break (0);
      return NULL;
    }
  }
  if (! rs->common.have_required_attributes)
  {
    GNUNET_break (0);
    ANASTASIS_REDUX_state_free_ (rs);
    return NULL;
  }
  ret = mask_id_data (&rs->common,
                      master_id,
                      m);
  ANASTASIS_REDUX_state_free_ (rs);
  return ret;
}
