/*
  This file is part of Challenger
  Copyright (C) 2023, 2024 Taler Systems SA

  Challenger is free software; you can redistribute it and/or modify it under the
  terms of the GNU Affero General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  Challenger 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 Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  Challenger; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file challenger-httpd_setup.c
 * @brief functions to handle incoming requests for setups
 * @author Christian Grothoff
 */
#include "platform.h"
#include "challenger-httpd.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_db_lib.h>
#include "challenger-httpd_setup.h"
#include "challenger-httpd_common.h"
#include "challenger-database/do_insert_validation.h"


/**
 * Set to 1 to dump addresses into the log.
 */
#define DEBUG 0

/**
 * Maximum number of retries for the database interaction.
 */
#define MAX_RETRIES 3

struct SetupContext
{

  /**
   * Our handler context.
   */
  struct CH_HandlerContext *hc;

  /**
   * Opaque parsing context.
   */
  void *opaque_post_parsing_context;

  /**
   * Uploaded JSON data, NULL if upload is not yet complete.
   */
  json_t *root;

  /**
   * Did the client actually send a request body?  The body is optional
   * for this endpoint, so we must not hand an empty upload to the JSON
   * parser (which would reject it as malformed).
   */
  bool have_upload;

};


/**
 * Callback to clean up the request context.
 *
 * @param[in,out] ctx a `struct SetupContext` to clean up
 */
static void
request_done (void *ctx)
{
  struct SetupContext *sc = ctx;

  if (NULL == ctx)
    return;
  if (NULL != sc->root)
  {
    json_decref (sc->root);
    sc->root = NULL;
  }
  TALER_MHD_parse_post_cleanup_callback (sc->opaque_post_parsing_context);
  GNUNET_free (sc);
}


enum MHD_Result
CH_handler_setup (struct CH_HandlerContext *hc,
                  const char *upload_data,
                  size_t *upload_data_size)
{
  struct SetupContext *sc = hc->ctx;
  unsigned long long client_id;
  const char *client_secret;

  if (NULL == sc)
  {
    /* Fresh request, do initial setup */
    sc = GNUNET_new (struct SetupContext);
    sc->hc = hc;
    hc->cc = &request_done;
    hc->ctx = sc;
    return MHD_YES;
  }
  /* Remember that a body is coming.  We must keep calling the parser after
     this point even once *upload_data_size has dropped back to zero: that
     final call is the one where GNUNET_MHD_post_parser() actually decodes
     the buffered bytes.  Conversely, if no body ever arrives we must not
     call the parser at all, as it would report an empty document as
     malformed JSON -- the body of this endpoint is optional. */
  if (0 != *upload_data_size)
    sc->have_upload = true;
  if ( (NULL == sc->root) &&
       (sc->have_upload) )
  {
    /* parse byte stream upload into JSON */
    enum GNUNET_GenericReturnValue res;

#if DEBUG
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Handling upload of `%.*s'\n",
                (int) *upload_data_size,
                upload_data);
#endif
    res = TALER_MHD_parse_post_json (hc->connection,
                                     &sc->opaque_post_parsing_context,
                                     upload_data,
                                     upload_data_size,
                                     &sc->root);
    if (GNUNET_SYSERR == res)
    {
      GNUNET_assert (NULL == sc->root);
      return MHD_NO; /* bad upload, could not even generate error */
    }
    if ( (GNUNET_NO == res) ||
         (NULL == sc->root) )
    {
      GNUNET_assert (NULL == sc->root);
      return MHD_YES; /* so far incomplete upload or parser error */
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Address data uploaded in /setup\n");
#if DEBUG
    json_dumpf (sc->root,
                stderr,
                JSON_INDENT (2));
#endif
    if (! json_is_object (sc->root))
    {
      /* The address is stored verbatim and later packed with
         GNUNET_JSON_pack_object_steal() by /authorize, which asserts on
         anything that is not an object; refuse it here instead. */
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (hc->connection,
                                         MHD_HTTP_BAD_REQUEST,
                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                         "address");
    }
  }

  {
    char dummy;

    if (1 != sscanf (hc->path,
                     "%llu%c",
                     &client_id,
                     &dummy))
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (hc->connection,
                                         MHD_HTTP_BAD_REQUEST,
                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                         "client_id");
    }
  }
  {
    enum TALER_ErrorCode ec;

    client_secret = CH_get_client_secret (hc->connection,
                                          &ec);
    if (NULL == client_secret)
    {
      GNUNET_break_op (0);
      /* Credentials are required to use this endpoint at all, so refusing
         the request is a matter of authorization, not of a bad request. */
      return TALER_MHD_reply_with_error (hc->connection,
                                         MHD_HTTP_FORBIDDEN,
                                         ec,
                                         MHD_HTTP_HEADER_AUTHORIZATION);
    }
  }
  {
    const json_t *ro;

    ro = json_object_get (sc->root,
                          "read_only");
    if ( (NULL != ro) &&
         (! json_is_boolean (ro)) )
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (hc->connection,
                                         MHD_HTTP_BAD_REQUEST,
                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                         "read_only");
    }
  }
  {
    struct CHALLENGER_ValidationNonceP nonce;
    struct GNUNET_TIME_Absolute expiration_time;
    enum GNUNET_DB_QueryStatus qs;

    expiration_time = GNUNET_TIME_relative_to_absolute (CH_validation_duration);
    GNUNET_CRYPTO_random_block (&nonce,
                                sizeof (nonce));
    /* Authenticating the client, charging it for the validation and
       creating the validation row all happen in this one statement, so
       the validation counter only moves if the setup really succeeded.
       As the session is SERIALIZABLE and every /setup of a given client
       updates the same 'validation_counter' row, serialization failures
       are routine here and must be retried instead of reported. */
    for (unsigned int r = 0; r<MAX_RETRIES; r++)
    {
      qs = CHALLENGERDB_do_insert_validation (CH_context,
                                              client_id,
                                              client_secret,
                                              &nonce,
                                              expiration_time,
                                              sc->root);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        return TALER_MHD_reply_with_error (
          hc->connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_STORE_FAILED,
          "do_insert_validation");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        if (r < MAX_RETRIES - 1)
          continue;
        GNUNET_break (0);
        return TALER_MHD_reply_with_error (
          hc->connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_STORE_FAILED,
          "do_insert_validation");
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        /* Nothing was inserted, so the client did not authenticate. */
        return TALER_MHD_reply_with_error (
          hc->connection,
          MHD_HTTP_NOT_FOUND,
          TALER_EC_CHALLENGER_GENERIC_CLIENT_UNKNOWN,
          NULL);
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      break;
    } /* for MAX_RETRIES */
    {
      char *nstr;

      nstr = GNUNET_STRINGS_data_to_string_alloc (&nonce,
                                                  sizeof (nonce));
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "/setup returns nonce `%s'\n",
                  nstr);
      GNUNET_free (nstr);
    }
    return TALER_MHD_REPLY_JSON_PACK (
      hc->connection,
      MHD_HTTP_OK,
      GNUNET_JSON_pack_data_auto ("nonce",
                                  &nonce));
  }
}
