/*
  This file is part of TALER
  (C) 2019--2026 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_get-private-orders.c
 * @brief implement GET /orders
 * @author Christian Grothoff
 *
 * FIXME-cleanup: consider introducing phases / state machine
 */
#include "taler/platform.h"
#include "taler-merchant-httpd_get-private-orders.h"
#include <taler/taler_merchant_util.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_dbevents.h>


/**
 * Sensible bound on TALER_MERCHANTDB_OrderFilter.delta
 */
#define MAX_DELTA 1024

#define CSV_HEADER \
        "Order ID,Row,YYYY-MM-DD,HH:MM,Timestamp,Amount,Refund amount,Pending refund amount,Summary,Refundable,Paid\r\n"
#define CSV_FOOTER "\r\n"

#define XML_HEADER "<?xml version=\"1.0\"?>" \
        "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \
        " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \
        " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \
        " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \
        " xmlns:o=\"urn:schemas-microsoft-com:office:office\""        \
        " xmlns:x=\"urn:schemas-microsoft-com:office:excel\""         \
        " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \
        "<Styles>" \
        "<Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd hh:mm:ss\"/></Style>" \
        "<Style ss:ID=\"Total\"><Font ss:Bold=\"1\"/></Style>" \
        "</Styles>\n" \
        "<Worksheet ss:Name=\"Orders\">\n"                            \
        "<Table>\n" \
        "<Row>\n" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Order ID</Data></Cell>\n" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Timestamp</Data></Cell>\n" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Price</Data></Cell>\n" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Refunded</Data></Cell>\n" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Summary</Data></Cell>\n" \
        "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Paid</Data></Cell>\n" \
        "</Row>\n"
#define XML_FOOTER "</Table></Worksheet></Workbook>"


/**
 * A pending GET /orders request.
 */
struct TMH_PendingOrder
{

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

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

  /**
   * Which connection was suspended.
   */
  struct MHD_Connection *con;

  /**
   * Which instance is this client polling? This also defines
   * which DLL this struct is part of.
   */
  struct TMH_MerchantInstance *mi;

  /**
   * At what time does this request expire? If set in the future, we
   * may wait this long for a payment to arrive before responding.
   */
  struct GNUNET_TIME_Absolute long_poll_timeout;

  /**
   * Filter to apply.
   */
  struct TALER_MERCHANTDB_OrderFilter of;

  /**
   * The array of orders (used for JSON and PDF/Typst).
   */
  json_t *pa;

  /**
   * Running total of order amounts, for totals row in CSV/XML/PDF.
   * Initialised to zero on first order seen.
   */
  struct TALER_AmountSet total_amount;

  /**
   * Running total of granted refund amounts.
   * Initialised to zero on first paid order seen.
   */
  struct TALER_AmountSet total_refund_amount;

  /**
   * Running total of pending refund amounts.
   * Initialised to zero on first paid order seen.
   */
  struct TALER_AmountSet total_pending_refund_amount;

  /**
   * The name of the instance we are querying for.
   */
  const char *instance_id;

  /**
   * Alias of @a of.summary_filter, but with memory to be released (owner).
   */
  char *summary_filter;

  /**
   * The result after adding the orders (#TALER_EC_NONE for okay, anything else for an error).
   */
  enum TALER_ErrorCode result;

  /**
   * Is the structure in the DLL
   */
  bool in_dll;

  /**
   * Output format requested by the client.
   */
  enum
  {
    POF_JSON,
    POF_CSV,
    POF_XML,
    POF_PDF
  } format;

  /**
   * Buffer used when format is #POF_CSV.
   */
  struct GNUNET_Buffer csv;

  /**
   * Buffer used when format is #POF_XML.
   */
  struct GNUNET_Buffer xml;

  /**
   * Async context used to run Typst (for #POF_PDF).
   */
  struct TALER_MHD_TypstContext *tc;

  /**
   * Pre-built MHD response (used when #POF_PDF Typst is done).
   */
  struct MHD_Response *response;

  /**
   * Task to timeout pending order.
   */
  struct GNUNET_SCHEDULER_Task *order_timeout_task;

  /**
   * HTTP status to return with @e response.
   */
  unsigned int http_status;
};


/**
 * DLL head for requests suspended waiting for Typst.
 */
static struct TMH_PendingOrder *pdf_head;

/**
 * DLL tail for requests suspended waiting for Typst.
 */
static struct TMH_PendingOrder *pdf_tail;


void
TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi)
{
  struct TMH_PendingOrder *po;

  while (NULL != (po = mi->po_head))
  {
    GNUNET_assert (po->in_dll);
    GNUNET_CONTAINER_DLL_remove (mi->po_head,
                                 mi->po_tail,
                                 po);
    MHD_resume_connection (po->con);
    po->in_dll = false;
  }
  if (NULL != mi->po_eh)
  {
    TMH_db->event_listen_cancel (mi->po_eh);
    mi->po_eh = NULL;
  }
}


void
TMH_force_get_orders_resume_typst ()
{
  struct TMH_PendingOrder *po;

  while (NULL != (po = pdf_head))
  {
    GNUNET_CONTAINER_DLL_remove (pdf_head,
                                 pdf_tail,
                                 po);
    MHD_resume_connection (po->con);
  }
}


