/*
  This file is part of TALER
  (C) 2025 Taler Systems SA

  TALER 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.

  TALER 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 TALER; see the file COPYING.  If not,
  see <http://www.gnu.org/licenses/>
*/

/**
 * @file taler-merchant-httpd_post-challenge-ID.c
 * @brief endpoint to trigger sending MFA challenge
 * @author Christian Grothoff
 */
#include "platform.h"
#include "taler-merchant-httpd.h"
#include "taler-merchant-httpd_mfa.h"
#include "taler-merchant-httpd_post-challenge-ID.h"


/**
 * How many attempts do we allow per solution at most? Note that
 * this is just for the API, the value must also match the
 * database logic in create_mfa_challenge.
 */
#define MAX_SOLUTIONS 3


/**
 * How long is an OTP code valid?
 */
#define OTP_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30)


/**
 * Internal state for MFA processing.
 */
struct MfaState
{

  /**
   * Kept in a DLL.
   */
  struct MfaState *next;

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

  /**
   * HTTP request we are handling.
   */
  struct TMH_HandlerContext *hc;

  /**
   * Challenge code.
   */
  char *code;

  /**
   * When does @e code expire?
   */
  struct GNUNET_TIME_Absolute expiration_date;

  /**
   * When may we transmit a new code?
   */
  struct GNUNET_TIME_Absolute retransmission_date;

  /**
   * Handle to the helper process.
   */
  struct GNUNET_OS_Process *child;

  /**
   * Handle to wait for @e child
   */
  struct GNUNET_ChildWaitHandle *cwh;

  /**
   * Address where to send the challenge.
   */
  char *required_address;

  /**
   * Message to send.
   */
  char *msg;

  /**
   * Offset of transmission in msg.
   */
  size_t msg_off;

  /**
   * ID of our challenge.
   */
  uint64_t challenge_id;

  /**
   * Salted hash over the request body.
   */
  struct TALER_MERCHANT_MFA_BodyHash h_body;

  /**
   * Channel to use for the challenge.
   */
  enum TALER_MERCHANT_MFA_Channel channel;

  enum
  {
    MFA_PHASE_PARSE = 0,
    MFA_PHASE_LOOKUP,
    MFA_PHASE_SENDING,
    MFA_PHASE_SUSPENDING,
    MFA_PHASE_SENT,
    MFA_PHASE_RETURN_YES,
    MFA_PHASE_RETURN_NO,

  } phase;


  /**
   * #GNUNET_NO if the @e connection was not suspended,
   * #GNUNET_YES if the @e connection was suspended,
   * #GNUNET_SYSERR if @e connection was resumed to as
   * part of #THM_mfa_done during shutdown.
   */
  enum GNUNET_GenericReturnValue suspended;

  /**
   * Type of critical operation being authorized.
   */
  enum TALER_MERCHANT_MFA_CriticalOperation op;

  /**
   * Set to true if sending worked.
   */
  bool send_ok;
};


/**
 * Kept in a DLL.
 */
static struct MfaState *mfa_head;

/**
 * Kept in a DLL.
 */
static struct MfaState *mfa_tail;


/**
 * Clean up @a mfa process.
 *
 * @param[in] cls the `struct MfaState` to clean up
 */
static void
mfa_context_cleanup (void *cls)
{
  struct MfaState *mfa = cls;

  GNUNET_CONTAINER_DLL_remove (mfa_head,
                               mfa_tail,
                               mfa);
  if (NULL != mfa->cwh)
  {
    GNUNET_wait_child_cancel (mfa->cwh);
    mfa->cwh = NULL;
  }
  if (NULL != mfa->child)
  {
    (void) GNUNET_OS_process_kill (mfa->child,
                                   SIGKILL);
    GNUNET_break (GNUNET_OK ==
                  GNUNET_OS_process_wait (mfa->child));
    mfa->child = NULL;
  }
  GNUNET_free (mfa->required_address);
  GNUNET_free (mfa->msg);
  GNUNET_free (mfa->code);
  GNUNET_free (mfa);
}


void
TMH_challenge_done ()
{
  for (struct MfaState *mfa = mfa_head;
       NULL != mfa;
       mfa = mfa->next)
  {
    if (GNUNET_YES == mfa->suspended)
    {
      mfa->suspended = GNUNET_SYSERR;
      MHD_resume_connection (mfa->hc->connection);
    }
  }
}


