/*
  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_solve.c
 * @brief functions to handle incoming /solve requests
 * @author Christian Grothoff
 */
#include "platform.h"
#include "challenger-httpd.h"
#include <gnunet/gnunet_util_lib.h>
#include "challenger-httpd_common.h"
#include "challenger-httpd_solve.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_templating_lib.h>
#include <taler/taler_signatures.h>

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

/**
 * Context for a /solve operation.
 */
struct SolveContext
{

  /**
   * Nonce of the operation.
   */
  struct CHALLENGER_ValidationNonceP nonce;

  /**
   * HTTP request context.
   */
  struct CH_HandlerContext *hc;

  /**
   * Handle for processing uploaded data.
   */
  struct MHD_PostProcessor *pp;

  /**
   * OAuth2 client redirection URI for error reporting.
   */
  char *client_redirect_uri;

  /**
   * 0-terminated PIN submitted to us.
   */
  char *pin;

  /**
   * OAuth2 state.
   */
  char *state;

  /**
   * Number of bytes in @a pin, excluding 0-terminator.
   */
  size_t pin_len;

  /**
   * How many address changes are still allowed?
   */
  uint32_t addr_left;

  /**
   * How many authentication attempts are still allowed?
   */
  uint32_t auth_attempts_left;

  /**
   * How many pin transmissions can still be requested?
   */
  uint32_t pin_transmissions_left;

};


/**
 * Function called to clean up a backup context.
 *
 * @param cls a `struct SolveContext`
 */
static void
cleanup_ctx (void *cls)
{
  struct SolveContext *bc = cls;

  if (NULL != bc->pp)
  {
    GNUNET_break_op (MHD_YES ==
                     MHD_destroy_post_processor (bc->pp));
  }
  GNUNET_free (bc->pin);
  GNUNET_free (bc->state);
  GNUNET_free (bc->client_redirect_uri);
  GNUNET_free (bc);
}


/**
 * Generate error reply in the format requested by
 * the client.
 *
 * @param bc our context
 * @param template error template to use
 * @param http_status HTTP status to return
 * @param ec error code to return
 * @param hint human-readable hint to give
 */
static MHD_RESULT
reply_error (struct SolveContext *bc,
             const char *template,
             unsigned int http_status,
             enum TALER_ErrorCode ec,
             const char *hint)
{
  struct CH_HandlerContext *hc = bc->hc;

  return TALER_MHD_reply_with_error (
    hc->connection,
    http_status,
    ec,
    hint);
}


/**
 * Iterator over key-value pairs where the value may be made available
 * in increments and/or may not be zero-terminated.  Used for
 * processing POST data.
 *
 * @param cls a `struct SolveContext *`
 * @param kind type of the value, always #MHD_POSTDATA_KIND when called from MHD
 * @param key 0-terminated key for the value
 * @param filename name of the uploaded file, NULL if not known
 * @param content_type mime-type of the data, NULL if not known
 * @param transfer_encoding encoding of the data, NULL if not known
 * @param data pointer to @a size bytes of data at the
 *              specified offset
 * @param off offset of data in the overall value
 * @param size number of bytes in @a data available
 * @return #MHD_YES to continue iterating,
 *         #MHD_NO to abort the iteration
 */
static enum MHD_Result
post_iter (void *cls,
           enum MHD_ValueKind kind,
           const char *key,
           const char *filename,
           const char *content_type,
           const char *transfer_encoding,
           const char *data,
           uint64_t off,
           size_t size)
{
  struct SolveContext *bc = cls;

  (void) filename;
  (void) content_type;
  (void) transfer_encoding;
  (void) off;
  if (0 != strcmp (key,
                   "pin"))
    return MHD_YES;
  if (MHD_POSTDATA_KIND != kind)
    return MHD_YES;
  bc->pin = GNUNET_realloc (bc->pin,
                            bc->pin_len + size + 1);
  memcpy (bc->pin + bc->pin_len,
          data,
          size);
  bc->pin_len += size;
  bc->pin[bc->pin_len] = '\0';
  return MHD_YES;
}