/**
 * Task run to trigger timeouts on GET /orders requests with long polling.
 *
 * @param cls a `struct TMH_PendingOrder *`
 */
static void
order_timeout (void *cls)
{
  struct TMH_PendingOrder *po = cls;
  struct TMH_MerchantInstance *mi = po->mi;

  po->order_timeout_task = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Resuming long polled job due to timeout\n");
  GNUNET_assert (po->in_dll);
  GNUNET_CONTAINER_DLL_remove (mi->po_head,
                               mi->po_tail,
                               po);
  po->in_dll = false;
  MHD_resume_connection (po->con);
  TALER_MHD_daemon_trigger ();   /* we resumed, kick MHD */
}


/**
 * Cleanup our "context", where we stored the data
 * we are building for the response.
 *
 * @param ctx context to clean up, must be a `struct TMH_PendingOrder *`
 */
static void
cleanup (void *ctx)
{
  struct TMH_PendingOrder *po = ctx;

  if (po->in_dll)
  {
    struct TMH_MerchantInstance *mi = po->mi;

    GNUNET_CONTAINER_DLL_remove (mi->po_head,
                                 mi->po_tail,
                                 po);
    MHD_resume_connection (po->con);
  }
  if (NULL != po->order_timeout_task)
  {
    GNUNET_SCHEDULER_cancel (po->order_timeout_task);
    po->order_timeout_task = NULL;
  }
  json_decref (po->pa);
  TALER_amount_set_free (&po->total_amount);
  TALER_amount_set_free (&po->total_refund_amount);
  TALER_amount_set_free (&po->total_pending_refund_amount);
  GNUNET_free (po->summary_filter);
  switch (po->format)
  {
  case POF_JSON:
    break;
  case POF_CSV:
    GNUNET_buffer_clear (&po->csv);
    break;
  case POF_XML:
    GNUNET_buffer_clear (&po->xml);
    break;
  case POF_PDF:
    if (NULL != po->tc)
    {
      TALER_MHD_typst_cancel (po->tc);
      po->tc = NULL;
    }
    break;
  }
  if (NULL != po->response)
  {
    MHD_destroy_response (po->response);
    po->response = NULL;
  }
  GNUNET_free (po);
}


/**
 * Closure for #process_refunds_cb().
 */
struct ProcessRefundsClosure
{
  /**
   * Place where we accumulate the granted refunds.
   */
  struct TALER_Amount total_refund_amount;

  /**
   * Place where we accumulate the pending refunds.
   */
  struct TALER_Amount pending_refund_amount;

  /**
   * Set to an error code if something goes wrong.
   */
  enum TALER_ErrorCode ec;
};


/**
 * Function called with information about a refund.
 * It is responsible for summing up the refund amount.
 *
 * @param cls closure
 * @param refund_serial unique serial number of the refund
 * @param timestamp time of the refund (for grouping of refunds in the wallet UI)
 * @param coin_pub public coin from which the refund comes from
 * @param exchange_url URL of the exchange that issued @a coin_pub
 * @param rtransaction_id identificator of the refund
 * @param reason human-readable explanation of the refund
 * @param refund_amount refund amount which is being taken from @a coin_pub
 * @param pending true if the this refund was not yet processed by the wallet/exchange
 */
static void
process_refunds_cb (void *cls,
                    uint64_t refund_serial,
                    struct GNUNET_TIME_Timestamp timestamp,
                    const struct TALER_CoinSpendPublicKeyP *coin_pub,
                    const char *exchange_url,
                    uint64_t rtransaction_id,
                    const char *reason,
                    const struct TALER_Amount *refund_amount,
                    bool pending)
{
  struct ProcessRefundsClosure *prc = cls;

  if (GNUNET_OK !=
      TALER_amount_cmp_currency (&prc->total_refund_amount,
                                 refund_amount))
  {
    /* Database error, refunds in mixed currency in DB. Not OK! */
    prc->ec = TALER_EC_GENERIC_DB_INVARIANT_FAILURE;
    GNUNET_break (0);
    return;
  }
  GNUNET_assert (0 <=
                 TALER_amount_add (&prc->total_refund_amount,
                                   &prc->total_refund_amount,
                                   refund_amount));
  if (pending)
    GNUNET_assert (0 <=
                   TALER_amount_add (&prc->pending_refund_amount,
                                     &prc->pending_refund_amount,
                                     refund_amount));
}


/**
 * Add one order entry to the running order-amount total in @a po.
 * Sets po->result to #TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow.
 *
 * @param[in,out] po pending order accumulator
 * @param amount the order amount to add
 */
static void
accumulate_total (struct TMH_PendingOrder *po,
                  const struct TALER_Amount *amount)
{
  if (0 > TALER_amount_set_add (&po->total_amount,
                                amount,
                                NULL))
  {
    GNUNET_break (0);
    po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
  }
}


/**
 * Add refund amounts to the running refund totals in @a po.
 * Sets po->result to #TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow.
 * Only called for paid orders (where refund tracking is meaningful).
 *
 * @param[in,out] po pending order accumulator
 * @param refund granted refund amount for this order
 * @param pending pending (not-yet-processed) refund amount for this order
 */
