/*
  This file is part of TALER
  Copyright (C) 2024, 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_aml-accounts-get.c
 * @brief Return summary information about accounts
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#include <microhttpd.h>
#include <pthread.h>
#include "taler/taler_json_lib.h"
#include "taler/taler_mhd_lib.h"
#include "taler/taler_signatures.h"
#include "taler-exchange-httpd.h"
#include "taler/taler_exchangedb_plugin.h"
#include "taler-exchange-httpd_aml-accounts-get.h"


/**
 * Maximum number of records we return in one go. Must be
 * small enough to ensure that XML/CSV encoded results stay
 * below the 16MB limit of GNUNET_malloc().
 */
#define MAX_RECORDS (1024 * 64)

#define CSV_HEADER \
        "File number,Customer,Comments,Risky,Acquisition date,Exit date\r\n"
#define CSV_FOOTER "\r\n"

#define XML_HEADER "<?xml version=\"1.0\"?>" \
        "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\""   \
        "xmlns:o=\"urn:schemas-microsoft-com:office:office\""        \
        "xmlns:x=\"urn:schemas-microsoft-com:office:excel\""         \
        "xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \
        "<Worksheet ss:Name=\"Sheet1\">"                                     \
        "<Table>" \
        "<Row>" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">File number</Data></Cell>" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Customer</Data></Cell>" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Comments</Data></Cell>" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Increased risk business relationship</Data></Cell>" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Acquisition date</Data></Cell>" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Exit date</Data></Cell>" \
        "</Row>\n"
#define XML_FOOTER "</Table></Worksheet></Workbook>"

/**
 * Closure for the record_cb().
 */
struct ResponseContext
{
  /**
   * Format of the response we are to generate.
   */
  enum
  {
    RCF_JSON,
    RCF_XML,
    RCF_CSV
  } format;

  /**
   * Where we store the response data.
   */
  union
  {
    /**
     * If @e format is #RCF_JSON.
     */
    json_t *json;

    /**
     * If @e format is #RCF_XML.
     */
    struct GNUNET_Buffer xml;

    /**
     * If @e format is #RCF_CSV.
     */
    struct GNUNET_Buffer csv;

  } details;
};


/**
 * Free resources from @a rc
 *
 * @param[in] rc context to clean up
 */
static void
free_rc (struct ResponseContext *rc)
{
  switch (rc->format)
  {
  case RCF_JSON:
    json_decref (rc->details.json);
    break;
  case RCF_XML:
    GNUNET_buffer_clear (&rc->details.xml);
    break;
  case RCF_CSV:
    GNUNET_buffer_clear (&rc->details.csv);
    break;
  }
}


/**
 * Escape @a str for encoding in XML.
 *
 * @param str string to escape
 * @return XML-encoded @a str
 */
static char *
escape_xml (const char *str)
{
  struct GNUNET_Buffer out = { 0 };
  const char *p = str;

  while (*p)
  {
    const char *esc = NULL;

    switch (*p)
    {
    case '&':
      esc = "&amp;";
      break;
    case '<':
      esc = "&lt;";
      break;
    case '>':
      esc = "&gt;";
      break;
    case '"':
      esc = "&quot;";
      break;
    case '\'':
      esc = "&apos;";
      break;
    }

    if (NULL != esc)
    {
      GNUNET_buffer_write_str (&out,
                               esc);
    }
    else
    {
      GNUNET_buffer_write (&out,
                           p,
                           1);
    }
    p++;
  }
  return GNUNET_buffer_reap_str (&out);
}


/**
 * Return account summary information.
 *
 * @param cls closure
 * @param row_id current row in AML status table
 * @param h_payto account for which the attribute data is stored
 * @param open_time when was the account opened formally,
 *          GNUNET_TIME_UNIT_FOREVER_TS if it was never opened
 * @param close_time when was the account formally closed,
 *          GNUNET_TIME_UNIT_ZERO_TS if it was never closed
 * @param comments comments on the account
 * @param high_risk is this a high-risk business relationship
 * @param to_investigate TRUE if this account should be investigated
 * @param payto the payto URI of the account
 */
static void
record_cb (
  void *cls,
  uint64_t row_id,
  const struct TALER_NormalizedPaytoHashP *h_payto,
  struct GNUNET_TIME_Timestamp open_time,
  struct GNUNET_TIME_Timestamp close_time,
  const char *comments,
  bool high_risk,
  bool to_investigate,
  struct TALER_FullPayto payto)
{
  struct ResponseContext *rc = cls;

