/*
  This file is part of TALER
  (C) 2020-2024 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-private-accounts.c
 * @brief implementing POST /private/accounts request handling
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include "taler-merchant-httpd_post-private-accounts.h"
#include "taler-merchant-httpd_helper.h"
#include "taler/taler_merchant_bank_lib.h"
#include <taler/taler_dbevents.h>
#include <taler/taler_json_lib.h>
#include "taler-merchant-httpd_mfa.h"
#include <regex.h>

/**
 * Maximum number of retries we do on serialization failures.
 */
#define MAX_RETRIES 5

/**
 * Closure for account_cb().
 */
struct PostAccountContext
{
  /**
   * Payto URI of the account to add (from the request).
   */
  struct TALER_FullPayto uri;

  /**
   * Hash of the wire details (@e uri and @e salt).
   * Set if @e have_same_account is true.
   */
  struct TALER_MerchantWireHashP h_wire;

  /**
   * Salt value used for hashing @e uri.
   * Set if @e have_same_account is true.
   */
  struct TALER_WireSaltP salt;

  /**
   * Credit facade URL from the request.
   */
  const char *credit_facade_url;

  /**
   * Facade credentials from the request.
   */
  const json_t *credit_facade_credentials;

  /**
   * Wire subject metadata from the request.
   */
  const char *extra_wire_subject_metadata;

  /**
   * True if we have ANY account already and thus require MFA.
   */
  bool have_any_account;

  /**
   * True if we have exact match already and thus require MFA.
   */
  bool have_same_account;

  /**
   * True if we have an account with the same normalized payto
   * already and thus the client can only do PATCH but not POST.
   */
  bool have_conflicting_account;
};


/**
 * Callback invoked with information about a bank account.
 *
 * @param cls closure with a `struct PostAccountContext`
 * @param merchant_priv private key of the merchant instance
 * @param ad details about the account
 */
static void
account_cb (
  void *cls,
  const struct TALER_MerchantPrivateKeyP *merchant_priv,
  const struct TALER_MERCHANTDB_AccountDetails *ad)
{
  struct PostAccountContext *pac = cls;

  if (! ad->active)
    return;
  pac->have_any_account = true;
  if ( (0 == TALER_full_payto_cmp (pac->uri,
                                   ad->payto_uri) ) &&
       ( (pac->credit_facade_credentials ==
          ad->credit_facade_credentials) ||
         ( (NULL != pac->credit_facade_credentials) &&
           (NULL != ad->credit_facade_credentials) &&
           (1 == json_equal (pac->credit_facade_credentials,
                             ad->credit_facade_credentials)) ) ) &&
       ( (pac->extra_wire_subject_metadata ==
          ad->extra_wire_subject_metadata) ||
         ( (NULL != pac->extra_wire_subject_metadata) &&
           (NULL != ad->extra_wire_subject_metadata) &&
           (0 == strcmp (pac->extra_wire_subject_metadata,
                         ad->extra_wire_subject_metadata)) ) ) &&
       ( (pac->credit_facade_url == ad->credit_facade_url) ||
         ( (NULL != pac->credit_facade_url) &&
           (NULL != ad->credit_facade_url) &&
           (0 == strcmp (pac->credit_facade_url,
                         ad->credit_facade_url)) ) ) )
  {
    pac->have_same_account = true;
    pac->salt = ad->salt;
    pac->h_wire = ad->h_wire;
    return;
  }

  if (0 == TALER_full_payto_normalize_and_cmp (pac->uri,
                                               ad->payto_uri) )
  {
    pac->have_conflicting_account = true;
    return;
  }
}