static void
accumulate_refund_totals (struct TMH_PendingOrder *po,
                          const struct TALER_Amount *refund,
                          const struct TALER_Amount *pending)
{
  if (TALER_EC_NONE != po->result)
    return;
  if (0 > TALER_amount_set_add (&po->total_refund_amount,
                                refund,
                                NULL))
  {
    GNUNET_break (0);
    po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
    return;
  }
  if (0 > TALER_amount_set_add (&po->total_pending_refund_amount,
                                pending,
                                NULL))
  {
    GNUNET_break (0);
    po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
  }
}


/**
 * Add order details to our response accumulator.
 *
 * @param cls some closure
 * @param orig_order_id the order this is about
 * @param order_serial serial ID of the order
 * @param creation_time when was the order created
 */
static void
add_order (void *cls,
           const char *orig_order_id,
           uint64_t order_serial,
           struct GNUNET_TIME_Timestamp creation_time)
{
  struct TMH_PendingOrder *po = cls;
  json_t *contract_terms = NULL;
  struct TALER_PrivateContractHashP h_contract_terms;
  enum GNUNET_DB_QueryStatus qs;
  char *order_id = NULL;
  bool refundable = false;
  bool paid;
  bool wired;
  struct TALER_MERCHANT_Contract *contract = NULL;
  int16_t choice_index = -1;
  struct ProcessRefundsClosure prc = {
    .ec = TALER_EC_NONE
  };
  const struct TALER_Amount *amount;
  char amount_buf[128];
  char refund_buf[128];
  char pending_buf[128];

