/*
  This file is part of TALER
  Copyright (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 Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-exchange-httpd_kyc-upload.c
 * @brief Handle /kyc-upload/$ID request
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include "taler-exchange-httpd_common_kyc.h"
#include "taler-exchange-httpd_kyc-upload.h"

#define MAX_RETRIES 3

/**
 * Context used for processing the KYC upload req
 */
struct UploadContext
{

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

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

  /**
   * Access token for the KYC data of the account.
   */
  struct TALER_AccountAccessTokenP access_token;

  /**
   * Index of the measure this upload is for.
   */
  unsigned int measure_index;

  /**
   * HTTP status code to use with @e response.
   */
  unsigned int response_code;

  /**
   * Index in the legitimization measures table this ID
   * refers to.
   */
  unsigned long long legitimization_measure_serial_id;

  /**
   * Response to return, NULL if none yet.
   */
  struct MHD_Response *response;

  /**
   * Request we are processing.
   */
  struct TEH_RequestContext *rc;

  /**
   * Handle for async KYC processing.
   */
  struct TEH_KycMeasureRunContext *kat;

  /**
   * Uploaded data, in JSON.
   */
  const json_t *result;

  /**
   * Set by the transaction to the legitimization process row.
   */
  uint64_t legi_process_row;

  /**
   * Set by the transaction to the affected account payto hash.
   */
  struct TALER_NormalizedPaytoHashP h_payto;

  /**
   * Set by the transaction to true if the account is for a wallet.
   */
  bool is_wallet;

};


/**
 * Kept in a DLL.
 */
static struct UploadContext *uc_head;

/**
 * Kept in a DLL.
 */
static struct UploadContext *uc_tail;


void
TEH_kyc_upload_cleanup ()
{
  struct UploadContext *uc;

  while (NULL != (uc = uc_head))
  {
    MHD_resume_connection (uc->rc->connection);
    GNUNET_CONTAINER_DLL_remove (uc_head,
                                 uc_tail,
                                 uc);
  }
}


/**
 * Function called to clean up upload context.
 *
 * @param[in,out] rc context to clean up
 */
static void
upload_cleaner (struct TEH_RequestContext *rc)
{
  struct UploadContext *uc = rc->rh_ctx;

  if (NULL != uc->kat)
  {
    TEH_kyc_run_measure_cancel (uc->kat);
    uc->kat = NULL;
  }
  if (NULL != uc->response)
  {
    MHD_destroy_response (uc->response);
    uc->response = NULL;
  }
  GNUNET_free (uc);
}


/**
 * Function called after the KYC-AML trigger is done.
 *
 * @param cls closure
 * @param ec error code or 0 on success
 * @param detail error message or NULL on success / no info
 */
static void
aml_trigger_callback (
  void *cls,
  enum TALER_ErrorCode ec,
  const char *detail)
{
  struct UploadContext *uc = cls;

  uc->kat = NULL;
  GNUNET_assert (NULL == uc->response);
  if (TALER_EC_NONE != ec)
  {
    uc->response_code = TALER_ErrorCode_get_http_status (ec);
    if (0 == uc->response_code)
    {
      GNUNET_break (0);
      uc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
    }
    GNUNET_assert (uc->response_code != UINT_MAX);
    uc->response = TALER_MHD_make_error (
      ec,
      detail);
  }
  else
  {
    uc->response_code = MHD_HTTP_NO_CONTENT;
    uc->response = MHD_create_response_from_buffer_static (
      0,
      ""
      );
    TALER_MHD_add_global_headers (uc->response,
                                  true);
  }

  MHD_resume_connection (uc->rc->connection);
  GNUNET_CONTAINER_DLL_remove (uc_head,
                               uc_tail,
                               uc);
  TALER_MHD_daemon_trigger ();
}


/**
 * Do the main database transaction.
 *
 * @param cls closure with a `struct UploadContext`
 * @param connection MHD request which triggered the transaction
 * @param[out] mhd_ret set to MHD response status for @a connection,
 *             if transaction failed (!)
 */