MHD_RESULT
TMH_private_post_account (const struct TMH_RequestHandler *rh,
                          struct MHD_Connection *connection,
                          struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi = hc->instance;
  struct PostAccountContext pac = { 0 };
  struct GNUNET_JSON_Specification ispec[] = {
    TALER_JSON_spec_full_payto_uri ("payto_uri",
                                    &pac.uri),
    GNUNET_JSON_spec_mark_optional (
      TALER_JSON_spec_web_url ("credit_facade_url",
                               &pac.credit_facade_url),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("extra_wire_subject_metadata",
                               &pac.extra_wire_subject_metadata),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_object_const ("credit_facade_credentials",
                                     &pac.credit_facade_credentials),
      NULL),
    GNUNET_JSON_spec_end ()
  };

  {
    enum GNUNET_GenericReturnValue res;

    res = TALER_MHD_parse_json_data (connection,
                                     hc->request_body,
                                     ispec);
    if (GNUNET_OK != res)
      return (GNUNET_NO == res)
             ? MHD_YES
             : MHD_NO;
  }

  {
    char *err;

    if (NULL !=
        (err = TALER_payto_validate (pac.uri)))
    {
      MHD_RESULT mret;

      GNUNET_break_op (0);
      mret = TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_BAD_REQUEST,
                                         TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
                                         err);
      GNUNET_free (err);
      return mret;
    }
  }
  if (! TALER_is_valid_subject_metadata_string (
        pac.extra_wire_subject_metadata))
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_BAD_REQUEST,
      TALER_EC_GENERIC_PARAMETER_MALFORMED,
      "extra_wire_subject_metadata");
  }

  {
    char *apt = GNUNET_strdup (TMH_allowed_payment_targets);
    char *method = TALER_payto_get_method (pac.uri.full_payto);
    bool ok;

    ok = false;
    for (const char *tok = strtok (apt,
                                   " ");
         NULL != tok;
         tok = strtok (NULL,
                       " "))
    {
      if (0 == strcmp ("*",
                       tok))
        ok = true;
      if (0 == strcmp (method,
                       tok))
        ok = true;
      if (ok)
        break;
    }
    GNUNET_free (method);
    GNUNET_free (apt);
    if (! ok)
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_BAD_REQUEST,
                                         TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
                                         "The payment target type is forbidden by policy");
    }
  }

  if ( (NULL != TMH_payment_target_regex) &&
       (0 !=
        regexec (&TMH_payment_target_re,
                 pac.uri.full_payto,
                 0,
                 NULL,
                 0)) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PAYTO_URI_MALFORMED,
                                       "The specific account is forbidden by policy");
  }

  if ( (NULL == pac.credit_facade_url) !=
       (NULL == pac.credit_facade_credentials) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MISSING,
                                       (NULL == pac.credit_facade_url)
                                       ? "credit_facade_url"
                                       : "credit_facade_credentials");
  }
  if ( (NULL != pac.credit_facade_url) ||
       (NULL != pac.credit_facade_credentials) )
  {
    struct TALER_MERCHANT_BANK_AuthenticationData auth;

    if (GNUNET_OK !=
        TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials,
                                             pac.credit_facade_url,
                                             &auth))
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_BAD_REQUEST,
                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                         "credit_facade_url or credit_facade_credentials");
    }
    TALER_MERCHANT_BANK_auth_free (&auth);
  }

  TMH_db->preflight (TMH_db->cls);
  for (unsigned int retries = 0;
       retries < MAX_RETRIES;
       retries++)
  {
    enum GNUNET_DB_QueryStatus qs;
    struct TMH_WireMethod *wm;

    TMH_db->rollback (TMH_db->cls);
    if (GNUNET_OK !=
        TMH_db->start (TMH_db->cls,
                       "post-account"))
    {
      GNUNET_break (0);
      break;
    }
    qs = TMH_db->select_accounts (TMH_db->cls,
                                  mi->settings.id,
                                  &account_cb,
                                  &pac);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      GNUNET_break (0);
      TMH_db->rollback (TMH_db->cls);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
                                         "select_accounts");
    case GNUNET_DB_STATUS_SOFT_ERROR:
      continue;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      break;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    }

    if (pac.have_same_account)
    {
      /* Idempotent request */
      TMH_db->rollback (TMH_db->cls);
      return TALER_MHD_REPLY_JSON_PACK (
        connection,
        MHD_HTTP_OK,
        GNUNET_JSON_pack_data_auto (
          "salt",
          &pac.salt),
        GNUNET_JSON_pack_data_auto (
          "h_wire",
          &pac.h_wire));

    }

    if (pac.have_conflicting_account)
    {
      /* Conflict, refuse request */
      TMH_db->rollback (TMH_db->cls);
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_CONFLICT,
                                         TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS,
                                         pac.uri.full_payto);
    }

    if (pac.have_any_account)
    {
      /* MFA needed */
      enum GNUNET_GenericReturnValue ret;

      TMH_db->rollback (TMH_db->cls);
      ret = TMH_mfa_check_simple (hc,
                                  TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
                                  mi);
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Account creation MFA check returned %d\n",
                  (int) ret);
      if (GNUNET_OK != ret)
      {
        return (GNUNET_NO == ret)
        ? MHD_YES
        : MHD_NO;
      }
    }

    /* convert provided payto URI into internal data structure with salts */
    wm = TMH_setup_wire_account (pac.uri,
                                 pac.credit_facade_url,
                                 pac.credit_facade_credentials);
    if (NULL != pac.extra_wire_subject_metadata)
      wm->extra_wire_subject_metadata
        = GNUNET_strdup (pac.extra_wire_subject_metadata);
    GNUNET_assert (NULL != wm);
    {
      struct TALER_MERCHANTDB_AccountDetails ad = {
        .payto_uri = wm->payto_uri,
        .salt = wm->wire_salt,
        .instance_id = mi->settings.id,
        .h_wire = wm->h_wire,
        .credit_facade_url = wm->credit_facade_url,
        .credit_facade_credentials = wm->credit_facade_credentials,
        .extra_wire_subject_metadata = wm->extra_wire_subject_metadata,
        .active = wm->active
      };
      struct GNUNET_DB_EventHeaderP es = {
        .size = htons (sizeof (es)),
        .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
      };

      qs = TMH_db->insert_account (TMH_db->cls,
                                   &ad);
      switch (qs)
      {
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        GNUNET_break (0);
        TMH_wire_method_free (wm);
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
          "insert_account");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        continue;
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        TMH_wire_method_free (wm);
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_STORE_FAILED,
          "insert_account");
      }

      TMH_db->event_notify (TMH_db->cls,
                            &es,
                            NULL,
                            0);
      qs = TMH_db->commit (TMH_db->cls);
      switch (qs)
      {
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        break;
      case GNUNET_DB_STATUS_SOFT_ERROR:
        TMH_wire_method_free (wm);
        continue;
      case GNUNET_DB_STATUS_HARD_ERROR:
        TMH_wire_method_free (wm);
        GNUNET_break (0);
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_COMMIT_FAILED,
          "post-account");
      }
      /* Finally, also update our running process */
      GNUNET_CONTAINER_DLL_insert (mi->wm_head,
                                   mi->wm_tail,
                                   wm);
      /* Note: we may not need to do this, as we notified
         about the account change above. But also hardly hurts. */
      TMH_reload_instances (mi->settings.id);
    }
    return TALER_MHD_REPLY_JSON_PACK (
      connection,
      MHD_HTTP_OK,
      GNUNET_JSON_pack_data_auto ("salt",
                                  &wm->wire_salt),
      GNUNET_JSON_pack_data_auto ("h_wire",
                                  &wm->h_wire));
  } /* end retries */
  TMH_db->rollback (TMH_db->cls);
  return TALER_MHD_reply_with_error (connection,
                                     MHD_HTTP_INTERNAL_SERVER_ERROR,
                                     TALER_EC_GENERIC_DB_SOFT_FAILURE,
                                     "post-accounts");
}


/* end of taler-merchant-httpd_post-private-accounts.c */