  /* Bail early if we already have an error */
  if (TALER_EC_NONE != po->result)
    return;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Adding order `%s' (%llu) to result set at instance `%s'\n",
              orig_order_id,
              (unsigned long long) order_serial,
              po->instance_id);
  qs = TMH_db->lookup_order_status_by_serial (TMH_db->cls,
                                              po->instance_id,
                                              order_serial,
                                              &order_id,
                                              &h_contract_terms,
                                              &paid);
  if (qs < 0)
  {
    GNUNET_break (0);
    po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
    return;
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    /* Contract terms don't exist, so the order cannot be paid. */
    paid = false;
    if (NULL == orig_order_id)
    {
      /* Got a DB trigger about a new proposal, but it
         was already deleted again. Just ignore the event. */
      return;
    }
    order_id = GNUNET_strdup (orig_order_id);
  }

  {
    /* First try to find the order in the contracts */
    uint64_t os;
    bool session_matches;

    qs = TMH_db->lookup_contract_terms3 (TMH_db->cls,
                                         po->instance_id,
                                         order_id,
                                         NULL,
                                         &contract_terms,
                                         &os,
                                         &paid,
                                         &wired,
                                         &session_matches,
                                         NULL,
                                         &choice_index);
    if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
      GNUNET_break (os == order_serial);
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    /* Might still be unclaimed, so try order table */
    struct TALER_MerchantPostDataHashP unused;

    paid = false;
    wired = false;
    qs = TMH_db->lookup_order (TMH_db->cls,
                               po->instance_id,
                               order_id,
                               NULL,
                               &unused,
                               &contract_terms);
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Order %llu disappeared during iteration. Skipping.\n",
                (unsigned long long) order_serial);
    goto cleanup;
  }
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
  {
    GNUNET_break (0);
    po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
    goto cleanup;
  }

  contract = TALER_MERCHANT_contract_parse (contract_terms,
                                            true);
  if (NULL == contract)
  {
    GNUNET_break (0);
    po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID;
    goto cleanup;
  }

  if (paid)
  {
    const struct TALER_Amount *brutto;

    switch (contract->version)
    {
    case TALER_MERCHANT_CONTRACT_VERSION_0:
      brutto = &contract->details.v0.brutto;
      break;
    case TALER_MERCHANT_CONTRACT_VERSION_1:
      {
        struct TALER_MERCHANT_ContractChoice *choice
          = &contract->details.v1.choices[choice_index];

        GNUNET_assert (choice_index < contract->details.v1.choices_len);
        brutto = &choice->amount;
      }
      break;
    default:
      GNUNET_break (0);
      goto cleanup;
    }
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (brutto->currency,
                                          &prc.total_refund_amount));
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (brutto->currency,
                                          &prc.pending_refund_amount));

    qs = TMH_db->lookup_refunds_detailed (TMH_db->cls,
                                          po->instance_id,
                                          &h_contract_terms,
                                          &process_refunds_cb,
                                          &prc);
    if (0 > qs)
    {
      GNUNET_break (0);
      po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
      goto cleanup;
    }
    if (TALER_EC_NONE != prc.ec)
    {
      GNUNET_break (0);
      po->result = prc.ec;
      goto cleanup;
    }
    if (0 > TALER_amount_cmp (&prc.total_refund_amount,
                              brutto) &&
        GNUNET_TIME_absolute_is_future (contract->refund_deadline.abs_time))
      refundable = true;
  }

  /* compute amount totals */
  amount = NULL;
  switch (contract->version)
  {
  case TALER_MERCHANT_CONTRACT_VERSION_0:
    {
      amount = &contract->details.v0.brutto;

      if (TALER_amount_is_zero (amount) &&
          (po->of.wired != TALER_EXCHANGE_YNA_ALL) )
      {
        /* If we are actually filtering by wire status,
           and the order was over an amount of zero,
           do not return it as wire status is not
           exactly meaningful for orders over zero. */
        goto cleanup;
      }

      /* Accumulate order total */
      if (paid)
        accumulate_total (po,
                          amount);
      if (TALER_EC_NONE != po->result)
        goto cleanup;
      /* Accumulate refund totals (only meaningful for paid orders) */
      if (paid)
      {
        accumulate_refund_totals (po,
                                  &prc.total_refund_amount,
                                  &prc.pending_refund_amount);
        if (TALER_EC_NONE != po->result)
          goto cleanup;
      }
    }
    break;
  case TALER_MERCHANT_CONTRACT_VERSION_1:
    if (-1 == choice_index)
      choice_index = 0; /* default choice */
    GNUNET_assert (choice_index < contract->details.v1.choices_len);
    {
      struct TALER_MERCHANT_ContractChoice *choice
        = &contract->details.v1.choices[choice_index];

      amount = &choice->amount;
      /* Accumulate order total */
      accumulate_total (po,
                        amount);
      if (TALER_EC_NONE != po->result)
        goto cleanup;
      /* Accumulate refund totals (only meaningful for paid orders) */
      if (paid)
      {
        accumulate_refund_totals (po,
                                  &prc.total_refund_amount,
                                  &prc.pending_refund_amount);
        if (TALER_EC_NONE != po->result)
          goto cleanup;
      }
    }
    break;
  default:
    GNUNET_break (0);
    po->result = TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION;
    goto cleanup;
  }

  /* convert amounts to strings (needed for some formats) */
  /* FIXME: use currency formatting rules in the future
     instead of TALER_amount2s for human readability... */
  strcpy (amount_buf,
          TALER_amount2s (amount));
  if (paid)
    strcpy (refund_buf,
            TALER_amount2s (&prc.total_refund_amount));
  if (paid)
    strcpy (pending_buf,
            TALER_amount2s (&prc.pending_refund_amount));

  switch (po->format)
  {
  case POF_JSON:
  case POF_PDF:
    GNUNET_assert (
      0 ==
      json_array_append_new (
        po->pa,
        GNUNET_JSON_PACK (
          GNUNET_JSON_pack_string ("order_id",
                                   contract->order_id),
          GNUNET_JSON_pack_uint64 ("row_id",
                                   order_serial),
          GNUNET_JSON_pack_timestamp ("timestamp",
                                      creation_time),
          TALER_JSON_pack_amount ("amount",
                                  amount),
          GNUNET_JSON_pack_allow_null (
            TALER_JSON_pack_amount (
              "refund_amount",
              paid
                ? &prc.total_refund_amount
                : NULL)),
          GNUNET_JSON_pack_allow_null (
            TALER_JSON_pack_amount (
              "pending_refund_amount",
              paid
                ? &prc.pending_refund_amount
                : NULL)),
          GNUNET_JSON_pack_string ("summary",
                                   contract->summary),
          GNUNET_JSON_pack_bool ("refundable",
                                 refundable),
          GNUNET_JSON_pack_bool ("paid",
                                 paid))));
    break;
  case POF_CSV:
    {
      size_t len = strlen (contract->summary);
      size_t wpos = 0;
      char *esummary;
      struct tm *tm;
      time_t t;

      /* Escape 'summary' to double '"' as per RFC 4180, 2.7. */
      esummary = GNUNET_malloc (2 * len + 1);
      for (size_t off = 0; off<len; off++)
      {
        if ('"' == contract->summary[off])
          esummary[wpos++] = '"';
        esummary[wpos++] = contract->summary[off];
      }
      t = GNUNET_TIME_timestamp_to_s (creation_time);
      tm = localtime (&t);
      GNUNET_buffer_write_fstr (
        &po->csv,
        "%s,%llu,%04u-%02u-%02u,%02u:%02u (%s),%llu,%s,%s,%s,\"%s\",%s,%s\r\n",
        contract->order_id,
        (unsigned long long) order_serial,
        tm->tm_year + 1900,
        tm->tm_mon + 1,
        tm->tm_mday,
        tm->tm_hour,
        tm->tm_min,
        tm->tm_zone,
        (unsigned long long) t,
        amount_buf,
        paid ? refund_buf : "",
        paid ? pending_buf : "",
        esummary,
        refundable ? "yes" : "no",
        paid ? "yes" : "no");
      GNUNET_free (esummary);
      break;
    }
  case POF_XML:
    {
      char *esummary = TALER_escape_xml (contract->summary);
      char creation_time_s[128];
      const struct tm *tm;
      time_t tt;

      tt = (time_t) GNUNET_TIME_timestamp_to_s (creation_time);
      tm = gmtime (&tt);
      strftime (creation_time_s,
                sizeof (creation_time_s),
                "%Y-%m-%dT%H:%M:%S",
                tm);
      GNUNET_buffer_write_fstr (
        &po->xml,
        "<Row>"
        "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
        "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"DateTime\">%s</Data></Cell>"
        "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
        "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
        "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
        "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>"
        "</Row>\n",
        contract->order_id,
        creation_time_s,
        amount_buf,
        paid ? refund_buf : "",
        NULL != esummary ? esummary : "",
        paid ? "TRUE" : "FALSE",
        paid ? "1" : "0");
      GNUNET_free (esummary);
    }
    break;
  }    /* end switch po->format */