  if ( (NULL == comments) &&
       (GNUNET_TIME_absolute_is_never (open_time.abs_time)) )
    comments = "transacted amounts below limits that trigger account opening";
  switch (rc->format)
  {
  case RCF_JSON:
    GNUNET_assert (
      0 ==
      json_array_append_new (
        rc->details.json,
        GNUNET_JSON_PACK (
          GNUNET_JSON_pack_data_auto ("h_payto",
                                      h_payto),
          TALER_JSON_pack_full_payto ("full_payto",
                                      payto),
          GNUNET_JSON_pack_bool ("high_risk",
                                 high_risk),
          GNUNET_JSON_pack_allow_null (
            GNUNET_JSON_pack_string ("comments",
                                     comments)),
          GNUNET_JSON_pack_int64 ("rowid",
                                  row_id),
          GNUNET_JSON_pack_timestamp ("open_time",
                                      open_time),
          GNUNET_JSON_pack_timestamp ("close_time",
                                      close_time),
          GNUNET_JSON_pack_bool ("to_investigate",
                                 to_investigate)
          )));
    return;
  case RCF_XML:
    {
      char *ecomments = NULL;

      if ( (NULL == comments) &&
           (GNUNET_TIME_absolute_is_never (open_time.abs_time)) )
        comments =
          "transacted amounts below limits that trigger account opening";
      ecomments = escape_xml (comments);
      GNUNET_buffer_write_fstr (&rc->details.xml,
                                "<Row>"
                                "<Cell><Data ss:Type=\"Number\">%llu</Data></Cell>"
                                "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
                                "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
                                "<Cell><Data ss:Type=\"Boolean\">%s</Data></Cell>"
                                "<Cell><Data ss:Type=\"DateTime\">%s</Data></Cell>"
                                "<Cell><Data ss:Type=\"DateTime\">%s</Data></Cell>"
                                "</Row>\n",
                                (unsigned long long) row_id,
                                payto.full_payto,
                                NULL == ecomments
                              ? ""
                              : ecomments,
                                high_risk ? "1" : "0",
                                GNUNET_TIME_absolute_is_never (open_time.
                                                               abs_time)
                              ? "-"
                              : GNUNET_TIME_timestamp2s (open_time),
                                GNUNET_TIME_absolute_is_never (close_time.
                                                               abs_time)
                              ? "-"
                              : GNUNET_TIME_timestamp2s (close_time));
      GNUNET_free (ecomments);
      break;
    } /* end case RCF_XML */
  case RCF_CSV:
    {
      char *ecomments = NULL;
      char otbuf[64];
      char ctbuf[64];

      GNUNET_snprintf (otbuf,
                       sizeof (otbuf),
                       "%s",
                       GNUNET_TIME_timestamp2s (open_time));
      GNUNET_snprintf (ctbuf,
                       sizeof (ctbuf),
                       "%s",
                       GNUNET_TIME_timestamp2s (close_time));
      if (NULL != comments)
      {
        size_t len = strlen (comments);
        size_t wpos = 0;

        /* Escape 'comments' to double '"' as per RFC 4180, 2.7. */
        ecomments = GNUNET_malloc (2 * len + 1);
        for (size_t off = 0; off<len; off++)
        {
          if ('"' == comments[off])
            ecomments[wpos++] = '"';
          ecomments[wpos++] = comments[off++];
        }

        GNUNET_buffer_write_fstr (&rc->details.xml,
                                  "%llu,%s,\"%s\",%s,%s,%s\r\n",
                                  (unsigned long long) row_id,
                                  payto.full_payto,
                                  NULL == ecomments
                              ? "-"
                              : ecomments,
                                  high_risk ? "X":" ",
                                  GNUNET_TIME_absolute_is_never (open_time.
                                                                 abs_time)
                              ? "-"
                              : otbuf,
                                  GNUNET_TIME_absolute_is_never (close_time.
                                                                 abs_time)
                              ? "-"
                              : ctbuf);
        GNUNET_free (ecomments);
        break;
      }
    } /* end case RCF_CSV */
  } /* end switch */
}


