/*
  This file is part of TALER
  (C) 2020-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_private-patch-instances-ID.c
 * @brief implementing PATCH /instances/$ID request handling
 * @author Christian Grothoff
 */
#include "platform.h"
#include "taler-merchant-httpd_private-patch-instances-ID.h"
#include "taler-merchant-httpd_helper.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_dbevents.h>
#include "taler-merchant-httpd_mfa.h"


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


/**
 * Free memory used by @a wm
 *
 * @param wm wire method to free
 */
static void
free_wm (struct TMH_WireMethod *wm)
{
  GNUNET_free (wm->payto_uri.full_payto);
  GNUNET_free (wm->wire_method);
  GNUNET_free (wm);
}


/**
 * PATCH configuration of an existing instance, given its configuration.
 *
 * @param mi instance to patch
 * @param mfa_check true if a MFA check is required
 * @param connection the MHD connection to handle
 * @param[in,out] hc context with further information about the request
 * @return MHD result code
 */
static MHD_RESULT
patch_instances_ID (struct TMH_MerchantInstance *mi,
                    bool mfa_check,
                    struct MHD_Connection *connection,
                    struct TMH_HandlerContext *hc)
{
  struct TALER_MERCHANTDB_InstanceSettings is;
  const char *name;
  struct TMH_WireMethod *wm_head = NULL;
  struct TMH_WireMethod *wm_tail = NULL;
  bool no_transfer_delay;
  bool no_pay_delay;
  bool no_refund_delay;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_string ("name",
                             &name),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("website",
                               (const char **) &is.website),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("email",
                               (const char **) &is.email),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("phone_number",
                               (const char **) &is.phone),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("logo",
                               (const char **) &is.logo),
      NULL),
    GNUNET_JSON_spec_json ("address",
                           &is.address),
    GNUNET_JSON_spec_json ("jurisdiction",
                           &is.jurisdiction),
    GNUNET_JSON_spec_bool ("use_stefan",
                           &is.use_stefan),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("default_pay_delay",
                                      &is.default_pay_delay),
      &no_pay_delay),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("default_refund_delay",
                                      &is.default_refund_delay),
      &no_refund_delay),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay",
                                      &is.default_wire_transfer_delay),
      &no_transfer_delay),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_time_rounder_interval (
        "default_wire_transfer_rounding_interval",
        &is.default_wire_transfer_rounding_interval),
      NULL),
    GNUNET_JSON_spec_end ()
  };
  enum GNUNET_DB_QueryStatus qs;

  GNUNET_assert (NULL != mi);
  memset (&is,
          0,
          sizeof (is));
  {
    enum GNUNET_GenericReturnValue res;

    res = TALER_MHD_parse_json_data (connection,
                                     hc->request_body,
                                     spec);
    if (GNUNET_OK != res)
      return (GNUNET_NO == res)
             ? MHD_YES
             : MHD_NO;
  }
  if (! TMH_location_object_valid (is.address))
  {
    GNUNET_break_op (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "address");
  }
  if ( (NULL != is.logo) &&
       (! TMH_image_data_url_valid (is.logo)) )
  {
    GNUNET_break_op (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "logo");
  }

  if (! TMH_location_object_valid (is.jurisdiction))
  {
    GNUNET_break_op (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "jurisdiction");
  }

  if (no_transfer_delay)
    is.default_wire_transfer_delay = mi->settings.default_wire_transfer_delay;
  if (no_pay_delay)
    is.default_pay_delay = mi->settings.default_pay_delay;
  if (no_refund_delay)
    is.default_refund_delay = mi->settings.default_refund_delay;
  if (GNUNET_TIME_relative_is_forever (is.default_pay_delay))
  {
    GNUNET_break_op (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "default_pay_delay");
  }
  if (GNUNET_TIME_relative_is_forever (is.default_refund_delay))
  {
    GNUNET_break_op (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "default_refund_delay");
  }
  if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay))
  {
    GNUNET_break_op (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "default_wire_transfer_delay");
  }

  if ( (NULL != is.phone) &&
       (NULL != mi->settings.phone) &&
       0 == strcmp (mi->settings.phone,
                    is.phone) )
    is.phone_validated = mi->settings.phone_validated;
  if ( (NULL != is.email) &&
       (NULL != mi->settings.email) &&
       0 == strcmp (mi->settings.email,
                    is.email) )
    is.email_validated = mi->settings.email_validated;
  if (mfa_check)
  {
    enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
    enum TEH_TanChannelSet mtc = TEH_mandatory_tan_channels;

    if ( (0 != (mtc & TEH_TCS_SMS)) &&
         (NULL == is.phone) )
    {
      GNUNET_break_op (0);
      GNUNET_JSON_parse_free (spec);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MISSING,
        "phone_number");
    }
    if ( (0 != (mtc & TEH_TCS_EMAIL)) &&
         (NULL == is.email) )
    {
      GNUNET_break_op (0);
      GNUNET_JSON_parse_free (spec);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MISSING,
        "email");
    }
    if ( (is.phone_validated) &&
         (0 != (mtc & TEH_TCS_SMS)) )
      mtc -= TEH_TCS_SMS;
    if ( (is.email_validated) &&
         (0 != (mtc & TEH_TCS_EMAIL)) )
      mtc -= TEH_TCS_EMAIL;
    switch (mtc)
    {
    case TEH_TCS_NONE:
      ret = GNUNET_OK;
      break;
    case TEH_TCS_SMS:
      if (NULL == is.phone)
      {
        GNUNET_break_op (0);
        GNUNET_JSON_parse_free (spec);
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_BAD_REQUEST,
          TALER_EC_GENERIC_PARAMETER_MISSING,
          "phone_number");
      }
      is.phone_validated = true;
      /* validate new phone number, if possible require old e-mail
         address for authorization */
      ret = TMH_mfa_challenges_do (hc,
                                   TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
                                   true,
                                   TALER_MERCHANT_MFA_CHANNEL_SMS,
                                   is.phone,
                                   0 == (TALER_MERCHANT_MFA_CHANNEL_EMAIL
                                         & TEH_mandatory_tan_channels)
                                   ? TALER_MERCHANT_MFA_CHANNEL_NONE
                                   : TALER_MERCHANT_MFA_CHANNEL_EMAIL,
                                   mi->settings.email,
                                   TALER_MERCHANT_MFA_CHANNEL_NONE);
      break;
    case TEH_TCS_EMAIL:
      is.email_validated = true;
      /* validate new e-mail address, if possible require old phone
         address for authorization */
      ret = TMH_mfa_challenges_do (hc,
                                   TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
                                   true,
                                   TALER_MERCHANT_MFA_CHANNEL_EMAIL,
                                   is.email,
                                   0 == (TALER_MERCHANT_MFA_CHANNEL_SMS
                                         & TEH_mandatory_tan_channels)
                                   ? TALER_MERCHANT_MFA_CHANNEL_NONE
                                   : TALER_MERCHANT_MFA_CHANNEL_SMS,
                                   mi->settings.phone,
                                   TALER_MERCHANT_MFA_CHANNEL_NONE);
      break;
    case TEH_TCS_EMAIL_AND_SMS:
      is.phone_validated = true;
      is.email_validated = true;
      /* To change both, we require both old and both new
         addresses to consent */
      ret = TMH_mfa_challenges_do (hc,
                                   TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION,
                                   true,
                                   TALER_MERCHANT_MFA_CHANNEL_SMS,
                                   mi->settings.phone,
                                   TALER_MERCHANT_MFA_CHANNEL_EMAIL,
                                   mi->settings.email,
                                   TALER_MERCHANT_MFA_CHANNEL_SMS,
                                   is.phone,
                                   TALER_MERCHANT_MFA_CHANNEL_EMAIL,
                                   is.email,
                                   TALER_MERCHANT_MFA_CHANNEL_NONE);
      break;
    }
    if (GNUNET_OK != ret)
    {
      GNUNET_JSON_parse_free (spec);
      return (GNUNET_NO == ret)
        ? MHD_YES
        : MHD_NO;
    }
  }

  for (unsigned int retry = 0; retry<MAX_RETRIES; retry++)
  {
    /* Cleanup after earlier loops */
    {
      struct TMH_WireMethod *wm;

      while (NULL != (wm = wm_head))
      {
        GNUNET_CONTAINER_DLL_remove (wm_head,
                                     wm_tail,
                                     wm);
        free_wm (wm);
      }
    }
    if (GNUNET_OK !=
        TMH_db->start (TMH_db->cls,
                       "PATCH /instances"))
    {
      GNUNET_JSON_parse_free (spec);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_START_FAILED,
                                         NULL);
    }
    /* Check for equality of settings */
    if (! ( (0 == strcmp (mi->settings.name,
                          name)) &&
            ((mi->settings.email == is.email) ||
             (NULL != is.email && NULL != mi->settings.email &&
              0 == strcmp (mi->settings.email,
                           is.email))) &&
            ((mi->settings.phone == is.phone) ||
             (NULL != is.phone && NULL != mi->settings.phone &&
              0 == strcmp (mi->settings.phone,
                           is.phone))) &&
            ((mi->settings.website == is.website) ||
             (NULL != is.website && NULL != mi->settings.website &&
              0 == strcmp (mi->settings.website,
                           is.website))) &&
            ((mi->settings.logo == is.logo) ||
             (NULL != is.logo && NULL != mi->settings.logo &&
              0 == strcmp (mi->settings.logo,
                           is.logo))) &&
            (1 == json_equal (mi->settings.address,
                              is.address)) &&
            (1 == json_equal (mi->settings.jurisdiction,
                              is.jurisdiction)) &&
            (mi->settings.use_stefan == is.use_stefan) &&
            (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay,
                                       ==,
                                       is.default_wire_transfer_delay)) &&
            (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay,
                                       ==,
                                       is.default_pay_delay)) ) )
    {
      is.id = mi->settings.id;
      is.name = GNUNET_strdup (name);
      qs = TMH_db->update_instance (TMH_db->cls,
                                    &is);
      GNUNET_free (is.name);
      if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
      {
        TMH_db->rollback (TMH_db->cls);
        if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
          goto retry;
        else
          goto giveup;
      }
    }
    qs = TMH_db->commit (TMH_db->cls);
