/*
  This file is part of GNU Taler
  (C) 2021 Taler Systems SA

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

  GNU 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_private-post-instances-ID-auth.c
 * @brief implementing POST /instances/$ID/auth request handling
 * @author Christian Grothoff
 * @author Florian Dold
 */
#include "platform.h"
#include "taler-merchant-httpd_private-post-instances-ID-auth.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_mfa.h"
#include <taler/taler_json_lib.h>


/**
 * How often do we retry the simple INSERT database transaction?
 */
#define MAX_RETRIES 3


/**
 * Change the authentication settings of an instance.
 *
 * @param mi instance to modify settings of
 * @param connection the MHD connection to handle
 * @param[in,out] hc context with further information about the request
 * @param auth_override The authentication settings for this instance
 *   do not apply due to administrative action. Do not check
 *   against the DB value when updating the auth token.
 * @param tcs set of multi-factor authorizations required
 * @return MHD result code
 */
static MHD_RESULT
post_instances_ID_auth (struct TMH_MerchantInstance *mi,
                        struct MHD_Connection *connection,
                        struct TMH_HandlerContext *hc,
                        bool auth_override,
                        enum TEH_TanChannelSet tcs)
{
  struct TALER_MERCHANTDB_InstanceAuthSettings ias;
  const char *auth_pw = NULL;
  json_t *jauth = hc->request_body;

  {
    enum GNUNET_GenericReturnValue ret;

    ret = TMH_check_auth_config (connection,
                                 jauth,
                                 &auth_pw);
    if (GNUNET_OK != ret)
      return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
  }

  if ( (0 != (tcs & TEH_TCS_SMS) &&
        ( (NULL == mi->settings.phone) ||
          (NULL == TMH_helper_sms) ||
          (! mi->settings.phone_validated) ) ) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Cannot reset password: SMS factor not available\n");
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_FORBIDDEN,
      TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
      "phone_number");
  }
  if ( (0 != (tcs & TEH_TCS_EMAIL) &&
        ( (NULL == mi->settings.email) ||
          (NULL == TMH_helper_email) ||
          (! mi->settings.email_validated) ) ) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Cannot reset password: E-mail factor not available\n");
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_FORBIDDEN,
      TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
      "email");
  }
  if (! auth_override)
  {
    enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized

    switch (tcs)
    {
    case TEH_TCS_NONE:
      ret = GNUNET_OK;
      break;
    case TEH_TCS_SMS:
      ret = TMH_mfa_challenges_do (hc,
                                   TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION,
                                   true,
                                   TALER_MERCHANT_MFA_CHANNEL_SMS,
                                   mi->settings.phone,
                                   TALER_MERCHANT_MFA_CHANNEL_NONE);
      break;
    case TEH_TCS_EMAIL:
      ret = TMH_mfa_challenges_do (hc,
                                   TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION,
                                   true,
                                   TALER_MERCHANT_MFA_CHANNEL_EMAIL,
                                   mi->settings.email,
                                   TALER_MERCHANT_MFA_CHANNEL_NONE);
      break;
    case TEH_TCS_EMAIL_AND_SMS:
      ret = TMH_mfa_challenges_do (hc,
                                   TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION,
                                   true,
                                   TALER_MERCHANT_MFA_CHANNEL_SMS,
                                   mi->settings.phone,
                                   TALER_MERCHANT_MFA_CHANNEL_EMAIL,
                                   mi->settings.email,
                                   TALER_MERCHANT_MFA_CHANNEL_NONE);
      break;
    }
    if (GNUNET_OK != ret)
    {
      return (GNUNET_NO == ret)
        ? MHD_YES
        : MHD_NO;
    }
  }

  if (NULL == auth_pw)
  {
    memset (&ias.auth_salt,
            0,
            sizeof (ias.auth_salt));
    memset (&ias.auth_hash,
            0,
            sizeof (ias.auth_hash));
  }
  else
  {
    TMH_compute_auth (auth_pw,
                      &ias.auth_salt,
                      &ias.auth_hash);
  }

  /* Store the new auth information in the database */
  {
    enum GNUNET_DB_QueryStatus qs;

    for (unsigned int i = 0; i<MAX_RETRIES; i++)
    {
      if (GNUNET_OK !=
          TMH_db->start (TMH_db->cls,
                         "post /instances/$ID/auth"))
      {
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_INTERNAL_SERVER_ERROR,
                                           TALER_EC_GENERIC_DB_START_FAILED,
                                           NULL);
      }

      /* Make the authentication update a serializable operation.
         We first check that the authentication information
         that the caller's request authenticated with
         is still up to date.
         Otherwise, we've detected a conflicting update
         to the authentication. */
      {
        struct TALER_MERCHANTDB_InstanceAuthSettings db_ias;
        enum TALER_ErrorCode ec;

        qs = TMH_db->lookup_instance_auth (TMH_db->cls,
                                           mi->settings.id,
                                           &db_ias);

        switch (qs)
        {
        case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
          /* Instance got purged. */
          TMH_db->rollback (TMH_db->cls);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_NOT_FOUND,
                                             TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
                                             NULL);
        case GNUNET_DB_STATUS_SOFT_ERROR:
          TMH_db->rollback (TMH_db->cls);
          goto retry;
        case GNUNET_DB_STATUS_HARD_ERROR:
          TMH_db->rollback (TMH_db->cls);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                                             TALER_EC_GENERIC_DB_FETCH_FAILED,
                                             NULL);
        case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
          /* Success! */
          break;
        }

        if (! auth_override)
        {
          // FIXME are we sure what the scope here is?
          ec = TMH_check_token (hc->auth_token,
                                mi->settings.id,
                                &hc->auth_scope);
          if (TALER_EC_NONE != ec)
          {
            TMH_db->rollback (TMH_db->cls);
            GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                        "Refusing auth change: `%s'\n",
                        TALER_ErrorCode_get_hint (ec));
            return TALER_MHD_reply_with_error (connection,
                                               MHD_HTTP_UNAUTHORIZED,
                                               TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
                                               NULL);
          }
        }
      }

      qs = TMH_db->update_instance_auth (TMH_db->cls,
                                         mi->settings.id,
                                         &ias);
      if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
      {
        GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
        TMH_db->rollback (TMH_db->cls);
        if (GNUNET_DB_STATUS_HARD_ERROR == qs)
        {
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                                             TALER_EC_GENERIC_DB_FETCH_FAILED,
                                             NULL);
        }
        goto retry;
      }
      qs = TMH_db->commit (TMH_db->cls);
      if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
        qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