cleanup:
  json_decref (contract_terms);
  GNUNET_free (order_id);
  if (NULL != contract)
  {
    TALER_MERCHANT_contract_free (contract);
    contract = NULL;
  }
}


/**
 * We have received a trigger from the database
 * that we should (possibly) resume some requests.
 *
 * @param cls a `struct TMH_MerchantInstance`
 * @param extra a `struct TMH_OrderChangeEventP`
 * @param extra_size number of bytes in @a extra
 */
static void
resume_by_event (void *cls,
                 const void *extra,
                 size_t extra_size)
{
  struct TMH_MerchantInstance *mi = cls;
  const struct TMH_OrderChangeEventDetailsP *oce = extra;
  struct TMH_PendingOrder *pn;
  enum TMH_OrderStateFlags osf;
  uint64_t order_serial_id;
  struct GNUNET_TIME_Timestamp date;

  if (sizeof (*oce) != extra_size)
  {
    GNUNET_break (0);
    return;
  }
  osf = (enum TMH_OrderStateFlags) (int) ntohl (oce->order_state);
  order_serial_id = GNUNET_ntohll (oce->order_serial_id);
  date = GNUNET_TIME_timestamp_ntoh (oce->execution_date);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Received notification about order %llu\n",
              (unsigned long long) order_serial_id);
  for (struct TMH_PendingOrder *po = mi->po_head;
       NULL != po;
       po = pn)
  {
    pn = po->next;
    if (! ( ( ((TALER_EXCHANGE_YNA_YES == po->of.paid) ==
               (0 != (osf & TMH_OSF_PAID))) ||
              (TALER_EXCHANGE_YNA_ALL == po->of.paid) ) &&
            ( ((TALER_EXCHANGE_YNA_YES == po->of.refunded) ==
               (0 != (osf & TMH_OSF_REFUNDED))) ||
              (TALER_EXCHANGE_YNA_ALL == po->of.refunded) ) &&
            ( ((TALER_EXCHANGE_YNA_YES == po->of.wired) ==
               (0 != (osf & TMH_OSF_WIRED))) ||
              (TALER_EXCHANGE_YNA_ALL == po->of.wired) ) ) )
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Client %p waits on different order type\n",
                  po);
      continue;
    }
    if (po->of.delta > 0)
    {
      if (order_serial_id < po->of.start_row)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Client %p waits on different order row\n",
                    po);
        continue;
      }
      if (GNUNET_TIME_timestamp_cmp (date,
                                     <,
                                     po->of.date))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Client %p waits on different order date\n",
                    po);
        continue;
      }
      po->of.delta--;
    }
    else
    {
      if (order_serial_id > po->of.start_row)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Client %p waits on different order row\n",
                    po);
        continue;
      }
      if (GNUNET_TIME_timestamp_cmp (date,
                                     >,
                                     po->of.date))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Client %p waits on different order date\n",
                    po);
        continue;
      }
      po->of.delta++;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Waking up client %p!\n",
                po);
    add_order (po,
               NULL,
               order_serial_id,
               date);
    GNUNET_assert (po->in_dll);
    GNUNET_CONTAINER_DLL_remove (mi->po_head,
                                 mi->po_tail,
                                 po);
    po->in_dll = false;
    MHD_resume_connection (po->con);
    TALER_MHD_daemon_trigger ();   /* we resumed, kick MHD */
  }
  if (NULL == mi->po_head)
  {
    TMH_db->event_listen_cancel (mi->po_eh);
    mi->po_eh = NULL;
  }
}


/**
 * There has been a change or addition of a new @a order_id.  Wake up
 * long-polling clients that may have been waiting for this event.
 *
 * @param mi the instance where the order changed
 * @param osf order state flags
 * @param date execution date of the order
 * @param order_serial_id serial ID of the order in the database
 */
void
TMH_notify_order_change (struct TMH_MerchantInstance *mi,
                         enum TMH_OrderStateFlags osf,
                         struct GNUNET_TIME_Timestamp date,
                         uint64_t order_serial_id)
{
  struct TMH_OrderChangeEventDetailsP oce = {
    .order_serial_id = GNUNET_htonll (order_serial_id),
    .execution_date = GNUNET_TIME_timestamp_hton (date),
    .order_state = htonl (osf)
  };
  struct TMH_OrderChangeEventP eh = {
    .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE),
    .header.size = htons (sizeof (eh)),
    .merchant_pub = mi->merchant_pub
  };

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Notifying clients of new order %llu at %s\n",
              (unsigned long long) order_serial_id,
              TALER_B2S (&mi->merchant_pub));
  TMH_db->event_notify (TMH_db->cls,
                        &eh.header,
                        &oce,
                        sizeof (oce));
}