retry:
    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
      continue;
    break;
  } /* for(... MAX_RETRIES) */
giveup:
  /* Update our 'settings' */
  GNUNET_free (mi->settings.name);
  GNUNET_free (mi->settings.email);
  GNUNET_free (mi->settings.phone);
  GNUNET_free (mi->settings.website);
  GNUNET_free (mi->settings.logo);
  json_decref (mi->settings.address);
  json_decref (mi->settings.jurisdiction);
  is.id = mi->settings.id;
  mi->settings = is;
  mi->settings.address = json_incref (mi->settings.address);
  mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction);
  mi->settings.name = GNUNET_strdup (name);
  if (NULL != is.email)
    mi->settings.email = GNUNET_strdup (is.email);
  if (NULL != is.phone)
    mi->settings.phone = GNUNET_strdup (is.phone);
  if (NULL != is.website)
    mi->settings.website = GNUNET_strdup (is.website);
  if (NULL != is.logo)
    mi->settings.logo = GNUNET_strdup (is.logo);

  GNUNET_JSON_parse_free (spec);
  TMH_reload_instances (mi->settings.id);
  return TALER_MHD_reply_static (connection,
                                 MHD_HTTP_NO_CONTENT,
                                 NULL,
                                 NULL,
                                 0);
}


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

  return patch_instances_ID (mi,
                             true,
                             connection,
                             hc);
}


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

  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);
  }
  if (mi->deleted)
  {
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_CONFLICT,
                                       TALER_EC_MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED,
                                       hc->infix);
  }
  return patch_instances_ID (mi,
                             false,
                             connection,
                             hc);
}


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