/**
 * Send the given @a response for the @a mfa request.
 *
 * @param[in,out] mfa process to generate an error response for
 * @param response_code response code to use
 * @param[in] response response data to send back
 */
static void
respond_to_challenge_with_response (struct MfaState *mfa,
                                    unsigned int response_code,
                                    struct MHD_Response *response)
{
  MHD_RESULT res;

  res = MHD_queue_response (mfa->hc->connection,
                            response_code,
                            response);
  MHD_destroy_response (response);
  mfa->phase = (MHD_NO == res)
    ? MFA_PHASE_RETURN_NO
    : MFA_PHASE_RETURN_YES;
}


/**
 * Generate an error for @a mfa.
 *
 * @param[in,out] mfa process to generate an error response for
 * @param http_status HTTP status of the response
 * @param ec Taler error code to return
 * @param hint hint to return, can be NULL
 */
static void
respond_with_error (struct MfaState *mfa,
                    unsigned int http_status,
                    enum TALER_ErrorCode ec,
                    const char *hint)
{
  respond_to_challenge_with_response (
    mfa,
    http_status,
    TALER_MHD_make_error (ec,
                          hint));
}


/**
 * Challenge code transmission complete. Continue based on the result.
 *
 * @param[in,out] mfa process to send the challenge for
 */
static void
phase_sent (struct MfaState *mfa)
{
  enum GNUNET_DB_QueryStatus qs;

  if (! mfa->send_ok)
  {
    respond_with_error (mfa,
                        MHD_HTTP_INTERNAL_SERVER_ERROR,
                        TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
                        "process exited with error");
    return;
  }
  qs = TMH_db->update_mfa_challenge (TMH_db->cls,
                                     mfa->challenge_id,
                                     mfa->code,
                                     MAX_SOLUTIONS,
                                     mfa->expiration_date,
                                     mfa->retransmission_date);
  switch (qs)
  {
  case GNUNET_DB_STATUS_HARD_ERROR:
    GNUNET_break (0);
    respond_with_error (mfa,
                        MHD_HTTP_INTERNAL_SERVER_ERROR,
                        TALER_EC_GENERIC_DB_COMMIT_FAILED,
                        "update_mfa_challenge");
    return;
  case GNUNET_DB_STATUS_SOFT_ERROR:
    GNUNET_break (0);
    respond_with_error (mfa,
                        MHD_HTTP_INTERNAL_SERVER_ERROR,
                        TALER_EC_GENERIC_DB_SOFT_FAILURE,
                        "update_mfa_challenge");
    return;
  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    GNUNET_break (0);
    respond_with_error (mfa,
                        MHD_HTTP_INTERNAL_SERVER_ERROR,
                        TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
                        "no results on INSERT, but success?");
    return;
  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    break;
  }
  {
    struct MHD_Response *response;

    response =
      TALER_MHD_make_json_steal (
        GNUNET_JSON_PACK (
          GNUNET_JSON_pack_timestamp (
            "solve_expiration",
            GNUNET_TIME_absolute_to_timestamp (
              mfa->expiration_date)),
          GNUNET_JSON_pack_timestamp (
            "earliest_retransmission",
            GNUNET_TIME_absolute_to_timestamp (
              mfa->retransmission_date))));
    respond_to_challenge_with_response (
      mfa,
      MHD_HTTP_OK,
      response);
  }
}


/**
 * Function called when our SMS helper has terminated.
 *
 * @param cls our `struct ANASTASIS_AUHTORIZATION_State`
 * @param type type of the process
 * @param exit_code status code of the process
 */
static void
transmission_done_cb (void *cls,
                      enum GNUNET_OS_ProcessStatusType type,
                      long unsigned int exit_code)
{
  struct MfaState *mfa = cls;

  mfa->cwh = NULL;
  if (NULL != mfa->child)
  {
    GNUNET_OS_process_destroy (mfa->child);
    mfa->child = NULL;
  }
  mfa->send_ok = ( (GNUNET_OS_PROCESS_EXITED == type) &&
                   (0 == exit_code) );
  if (! mfa->send_ok)
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "MFA helper failed with status %d/%u\n",
                (int) type,
                (unsigned int) exit_code);
  mfa->phase = MFA_PHASE_SENT;
  GNUNET_assert (GNUNET_YES == mfa->suspended);
  mfa->suspended = GNUNET_NO;
  MHD_resume_connection (mfa->hc->connection);
  TALER_MHD_daemon_trigger ();
}