static enum GNUNET_DB_QueryStatus
transact (void *cls,
          struct MHD_Connection *connection,
          MHD_RESULT *mhd_ret)
{
  struct UploadContext *uc = cls;
  struct TEH_RequestContext *rc = uc->rc;
  enum GNUNET_DB_QueryStatus qs;
  json_t *jmeasures;
  bool is_finished = false;
  size_t enc_attributes_len;
  void *enc_attributes;
  const char *error_message;
  char *form_name;
  enum TALER_ErrorCode ec;

  qs = TEH_plugin->lookup_completed_legitimization (
    TEH_plugin->cls,
    uc->legitimization_measure_serial_id,
    uc->measure_index,
    &uc->access_token,
    &uc->h_payto,
    &uc->is_wallet,
    &jmeasures,
    &is_finished,
    &enc_attributes_len,
    &enc_attributes);
  /* FIXME: not exactly performant/elegant, should eventually
     modify lookup_completed_legitimization to
     return something if we are purely pending? */
  switch (qs)
  {
  case GNUNET_DB_STATUS_HARD_ERROR:
    GNUNET_break (0);
    *mhd_ret = TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_FETCH_FAILED,
      "lookup_completed_legitimization");
    return qs;
  case GNUNET_DB_STATUS_SOFT_ERROR:
    return qs;
  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    qs = TEH_plugin->lookup_pending_legitimization (
      TEH_plugin->cls,
      uc->legitimization_measure_serial_id,
      &uc->access_token,
      &uc->h_payto,
      &jmeasures,
      &is_finished,
      &uc->is_wallet);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      GNUNET_break (0);
      *mhd_ret = TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "lookup_pending_legitimization");
      return qs;
    case GNUNET_DB_STATUS_SOFT_ERROR:
      return qs;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      GNUNET_break_op (0);
      *mhd_ret = TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_NOT_FOUND,
        TALER_EC_EXCHANGE_KYC_CHECK_REQUEST_UNKNOWN,
        NULL);
      return GNUNET_DB_STATUS_HARD_ERROR;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    }
    break;
  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    break;
  }

  if (NULL != enc_attributes)
  {
    json_t *xattributes;

    xattributes
      = TALER_CRYPTO_kyc_attributes_decrypt (
          &TEH_attribute_key,
          enc_attributes,
          enc_attributes_len);
    if (json_equal (xattributes,
                    uc->result))
    {
      /* Request is idempotent! */
      json_decref (xattributes);
      GNUNET_free (enc_attributes);
      json_decref (jmeasures);
      if (is_finished)
      {
        *mhd_ret = TALER_MHD_reply_static (
          rc->connection,
          MHD_HTTP_NO_CONTENT,
          NULL,
          NULL,
          0);
        return GNUNET_DB_STATUS_HARD_ERROR;
      }

      /* Note: problem below is not here, but likely some previous
         upload of the attributes failed badly in an AML program. */
      GNUNET_break (0);
      *mhd_ret = TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG,
        "attributes known, but legitimization process failed");
      return GNUNET_DB_STATUS_HARD_ERROR;
    }
    json_decref (xattributes);
    GNUNET_free (enc_attributes);
    json_decref (jmeasures);
    /* Form was already done with with different attributes, conflict! */
    GNUNET_break_op (0);
    *mhd_ret = TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_CONFLICT,
      TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED,
      NULL);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }
  if (is_finished)
  {
    /* This should not be possible (is_finished but NULL==enc_attributes),
       but also we should not run logic again if we are finished. */
    GNUNET_break_op (0);
    json_decref (jmeasures);
    *mhd_ret = TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_CONFLICT,
      TALER_EC_EXCHANGE_KYC_FORM_ALREADY_UPLOADED,
      NULL);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }
  ec = TALER_KYCLOGIC_check_form (jmeasures,
                                  uc->measure_index,
                                  uc->result,
                                  &form_name,
                                  &error_message);
  if (TALER_EC_NONE != ec)
  {
    GNUNET_break_op (0);
    json_decref (jmeasures);
    *mhd_ret = TALER_MHD_reply_with_ec (
      rc->connection,
      ec,
      error_message);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }
  json_decref (jmeasures);

  /* Setup KYC process (which we will then immediately 'finish') */
  qs = TEH_plugin->insert_kyc_requirement_process (
    TEH_plugin->cls,
    &uc->h_payto,
    uc->measure_index,
    uc->legitimization_measure_serial_id,
    form_name,
    NULL,       /* provider account ID */
    NULL,       /* provider legi ID */
    &uc->legi_process_row);
  switch (qs)
  {
  case GNUNET_DB_STATUS_HARD_ERROR:
    GNUNET_break (0);
    GNUNET_free (form_name);
    *mhd_ret = TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_STORE_FAILED,
      "insert_kyc_requirement_process");
    return GNUNET_DB_STATUS_HARD_ERROR;
  case GNUNET_DB_STATUS_SOFT_ERROR:
    GNUNET_free (form_name);
    return qs;
  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    GNUNET_break (0);
    GNUNET_free (form_name);
    *mhd_ret = TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
      "insert_kyc_requirement_process");
    return GNUNET_DB_STATUS_HARD_ERROR;
  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    break;
  }
  qs = TEH_kyc_store_attributes (
    uc->legi_process_row,
    &uc->h_payto,
    form_name,
    NULL /* provider account */,
    NULL /* provider legi ID */,
    GNUNET_TIME_UNIT_FOREVER_ABS,   /* expiration time */
    uc->result);
  GNUNET_free (form_name);
  switch (qs)
  {
  case GNUNET_DB_STATUS_HARD_ERROR:
    GNUNET_break (0);
    *mhd_ret = TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_STORE_FAILED,
      "kyc_store_attributes");
    return GNUNET_DB_STATUS_HARD_ERROR;
  case GNUNET_DB_STATUS_SOFT_ERROR:
    return qs;
  case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    GNUNET_break (0);
    *mhd_ret = TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_STORE_FAILED,
      "kyc_store_attributes");
    return GNUNET_DB_STATUS_HARD_ERROR;
  case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    return qs;
  }
  GNUNET_assert (0);
  *mhd_ret = MHD_NO;
  return GNUNET_DB_STATUS_HARD_ERROR;
}