retry:
      if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
        break; /* success! -- or hard failure */
    } /* for .. MAX_RETRIES */
    if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    {
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_COMMIT_FAILED,
                                         NULL);
    }
    /* Finally, also update our running process */
    mi->auth = ias;
  }
  TMH_reload_instances (mi->settings.id);
  return TALER_MHD_reply_static (connection,
                                 MHD_HTTP_NO_CONTENT,
                                 NULL,
                                 NULL,
                                 0);
}


MHD_RESULT
TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
                                    struct MHD_Connection *connection,
                                    struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi = hc->instance;

  return post_instances_ID_auth (mi,
                                 connection,
                                 hc,
                                 false,
                                 TEH_TCS_NONE);
}


MHD_RESULT
TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
                                   struct MHD_Connection *connection,
                                   struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi = hc->instance;

  return post_instances_ID_auth (mi,
                                 connection,
                                 hc,
                                 false,
                                 TEH_mandatory_tan_channels);
}


MHD_RESULT
TMH_private_post_instances_default_ID_auth (
  const struct TMH_RequestHandler *rh,
  struct MHD_Connection *connection,
  struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi;
  MHD_RESULT ret;

  if ( (NULL == hc->infix) ||
       (0 == strcmp ("admin",
                     hc->infix)) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_FORBIDDEN,
      TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
      "not allowed for 'admin' account");
  }
  mi = TMH_lookup_instance (hc->infix);
  if (NULL == mi)
  {
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_NOT_FOUND,
      TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
      hc->infix);
  }
  ret = post_instances_ID_auth (mi,
                                connection,
                                hc,
                                true,
                                TEH_TCS_NONE);
  return ret;
}


/* end of taler-merchant-httpd_private-post-instances-ID-auth.c */