MHD_RESULT
TEH_handler_aml_accounts_get (
  struct TEH_RequestContext *rc,
  const struct TALER_AmlOfficerPublicKeyP *officer_pub,
  const char *const args[])
{
  struct ResponseContext rctx;
  int64_t limit = -20;
  uint64_t offset;
  enum TALER_EXCHANGE_YesNoAll open_filter;
  enum TALER_EXCHANGE_YesNoAll high_risk_filter;
  enum TALER_EXCHANGE_YesNoAll investigation_filter;

  memset (&rctx,
          0,
          sizeof (rctx));
  if (NULL != args[0])
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_NOT_FOUND,
      TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
      args[0]);
  }
  TALER_MHD_parse_request_snumber (rc->connection,
                                   "limit",
                                   &limit);
  if (limit > 0)
    offset = 0;
  else
    offset = INT64_MAX;
  TALER_MHD_parse_request_number (rc->connection,
                                  "offset",
                                  &offset);
  if (offset > INT64_MAX)
  {
    GNUNET_break_op (0); /* broken client */
    offset = INT64_MAX;
  }
  TALER_MHD_parse_request_yna (rc->connection,
                               "open",
                               TALER_EXCHANGE_YNA_ALL,
                               &open_filter);
  TALER_MHD_parse_request_yna (rc->connection,
                               "investigation",
                               TALER_EXCHANGE_YNA_ALL,
                               &investigation_filter);
  TALER_MHD_parse_request_yna (rc->connection,
                               "high_risk",
                               TALER_EXCHANGE_YNA_ALL,
                               &high_risk_filter);
  {
    const char *mime;

    mime = MHD_lookup_connection_value (rc->connection,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_ACCEPT);
    if (NULL == mime)
      mime = "application/json";
    if (0 == strcmp (mime,
                     "application/json"))
    {
      rctx.format = RCF_JSON;
      rctx.details.json = json_array ();
      GNUNET_assert (NULL != rctx.details.json);
    }
    else if (0 == strcmp (mime,
                          "application/vnd.ms-excel"))
    {
      rctx.format = RCF_XML;
      GNUNET_buffer_write_str (&rctx.details.xml,
                               XML_HEADER);
    }
    else if (0 == strcmp (mime,
                          "text/csv"))
    {
      rctx.format = RCF_CSV;
      GNUNET_buffer_write_str (&rctx.details.csv,
                               CSV_HEADER);
    }
    else
    {
      GNUNET_break_op (0);
      return TALER_MHD_REPLY_JSON_PACK (
        rc->connection,
        MHD_HTTP_NOT_ACCEPTABLE,
        GNUNET_JSON_pack_string ("hint",
                                 mime));
    }
  }

  {
    enum GNUNET_DB_QueryStatus qs;

    if (limit > MAX_RECORDS)
      limit = MAX_RECORDS;
    if (limit < -MAX_RECORDS)
      limit = -MAX_RECORDS;
    qs = TEH_plugin->select_kyc_accounts (
      TEH_plugin->cls,
      investigation_filter,
      open_filter,
      high_risk_filter,
      offset,
      limit,
      &record_cb,
      &rctx);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
    case GNUNET_DB_STATUS_SOFT_ERROR:
      free_rc (&rctx);
      GNUNET_break (0);
      return TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "select_kyx_accounts");
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      free_rc (&rctx);
      return TALER_MHD_reply_static (
        rc->connection,
        MHD_HTTP_NO_CONTENT,
        NULL,
        NULL,
        0);
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    } /* end switch (qs) */

    switch (rctx.format)
    {
    case RCF_JSON:
      return TALER_MHD_REPLY_JSON_PACK (
        rc->connection,
        MHD_HTTP_OK,
        GNUNET_JSON_pack_array_steal ("accounts",
                                      rctx.details.json));
    case RCF_XML:
      {
        struct MHD_Response *resp;
        MHD_RESULT mret;

        GNUNET_buffer_write_str (&rctx.details.xml,
                                 XML_FOOTER);
        /* FIXME: add support for compression */
        resp = MHD_create_response_from_buffer (rctx.details.xml.position,
                                                rctx.details.xml.mem,
                                                MHD_RESPMEM_MUST_FREE);
        TALER_MHD_add_global_headers (resp,
                                      false);
        GNUNET_break (MHD_YES ==
                      MHD_add_response_header (resp,
                                               MHD_HTTP_HEADER_CONTENT_TYPE,
                                               "application/vnd.ms-excel"));
        mret = MHD_queue_response (rc->connection,
                                   MHD_HTTP_OK,
                                   resp);
        MHD_destroy_response (resp);
        return mret;
      }
    case RCF_CSV:
      {
        struct MHD_Response *resp;
        MHD_RESULT mret;

        GNUNET_buffer_write_str (&rctx.details.csv,
                                 CSV_FOOTER);
        /* FIXME: add support for compression */
        resp = MHD_create_response_from_buffer (rctx.details.xml.position,
                                                rctx.details.xml.mem,
                                                MHD_RESPMEM_MUST_FREE);
        TALER_MHD_add_global_headers (resp,
                                      false);
        GNUNET_break (MHD_YES ==
                      MHD_add_response_header (resp,
                                               MHD_HTTP_HEADER_CONTENT_TYPE,
                                               "text/csv"));
        mret = MHD_queue_response (rc->connection,
                                   MHD_HTTP_OK,
                                   resp);
        MHD_destroy_response (resp);
        return mret;
      }
    } /* end switch (rctx.format) */
  }
  GNUNET_break (0);
  return MHD_NO;
}


/* end of taler-exchange-httpd_aml-accounts_get.c */