/**
 * Setup challenge code for @a mfa and send it to the
 * @a required_address; on success.
 *
 * @param[in,out] mfa process to send the challenge for
 * @param required_address where to send the challenge
 */
static void
phase_send_challenge (struct MfaState *mfa)
{
  const char *prog = NULL;

  switch (mfa->channel)
  {
  case TALER_MERCHANT_MFA_CHANNEL_NONE:
    GNUNET_assert (0);
    break;
  case TALER_MERCHANT_MFA_CHANNEL_SMS:
    mfa->expiration_date
      = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    mfa->retransmission_date
      = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    GNUNET_asprintf (&mfa->code,
                     "%llu",
                     (unsigned long long)
                     GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
                                               100000000));
    prog = TMH_helper_sms;
    break;
  case TALER_MERCHANT_MFA_CHANNEL_EMAIL:
    mfa->expiration_date
      = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    mfa->retransmission_date
      = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
    GNUNET_asprintf (&mfa->code,
                     "%llu",
                     (unsigned long long)
                     GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
                                               100000000));
    prog = TMH_helper_email;
    break;
  case TALER_MERCHANT_MFA_CHANNEL_TOTP:
    mfa->expiration_date
      = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT);
    mfa->retransmission_date
      = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT);
    respond_with_error (mfa,
                        MHD_HTTP_NOT_IMPLEMENTED,
                        TALER_EC_GENERIC_FEATURE_NOT_IMPLEMENTED,
                        "#10327");
    return;
  }
  if (NULL == prog)
  {
    respond_with_error (
      mfa,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
      TALER_MERCHANT_MFA_channel_to_string (mfa->channel));
    return;
  }
  {
    /* Start child process and feed pipe */
    struct GNUNET_DISK_PipeHandle *p;
    struct GNUNET_DISK_FileHandle *pipe_stdin;

    p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW);
    if (NULL == p)
    {
      respond_with_error (mfa,
                          MHD_HTTP_INTERNAL_SERVER_ERROR,
                          TALER_EC_GENERIC_ALLOCATION_FAILURE,
                          "pipe");
      return;
    }
    mfa->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR,
                                          p,
                                          NULL,
                                          NULL,
                                          prog,
                                          prog,
                                          mfa->required_address,
                                          NULL);
    if (NULL == mfa->child)
    {
      GNUNET_break (GNUNET_OK ==
                    GNUNET_DISK_pipe_close (p));
      respond_with_error (mfa,
                          MHD_HTTP_INTERNAL_SERVER_ERROR,
                          TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
                          "exec");
      return;
    }

    pipe_stdin = GNUNET_DISK_pipe_detach_end (p,
                                              GNUNET_DISK_PIPE_END_WRITE);
    GNUNET_assert (NULL != pipe_stdin);
    GNUNET_break (GNUNET_OK ==
                  GNUNET_DISK_pipe_close (p));
    GNUNET_asprintf (&mfa->msg,
                     "%s\nTaler-Merchant:\n%s",
                     mfa->code,
                     TALER_MERCHANT_MFA_co2s (mfa->op));
    {
      const char *off = mfa->msg;
      size_t left = strlen (off);

      while (0 != left)
      {
        ssize_t ret;

        ret = GNUNET_DISK_file_write (pipe_stdin,
                                      off,
                                      left);
        if (ret <= 0)
        {
          respond_with_error (mfa,
                              MHD_HTTP_INTERNAL_SERVER_ERROR,
                              TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
                              "write");
          return;
        }
        mfa->msg_off += ret;
        off += ret;
        left -= ret;
      }
      GNUNET_DISK_file_close (pipe_stdin);
    }
  }
  mfa->phase = MFA_PHASE_SUSPENDING;
}


/**
 * Lookup challenge in DB.
 *
 * @param[in,out] mfa process to parse data for
 */