/**
 * Transforms an (untrusted) input filter into a Postgresql LIKE filter.
 * Escapes "%" and "_" in the @a input and adds "%" at the beginning
 * and the end to turn the @a input into a suitable Postgresql argument.
 *
 * @param input text to turn into a substring match expression, or NULL
 * @return NULL if @a input was NULL, otherwise transformed @a input
 */
static char *
tr (const char *input)
{
  char *out;
  size_t slen;
  size_t wpos;

  if (NULL == input)
    return NULL;
  slen = strlen (input);
  out = GNUNET_malloc (slen * 2 + 3);
  wpos = 0;
  out[wpos++] = '%';
  for (size_t i = 0; i<slen; i++)
  {
    char c = input[i];

    if ( (c == '%') ||
         (c == '_') )
      out[wpos++] = '\\';
    out[wpos++] = c;
  }
  out[wpos++] = '%';
  GNUNET_assert (wpos < slen * 2 + 3);
  return out;
}


/**
 * Function called with the result of a #TALER_MHD_typst() operation.
 *
 * @param cls closure, a `struct TMH_PendingOrder *`
 * @param tr result of the operation
 */
static void
pdf_cb (void *cls,
        const struct TALER_MHD_TypstResponse *tr)
{
  struct TMH_PendingOrder *po = cls;

  po->tc = NULL;
  GNUNET_CONTAINER_DLL_remove (pdf_head,
                               pdf_tail,
                               po);
  if (TALER_EC_NONE != tr->ec)
  {
    po->http_status
      = TALER_ErrorCode_get_http_status (tr->ec);
    po->response
      = TALER_MHD_make_error (tr->ec,
                              tr->details.hint);
  }
  else
  {
    po->http_status = MHD_HTTP_OK;
    po->response = TALER_MHD_response_from_pdf_file (tr->details.filename);
  }
  MHD_resume_connection (po->con);
  TALER_MHD_daemon_trigger ();
}


/**
 * Build the final response for a completed (non-long-poll) request and
 * queue it on @a connection.
 *
 * Handles all formats (JSON, CSV, XML, PDF).  For PDF this may suspend
 * the connection while Typst runs asynchronously; in that case the caller
 * must return #MHD_YES immediately.
 *
 * @param po the pending order state (already fully populated)
 * @param connection the MHD connection
 * @param mi the merchant instance
 * @return MHD result code
 */
static MHD_RESULT
reply_orders (struct TMH_PendingOrder *po,
              struct MHD_Connection *connection,
              struct TMH_MerchantInstance *mi)
{
  char total_buf[128];
  char refund_buf[128];
  char pending_buf[128];

  switch (po->format)
  {
  case POF_JSON:
    return TALER_MHD_REPLY_JSON_PACK (
      connection,
      MHD_HTTP_OK,
      GNUNET_JSON_pack_array_incref ("orders",
                                     po->pa));
  case POF_CSV:
    {
      struct MHD_Response *resp;
      MHD_RESULT mret;

      for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
      {
        struct TALER_Amount *tai = &po->total_amount.taa[i];
        const struct TALER_Amount *r;

        strcpy (total_buf,
                TALER_amount2s (tai));
        r = TALER_amount_set_find (tai->currency,
                                   &po->total_refund_amount);
        strcpy (refund_buf,
                TALER_amount2s (r));
        r = TALER_amount_set_find (tai->currency,
                                   &po->total_pending_refund_amount);
        strcpy (pending_buf,
                TALER_amount2s (r));

        GNUNET_buffer_write_fstr (
          &po->csv,
          "Total (paid %s only),,,,%s,%s,%s,,,\r\n",
          tai->currency,
          total_buf,
          refund_buf,
          pending_buf);
      }
      GNUNET_buffer_write_str (&po->csv,
                               CSV_FOOTER);
      resp = MHD_create_response_from_buffer (po->csv.position,
                                              po->csv.mem,
                                              MHD_RESPMEM_MUST_COPY);
      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 (connection,
                                 MHD_HTTP_OK,
                                 resp);
      MHD_destroy_response (resp);
      return mret;
    }
  case POF_XML:
    {
      struct MHD_Response *resp;
      MHD_RESULT mret;

      for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
      {
        struct TALER_Amount *tai = &po->total_amount.taa[i];
        const struct TALER_Amount *r;

        strcpy (total_buf,
                TALER_amount2s (tai));
        r = TALER_amount_set_find (tai->currency,
                                   &po->total_refund_amount);
        strcpy (refund_buf,
                TALER_amount2s (r));
        r = TALER_amount_set_find (tai->currency,
                                   &po->total_pending_refund_amount);
        strcpy (pending_buf,
                TALER_amount2s (r));

        /* Append totals row with paid and refunded amount columns */
        GNUNET_buffer_write_fstr (
          &po->xml,
          "<Row>"
          "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">Total (paid %s only)</Data></Cell>"
          "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>"
          "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>"
          "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>"
          "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>"
          "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>"
          "</Row>\n",
          tai->currency,
          total_buf,
          refund_buf);
      }
      GNUNET_buffer_write_str (&po->xml,
                               XML_FOOTER);
      resp = MHD_create_response_from_buffer (po->xml.position,
                                              po->xml.mem,
                                              MHD_RESPMEM_MUST_COPY);
      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 (connection,
                                 MHD_HTTP_OK,
                                 resp);
      MHD_destroy_response (resp);
      return mret;
    }
  case POF_PDF:
    {
      /* Build the JSON document for Typst, passing all totals */
      json_t *root;
      struct TALER_MHD_TypstDocument doc;
      json_t *ta = json_array ();
      json_t *ra = json_array ();
      json_t *pa = json_array ();

      GNUNET_assert (NULL != ta);
      GNUNET_assert (NULL != ra);
      GNUNET_assert (NULL != pa);
      for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
      {
        struct TALER_Amount *tai = &po->total_amount.taa[i];
        const struct TALER_Amount *r;

        GNUNET_assert (0 ==
                       json_array_append_new (ta,
                                              TALER_JSON_from_amount (tai)));
        r = TALER_amount_set_find (tai->currency,
                                   &po->total_refund_amount);
        GNUNET_assert (0 ==
                       json_array_append_new (ra,
                                              TALER_JSON_from_amount (r)));
        r = TALER_amount_set_find (tai->currency,
                                   &po->total_pending_refund_amount);
        GNUNET_assert (0 ==
                       json_array_append_new (pa,
                                              TALER_JSON_from_amount (r)));
      }
      root = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("business_name",
                                 mi->settings.name),
        GNUNET_JSON_pack_array_incref ("orders",
                                       po->pa),
        GNUNET_JSON_pack_array_steal ("total_amounts",
                                      ta),
        GNUNET_JSON_pack_array_steal ("total_refund_amounts",
                                      ra),
        GNUNET_JSON_pack_array_steal ("total_pending_refund_amounts",
                                      pa));
      doc.form_name = "orders";
      doc.form_version = "0.0.0";
      doc.data = root;

      po->tc = TALER_MHD_typst (TALER_MERCHANT_project_data (),
                                TMH_cfg,
                                false,  /* remove on exit */
                                "merchant",
                                1,     /* one document */
                                &doc,
                                &pdf_cb,
                                po);
      json_decref (root);
      if (NULL == po->tc)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Client requested PDF, but Typst is unavailable\n");
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_NOT_IMPLEMENTED,
          TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK,
          NULL);
      }
      GNUNET_CONTAINER_DLL_insert (pdf_head,
                                   pdf_tail,
                                   po);
      MHD_suspend_connection (connection);
      return MHD_YES;
    }
  } /* end switch */
  GNUNET_assert (0);
  return MHD_NO;
}