MHD_RESULT
TEH_handler_kyc_upload (
  struct TEH_RequestContext *rc,
  const json_t *root,
  const char *const args[1])
{
  struct UploadContext *uc = rc->rh_ctx;
  const char *id = args[0];

  if (NULL == uc)
  {
    const char *slash;
    char dummy;

    uc = GNUNET_new (struct UploadContext);
    uc->rc = rc;
    uc->result = root;
    rc->rh_ctx = uc;
    rc->rh_cleaner = &upload_cleaner;
    slash = strchr (id, '-');
    if (NULL == slash)
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_NOT_FOUND,
        TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
        rc->url);
    }
    if (GNUNET_OK !=
        GNUNET_STRINGS_string_to_data (id,
                                       slash - id,
                                       &uc->access_token,
                                       sizeof (uc->access_token)))
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "Access token in ID is malformed");
    }
    if (2 !=
        sscanf (slash + 1,
                "%u-%llu%c",
                &uc->measure_index,
                &uc->legitimization_measure_serial_id,
                &dummy))
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "ID is malformed");
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "/kyc-upload received form submission\n");
    json_dumpf (root,
                stderr,
                JSON_INDENT (2));
  }
  if (NULL != uc->response)
  {
    return MHD_queue_response (rc->connection,
                               uc->response_code,
                               uc->response);

  }

  if (GNUNET_OK !=
      TEH_plugin->preflight (TEH_plugin->cls))
  {
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_SETUP_FAILED,
      NULL);
  }

  {
    MHD_RESULT mhd_ret = -1;

    if (GNUNET_OK !=
        TEH_DB_run_transaction (rc->connection,
                                "kyc-upload",
                                TEH_MT_REQUEST_KYC_UPLOAD,
                                &mhd_ret,
                                &transact,
                                uc))
      return mhd_ret;
  }

  uc->kat = TEH_kyc_run_measure_for_attributes (
    &rc->async_scope_id,
    uc->legi_process_row,
    &uc->h_payto,
    uc->is_wallet,
    &aml_trigger_callback,
    uc);
  if (NULL == uc->kat)
  {
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_EXCHANGE_KYC_GENERIC_AML_LOGIC_BUG,
      "TEH_kyc_finished");
  }
  MHD_suspend_connection (uc->rc->connection);
  GNUNET_CONTAINER_DLL_insert (uc_head,
                               uc_tail,
                               uc);
  return MHD_YES;
}