static void
phase_lookup (struct MfaState *mfa)
{
  enum GNUNET_DB_QueryStatus qs;
  uint32_t retry_counter;
  struct GNUNET_TIME_Absolute confirmation_date;
  struct GNUNET_TIME_Absolute retransmission_date;
  struct TALER_MERCHANT_MFA_BodySalt salt;

  qs = TMH_db->lookup_mfa_challenge (TMH_db->cls,
                                     mfa->challenge_id,
                                     &mfa->h_body,
                                     &salt,
                                     &mfa->required_address,
                                     &mfa->op,
                                     &confirmation_date,
                                     &retransmission_date,
                                     &retry_counter,
                                     &mfa->channel);
  switch (qs)
  {
  case GNUNET_DB_STATUS_HARD_ERROR:
    GNUNET_break (0);
    respond_with_error (mfa,
                        MHD_HTTP_INTERNAL_SERVER_ERROR,
                        TALER_EC_GENERIC_DB_COMMIT_FAILED,
                        "lookup_mfa_challenge");
    return;
  case GNUNET_DB_STATUS_SOFT_ERROR:
    GNUNET_break (0);
    respond_with_error (mfa,
                        MHD_HTTP_INTERNAL_SERVER_ERROR,
                        TALER_EC_GENERIC_DB_SOFT_FAILURE,
                        "lookup_mfa_challenge");
    return;
  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    GNUNET_break (0);
    respond_with_error (mfa,
                        MHD_HTTP_NOT_FOUND,
                        TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN,
                        mfa->hc->infix);
    return;
  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    break;
  }
  if (! GNUNET_TIME_absolute_is_future (confirmation_date))
  {
    /* was already solved */
    respond_with_error (mfa,
                        MHD_HTTP_GONE,
                        TALER_EC_MERCHANT_TAN_CHALLENGE_SOLVED,
                        NULL);
    return;
  }
  if (GNUNET_TIME_absolute_is_future (retransmission_date))
  {
    /* too early to try again */
    respond_with_error (mfa,
                        MHD_HTTP_TOO_MANY_REQUESTS,
                        TALER_EC_MERCHANT_TAN_TOO_EARLY,
                        GNUNET_TIME_absolute2s (retransmission_date));
    return;
  }
  mfa->phase++;
}


/**
 * Parse challenge request.
 *
 * @param[in,out] mfa process to parse data for
 */
static void
phase_parse (struct MfaState *mfa)
{
  struct TMH_HandlerContext *hc = mfa->hc;
  enum GNUNET_GenericReturnValue ret;

  ret = TMH_mfa_parse_challenge_id (hc,
                                    hc->infix,
                                    &mfa->challenge_id,
                                    &mfa->h_body);
  if (GNUNET_OK != ret)
  {
    mfa->phase = (GNUNET_NO == ret)
      ? MFA_PHASE_RETURN_YES
      : MFA_PHASE_RETURN_NO;
    return;
  }
  mfa->phase++;
}


MHD_RESULT
TMH_post_challenge_ID (const struct TMH_RequestHandler *rh,
                       struct MHD_Connection *connection,
                       struct TMH_HandlerContext *hc)
{
  struct MfaState *mfa = hc->ctx;

  if (NULL == mfa)
  {
    mfa = GNUNET_new (struct MfaState);
    mfa->hc = hc;
    hc->ctx = mfa;
    hc->cc = &mfa_context_cleanup;
    GNUNET_CONTAINER_DLL_insert (mfa_head,
                                 mfa_tail,
                                 mfa);
  }

  while (1)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Processing /challenge in phase %d\n",
                (int) mfa->phase);
    switch (mfa->phase)
    {
    case MFA_PHASE_PARSE:
      phase_parse (mfa);
      break;
    case MFA_PHASE_LOOKUP:
      phase_lookup (mfa);
      break;
    case MFA_PHASE_SENDING:
      phase_send_challenge (mfa);
      break;
    case MFA_PHASE_SUSPENDING:
      mfa->cwh = GNUNET_wait_child (mfa->child,
                                    &transmission_done_cb,
                                    mfa);
      if (NULL == mfa->cwh)
      {
        respond_with_error (mfa,
                            MHD_HTTP_INTERNAL_SERVER_ERROR,
                            TALER_EC_GENERIC_ALLOCATION_FAILURE,
                            "GNUNET_wait_child");
        continue;
      }
      mfa->suspended = GNUNET_YES;
      MHD_suspend_connection (hc->connection);
      return MHD_YES;
    case MFA_PHASE_SENT:
      phase_sent (mfa);
      break;
    case MFA_PHASE_RETURN_YES:
      return MHD_YES;
    case MFA_PHASE_RETURN_NO:
      GNUNET_break (0);
      return MHD_NO;
    }
  }
}