/**
 * Handle a GET "/orders" request.
 *
 * @param rh context of the handler
 * @param connection the MHD connection to handle
 * @param[in,out] hc context with further information about the request
 * @return MHD result code
 */
MHD_RESULT
TMH_private_get_orders (const struct TMH_RequestHandler *rh,
                        struct MHD_Connection *connection,
                        struct TMH_HandlerContext *hc)
{
  struct TMH_PendingOrder *po = hc->ctx;
  struct TMH_MerchantInstance *mi = hc->instance;
  enum GNUNET_DB_QueryStatus qs;

  if (NULL != po)
  {
    if (TALER_EC_NONE != po->result)
    {
      /* Resumed from long-polling with error */
      GNUNET_break (0);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         po->result,
                                         NULL);
    }
    if (POF_PDF == po->format)
    {
      /* resumed from long-polling or from Typst PDF generation */
      /* We really must have a response in this case */
      if (NULL == po->response)
      {
        GNUNET_break (0);
        return MHD_NO;
      }
      return MHD_queue_response (connection,
                                 po->http_status,
                                 po->response);
    }
    return reply_orders (po,
                         connection,
                         mi);
  }
  po = GNUNET_new (struct TMH_PendingOrder);
  hc->ctx = po;
  hc->cc = &cleanup;
  po->con = connection;
  po->pa = json_array ();
  GNUNET_assert (NULL != po->pa);
  po->instance_id = mi->settings.id;
  po->mi = mi;

  /* Determine desired output format from Accept header */
  {
    const char *mime;

    mime = MHD_lookup_connection_value (connection,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_ACCEPT);
    if (NULL == mime)
      mime = "application/json";
    if (0 == strcmp (mime,
                     "*/*"))
      mime = "application/json";
    if (0 == strcmp (mime,
                     "application/json"))
    {
      po->format = POF_JSON;
    }
    else if (0 == strcmp (mime,
                          "text/csv"))
    {
      po->format = POF_CSV;
      GNUNET_buffer_write_str (&po->csv,
                               CSV_HEADER);
    }
    else if (0 == strcmp (mime,
                          "application/vnd.ms-excel"))
    {
      po->format = POF_XML;
      GNUNET_buffer_write_str (&po->xml,
                               XML_HEADER);
    }
    else if (0 == strcmp (mime,
                          "application/pdf"))
    {
      po->format = POF_PDF;
    }
    else
    {
      GNUNET_break_op (0);
      return TALER_MHD_REPLY_JSON_PACK (
        connection,
        MHD_HTTP_NOT_ACCEPTABLE,
        GNUNET_JSON_pack_string ("hint",
                                 mime));
    }
  }

  if (! (TALER_MHD_arg_to_yna (connection,
                               "paid",
                               TALER_EXCHANGE_YNA_ALL,
                               &po->of.paid)) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "paid");
  }
  if (! (TALER_MHD_arg_to_yna (connection,
                               "refunded",
                               TALER_EXCHANGE_YNA_ALL,
                               &po->of.refunded)) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "refunded");
  }
  if (! (TALER_MHD_arg_to_yna (connection,
                               "wired",
                               TALER_EXCHANGE_YNA_ALL,
                               &po->of.wired)) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "wired");
  }
  po->of.delta = -20;
  /* deprecated in protocol v12 */
  TALER_MHD_parse_request_snumber (connection,
                                   "delta",
                                   &po->of.delta);
  /* since protocol v12 */
  TALER_MHD_parse_request_snumber (connection,
                                   "limit",
                                   &po->of.delta);
  if ( (-MAX_DELTA > po->of.delta) ||
       (po->of.delta > MAX_DELTA) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "limit");
  }
  {
    const char *date_s_str;

    date_s_str = MHD_lookup_connection_value (connection,
                                              MHD_GET_ARGUMENT_KIND,
                                              "date_s");
    if (NULL == date_s_str)
    {
      if (po->of.delta > 0)
        po->of.date = GNUNET_TIME_UNIT_ZERO_TS;
      else
        po->of.date = GNUNET_TIME_UNIT_FOREVER_TS;
    }
    else
    {
      char dummy;
      unsigned long long ll;

      if (1 !=
          sscanf (date_s_str,
                  "%llu%c",
                  &ll,
                  &dummy))
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_BAD_REQUEST,
                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                           "date_s");
      }

      po->of.date = GNUNET_TIME_absolute_to_timestamp (
        GNUNET_TIME_absolute_from_s (ll));
      if (GNUNET_TIME_absolute_is_never (po->of.date.abs_time))
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_BAD_REQUEST,
                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                           "date_s");
      }
    }
  }
  if (po->of.delta > 0)
  {
    struct GNUNET_TIME_Relative duration
      = GNUNET_TIME_UNIT_FOREVER_REL;
    struct GNUNET_TIME_Absolute cut_off;

    TALER_MHD_parse_request_rel_time (connection,
                                      "max_age",
                                      &duration);
    cut_off = GNUNET_TIME_absolute_subtract (GNUNET_TIME_absolute_get (),
                                             duration);
    po->of.date = GNUNET_TIME_timestamp_max (
      po->of.date,
      GNUNET_TIME_absolute_to_timestamp (cut_off));
  }
  if (po->of.delta > 0)
    po->of.start_row = 0;
  else
    po->of.start_row = INT64_MAX;
  /* deprecated in protocol v12 */
  TALER_MHD_parse_request_number (connection,
                                  "start",
                                  &po->of.start_row);
  /* since protocol v12 */
  TALER_MHD_parse_request_number (connection,
                                  "offset",
                                  &po->of.start_row);
  if (INT64_MAX < po->of.start_row)
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "offset");
  }
  po->summary_filter = tr (MHD_lookup_connection_value (connection,
                                                        MHD_GET_ARGUMENT_KIND,
                                                        "summary_filter"));
  po->of.summary_filter = po->summary_filter; /* just an (read-only) alias! */
  po->of.session_id
    = MHD_lookup_connection_value (connection,
                                   MHD_GET_ARGUMENT_KIND,
                                   "session_id");
  po->of.fulfillment_url
    = MHD_lookup_connection_value (connection,
                                   MHD_GET_ARGUMENT_KIND,
                                   "fulfillment_url");
  TALER_MHD_parse_request_timeout (connection,
                                   &po->long_poll_timeout);
  if (GNUNET_TIME_absolute_is_never (po->long_poll_timeout))
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "timeout_ms");
  }
  if ( (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) &&
       (NULL == mi->po_eh) )
  {
    struct TMH_OrderChangeEventP change_eh = {
      .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE),
      .header.size = htons (sizeof (change_eh)),
      .merchant_pub = mi->merchant_pub
    };

    mi->po_eh = TMH_db->event_listen (TMH_db->cls,
                                      &change_eh.header,
                                      GNUNET_TIME_UNIT_FOREVER_REL,
                                      &resume_by_event,
                                      mi);
  }

  po->of.timeout = GNUNET_TIME_absolute_get_remaining (po->long_poll_timeout);

  qs = TMH_db->lookup_orders (TMH_db->cls,
                              po->instance_id,
                              &po->of,
                              &add_order,
                              po);
  if (0 > qs)
  {
    GNUNET_break (0);
    po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
  }
  if (TALER_EC_NONE != po->result)
  {
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
                                       po->result,
                                       NULL);
  }
  if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) &&
       (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) )
  {
    GNUNET_assert (NULL == po->order_timeout_task);
    po->order_timeout_task
      = GNUNET_SCHEDULER_add_at (po->long_poll_timeout,
                                 &order_timeout,
                                 po);
    GNUNET_CONTAINER_DLL_insert (mi->po_head,
                                 mi->po_tail,
                                 po);
    po->in_dll = true;
    MHD_suspend_connection (connection);
    return MHD_YES;
  }
  return reply_orders (po,
                       connection,
                       mi);
}


/* end of taler-merchant-httpd_get-private-orders.c */