MHD_RESULT
CH_handler_solve (struct CH_HandlerContext *hc,
                  const char *upload_data,
                  size_t *upload_data_size)
{
  struct SolveContext *bc = hc->ctx;

  if (NULL == bc)
  {
    /* first call, setup internals */
    bc = GNUNET_new (struct SolveContext);
    hc->cc = &cleanup_ctx;
    hc->ctx = bc;
    bc->hc = hc;
    bc->pp = MHD_create_post_processor (hc->connection,
                                        1024,
                                        &post_iter,
                                        bc);
    if (GNUNET_OK !=
        GNUNET_STRINGS_string_to_data (hc->path,
                                       strlen (hc->path),
                                       &bc->nonce,
                                       sizeof (bc->nonce)))
    {
      GNUNET_break_op (0);
      return reply_error (bc,
                          "invalid-request",
                          MHD_HTTP_BAD_REQUEST,
                          TALER_EC_CHALLENGER_HELPER_EXEC_FAILED,
                          hc->path);
    }
    TALER_MHD_check_content_length (hc->connection,
                                    1024);
    return MHD_YES;
  }
  /* handle upload */
  if (0 != *upload_data_size)
  {
    enum MHD_Result res;

    res = MHD_post_process (bc->pp,
                            upload_data,
                            *upload_data_size);
    *upload_data_size = 0;
    if (MHD_YES == res)
      return MHD_YES;
    return MHD_NO;
  }
  if (NULL == bc->pin)
  {
    GNUNET_break_op (0);
    return reply_error (bc,
                        "invalid-request",
                        MHD_HTTP_BAD_REQUEST,
                        TALER_EC_GENERIC_PARAMETER_MISSING,
                        "pin");
  }
  {
    unsigned int pin;
    char dummy;
    enum GNUNET_DB_QueryStatus qs;
    bool solved;
    bool exhausted;
    bool no_challenge;

    if (1 != sscanf (bc->pin,
                     "%u%c",
                     &pin,
                     &dummy))
    {
      GNUNET_break_op (0);
      return reply_error (bc,
                          "invalid-request",
                          MHD_HTTP_BAD_REQUEST,
                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
                          "pin");
    }

    for (unsigned int r = 0; r<MAX_RETRIES; r++)
    {
      qs = CH_db->validate_solve_pin (CH_db->cls,
                                      &bc->nonce,
                                      pin,
                                      &solved,
                                      &exhausted,
                                      &no_challenge,
                                      &bc->state,
                                      &bc->addr_left,
                                      &bc->auth_attempts_left,
                                      &bc->pin_transmissions_left,
                                      &bc->client_redirect_uri);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        return reply_error (
          bc,
          "internal-error",
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_FETCH_FAILED,
          "validate_solve_pin");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        if (r < MAX_RETRIES - 1)
          continue;
        GNUNET_break (0);
        return reply_error (
          bc,
          "internal-error",
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_FETCH_FAILED,
          "validate_solve_pin");
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        return reply_error (
          bc,
          "validation-unknown",
          MHD_HTTP_NOT_FOUND,
          TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
          NULL);
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      break;
    }
    if (! solved)
    {
      MHD_RESULT ret;
      json_t *details;

      if ( (NULL != bc->state) &&
           (0 == bc->addr_left) &&
           (0 == bc->auth_attempts_left) )
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Client exhausted all chances to satisfy challenge\n");
        return reply_error (bc,
                            "access_denied",
                            MHD_HTTP_TOO_MANY_REQUESTS,
                            TALER_EC_CHALLENGER_TOO_MANY_ATTEMPTS,
                            "users exhausted all possibilities of passing the check");
      }

      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Invalid PIN supplied\n");
      details = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("type",
                                 "pending"),
        TALER_JSON_pack_ec (TALER_EC_CHALLENGER_INVALID_PIN),
        GNUNET_JSON_pack_uint64 ("addresses_left",
                                 bc->addr_left),
        GNUNET_JSON_pack_uint64 ("pin_transmissions_left",
                                 bc->pin_transmissions_left),
        GNUNET_JSON_pack_uint64 ("auth_attempts_left",
                                 bc->auth_attempts_left),
        GNUNET_JSON_pack_bool ("exhausted",
                               exhausted),
        GNUNET_JSON_pack_bool ("no_challenge",
                               no_challenge)
        );
      ret = TALER_MHD_reply_json (hc->connection,
                                  details,
                                  MHD_HTTP_FORBIDDEN);
      json_decref (details);
      return ret;
    }
  }

  {
    struct MHD_Response *response;
    char *url;
    unsigned int http_status;

    {
      char *client_secret;
      json_t *address;
      char *client_scope;
      char *client_state;
      char *client_redirect_uri;
      enum GNUNET_DB_QueryStatus qs;

      qs = CH_db->validation_get (CH_db->cls,
                                  &bc->nonce,
                                  &client_secret,
                                  &address,
                                  &client_scope,
                                  &client_state,
                                  &client_redirect_uri);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        return reply_error (bc,
                            "internal-server-error",
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_DB_FETCH_FAILED,
                            "validation_get");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        GNUNET_break (0);
        return MHD_NO;
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        return reply_error (bc,
                            "validation-unknown",
                            MHD_HTTP_NOT_FOUND,
                            TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
                            NULL);
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      {
        char *code;
        char *url_encoded;

        code = CH_compute_code (&bc->nonce,
                                client_secret,
                                client_scope,
                                address,
                                client_redirect_uri);
        if (NULL == client_state)
        {
          GNUNET_asprintf (&url,
                           "%s?code=%s",
                           client_redirect_uri,
                           code);
        }
        else
        {
          url_encoded = TALER_urlencode (client_state);
          GNUNET_asprintf (&url,
                           "%s?code=%s&state=%s",
                           client_redirect_uri,
                           code,
                           url_encoded);
          GNUNET_free (url_encoded);
        }
        GNUNET_free (code);
      }
      json_decref (address);
      GNUNET_free (client_scope);
      GNUNET_free (client_secret);
      GNUNET_free (client_redirect_uri);
      GNUNET_free (client_state);
    }

    if (0 == CH_get_output_type (hc->connection))
    {
      {
        const char *ok = "Ok!";

        response = MHD_create_response_from_buffer (strlen (ok),
                                                    (void *) ok,
                                                    MHD_RESPMEM_PERSISTENT);
      }
      if (NULL == response)
      {
        GNUNET_break (0);
        GNUNET_free (url);
        return MHD_NO;
      }
      TALER_MHD_add_global_headers (response,
                                    false);
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (response,
                                             MHD_HTTP_HEADER_CONTENT_TYPE,
                                             "text/plain"));
      if (MHD_NO ==
          MHD_add_response_header (response,
                                   MHD_HTTP_HEADER_LOCATION,
                                   url))
      {
        GNUNET_break (0);
        MHD_destroy_response (response);
        GNUNET_free (url);
        return MHD_NO;
      }
      http_status = MHD_HTTP_FOUND;
      GNUNET_free (url);
    }
    else
    {
      json_t *args;

      args = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("type",
                                 "completed"),
        GNUNET_JSON_pack_string ("redirect_url",
                                 url)
        );
      GNUNET_free (url);
      response = TALER_MHD_make_json (args);
      http_status = MHD_HTTP_OK;
    }

    {
      MHD_RESULT ret;

      ret = MHD_queue_response (hc->connection,
                                http_status,
                                response);
      MHD_destroy_response (response);
      return ret;
    }
  }
}
