/*
  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_authorize.c
 * @brief functions to handle incoming requests for authorizations
 * @author Christian Grothoff
 */
#include "platform.h"
#include "challenger-httpd.h"
#include <gnunet/gnunet_util_lib.h>
#include <taler/taler_templating_lib.h>
#include "challenger-httpd_authorize.h"
#include "challenger-httpd_common.h"
#include "challenger-httpd_spa.h"
#include "challenger_cm_enums.h"

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

/**
 * Generate error reply in the format requested by
 * the client.
 *
 * @param hc 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 CH_HandlerContext *hc,
             const char *template,
             unsigned int http_status,
             enum TALER_ErrorCode ec,
             const char *hint)
{
  return TALER_MHD_reply_with_error (
    hc->connection,
    http_status,
    ec,
    hint);
}


MHD_RESULT
CH_handler_authorize (struct CH_HandlerContext *hc,
                      const char *upload_data,
                      size_t *upload_data_size)
{
  const char *response_type;
  unsigned long long client_id;
  const char *redirect_uri;
  const char *state;
  const char *scope;
  const char *code_challenge;
  enum CHALLENGER_CM code_challenge_method_enum;
  struct CHALLENGER_ValidationNonceP nonce;

  (void) upload_data;
  (void) upload_data_size;
  if (GNUNET_OK !=
      GNUNET_STRINGS_string_to_data (hc->path,
                                     strlen (hc->path),
                                     &nonce,
                                     sizeof (nonce)))
  {
    GNUNET_break_op (0);
    return reply_error (
      hc,
      "invalid-request",
      MHD_HTTP_NOT_FOUND,
      TALER_EC_GENERIC_PARAMETER_MISSING,
      hc->path);
  }
  response_type
    = MHD_lookup_connection_value (hc->connection,
                                   MHD_GET_ARGUMENT_KIND,
                                   "response_type");
  if (NULL == response_type)
  {
    GNUNET_break_op (0);
    return reply_error (
      hc,
      "invalid-request",
      MHD_HTTP_BAD_REQUEST,
      TALER_EC_GENERIC_PARAMETER_MISSING,
      "response_type");
  }
  if (0 != strcmp (response_type,
                   "code"))
  {
    GNUNET_break_op (0);
    return reply_error (
      hc,
      "invalid-request",
      MHD_HTTP_BAD_REQUEST,
      TALER_EC_GENERIC_PARAMETER_MALFORMED,
      "response_type (must be 'code')");
  }

  {
    const char *client_id_str;
    char dummy;

    client_id_str
      = MHD_lookup_connection_value (hc->connection,
                                     MHD_GET_ARGUMENT_KIND,
                                     "client_id");
    if (NULL == client_id_str)
    {
      GNUNET_break_op (0);
      return reply_error (
        hc,
        "invalid_request",
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MISSING,
        "client_id");
    }
    if (1 != sscanf (client_id_str,
                     "%llu%c",
                     &client_id,
                     &dummy))
    {
      GNUNET_break_op (0);
      return reply_error (
        hc,
        "invalid-request",
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "client_id");
    }
  }
  redirect_uri
    = MHD_lookup_connection_value (hc->connection,
                                   MHD_GET_ARGUMENT_KIND,
                                   "redirect_uri");

  {
    const char *code_challenge_method;

    code_challenge_method
      = MHD_lookup_connection_value (hc->connection,
                                     MHD_GET_ARGUMENT_KIND,
                                     "code_challenge_method");
    code_challenge_method_enum
      = CHALLENGER_cm_from_string (
          code_challenge_method);
  }
  if (CHALLENGER_CM_UNKNOWN == code_challenge_method_enum)
  {
    GNUNET_break_op (0);
    return reply_error (hc,
                        "invalid-request",
                        MHD_HTTP_BAD_REQUEST,
                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
                        "Unsupported code_challenge_method, supported only \"plain\", \"S256\".");
  }
  code_challenge = MHD_lookup_connection_value (hc->connection,
                                                MHD_GET_ARGUMENT_KIND,
                                                "code_challenge");
  /* If we have a code challenge, we default to PLAIN instead of EMPTY */
  if ( (NULL != code_challenge) &&
       (CHALLENGER_CM_EMPTY == code_challenge_method_enum) )
    code_challenge_method_enum = CHALLENGER_CM_PLAIN;

  /**
   * Safety check to not allow public clients without s256 code_challenge
   */
  if ( (NULL != redirect_uri) &&
       (! TALER_is_web_url (redirect_uri)) &&
       ( (CHALLENGER_CM_EMPTY == code_challenge_method_enum) ||
         (CHALLENGER_CM_PLAIN == code_challenge_method_enum) ) )
  {
    GNUNET_break_op (0);
    return reply_error (
      hc,
      "invalid-request",
      MHD_HTTP_BAD_REQUEST,
      TALER_EC_GENERIC_PARAMETER_MALFORMED,
      "redirect_uri (has to start with 'http://' or 'https://' or not use 'plain'/NULL as code_challenge)");
  }

  state
    = MHD_lookup_connection_value (hc->connection,
                                   MHD_GET_ARGUMENT_KIND,
                                   "state");
  if (NULL == state)
    state = "";

  scope
    = MHD_lookup_connection_value (hc->connection,
                                   MHD_GET_ARGUMENT_KIND,
                                   "scope");
  {
    json_t *last_address = NULL;
    uint32_t address_attempts_left;
    uint32_t pin_transmissions_left;
    uint32_t auth_attempts_left;
    struct GNUNET_TIME_Absolute last_tx_time;
    bool solved;
    enum GNUNET_DB_QueryStatus qs;

    /* authorize_start will return 0 if a 'redirect_uri' was
       configured for the client and this one differs. */
    for (unsigned int r = 0; r<MAX_RETRIES; r++)
    {
      qs = CH_db->authorize_start (CH_db->cls,
                                   &nonce,
                                   client_id,
                                   scope,
                                   state,
                                   redirect_uri,
                                   code_challenge,
                                   (uint32_t) code_challenge_method_enum,
                                   &last_address,
                                   &address_attempts_left,
                                   &pin_transmissions_left,
                                   &auth_attempts_left,
                                   &solved,
                                   &last_tx_time);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        return reply_error (
          hc,
          "internal-error",
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_STORE_FAILED,
          "authorize_start");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        if (r < MAX_RETRIES - 1)
          continue;
        GNUNET_break (0);
        return reply_error (
          hc,
          "internal-error",
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_STORE_FAILED,
          "authorize_start");
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        GNUNET_break_op (0);
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Failed to find authorization process of client %llu for nonce `%s'\n",
                    client_id,
                    hc->path);
        return reply_error (
          hc,
          "validation-unknown",
          MHD_HTTP_NOT_FOUND,
          TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
          NULL);
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      break;
    }
    if (0 == CH_get_output_type (hc->connection))
    {
      char *prev_full_url = hc->full_url;
      const char *rparams = strchr (hc->full_url, '?');

      if (NULL == rparams)
        GNUNET_asprintf (&hc->full_url,
                         "%s?nonce=%s",
                         prev_full_url,
                         hc->path);

      else
        GNUNET_asprintf (&hc->full_url,
                         "%s&nonce=%s",
                         prev_full_url,
                         hc->path);

      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Redirect before %s now `%s'\n",
                  prev_full_url,
                  hc->full_url);
      GNUNET_free (prev_full_url);
      json_decref (last_address);
      return CH_spa_redirect (hc,
                              NULL,
                              0);
    }
    {
      json_t *args;
      struct MHD_Response *resp;
      MHD_RESULT res;
      json_t *ro;

      ro = json_object_get (last_address,
                            "read_only");
      if ( (NULL != ro) &&
           (json_boolean_value (ro)) )
        address_attempts_left = 0;

      args = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_bool ("fix_address",
                               0 == address_attempts_left),
        GNUNET_JSON_pack_allow_null (
          GNUNET_JSON_pack_object_steal ("last_address",
                                         last_address)),
        GNUNET_JSON_pack_bool ("solved",
                               solved),
        GNUNET_JSON_pack_uint64 ("pin_transmissions_left",
                                 pin_transmissions_left),
        GNUNET_JSON_pack_uint64 ("auth_attempts_left",
                                 auth_attempts_left),
        GNUNET_JSON_pack_timestamp ("retransmission_time",
                                    GNUNET_TIME_absolute_to_timestamp (
                                      GNUNET_TIME_absolute_add (
                                        last_tx_time,
                                        CH_validation_duration))),
        GNUNET_JSON_pack_uint64 ("changes_left",
                                 address_attempts_left)
        );
      resp = TALER_MHD_make_json_steal (args);
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (resp,
                                             MHD_HTTP_HEADER_CACHE_CONTROL,
                                             "no-store,no-cache"));
      res = MHD_queue_response (hc->connection,
                                MHD_HTTP_OK,
                                resp);
      MHD_destroy_response (resp);
      return res;
    }
  }
}
