/*
  This file is part of TALER
  (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 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-statistics-report-transactions.c
 * @brief implement GET /statistics-report/transactions
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include "taler-merchant-httpd_get-private-statistics-report-transactions.h"
#include <gnunet/gnunet_json_lib.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_mhd_lib.h>


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

  /**
   * Stored in a DLL while suspended.
   */
  struct ResponseContext *next;

  /**
   * Stored in a DLL while suspended.
   */
  struct ResponseContext *prev;

  /**
   * Context for this request.
   */
  struct TMH_HandlerContext *hc;

  /**
   * Async context used to run Typst.
   */
  struct TALER_MHD_TypstContext *tc;

  /**
   * Response to return.
   */
  struct MHD_Response *response;

  /**
   * Time when we started processing the request.
   */
  struct GNUNET_TIME_Timestamp now;

  /**
   * Period of each bucket.
   */
  struct GNUNET_TIME_Relative period;

  /**
   * Granularity of the buckets. Matches @e period.
   */
  const char *granularity;

  /**
   * Number of buckets to return.
   */
  uint64_t count;

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

  /**
   * Length of the @e labels array.
   */
  unsigned int labels_cnt;

  /**
   * Array of labels for the chart.
   */
  char **labels;

  /**
   * Data groups for the chart.
   */
  json_t *data_groups;

  /**
   * #GNUNET_YES if connection was suspended,
   * #GNUNET_SYSERR if we were resumed on shutdown.
   */
  enum GNUNET_GenericReturnValue suspended;

};


/**
 * DLL of requests awaiting Typst.
 */
static struct ResponseContext *rctx_head;

/**
 * DLL of requests awaiting Typst.
 */
static struct ResponseContext *rctx_tail;


void
TMH_handler_statistic_report_transactions_cleanup ()
{
  struct ResponseContext *rctx;

  while (NULL != (rctx = rctx_head))
  {
    GNUNET_CONTAINER_DLL_remove (rctx_head,
                                 rctx_tail,
                                 rctx);
    rctx->suspended = GNUNET_SYSERR;
    MHD_resume_connection (rctx->hc->connection);
  }
}


/**
 * Free resources from @a ctx
 *
 * @param[in] ctx the `struct ResponseContext` to clean up
 */
static void
free_rc (void *ctx)
{
  struct ResponseContext *rctx = ctx;

  if (NULL != rctx->tc)
  {
    TALER_MHD_typst_cancel (rctx->tc);
    rctx->tc = NULL;
  }
  if (NULL != rctx->response)
  {
    MHD_destroy_response (rctx->response);
    rctx->response = NULL;
  }
  for (unsigned int i = 0; i<rctx->labels_cnt; i++)
    GNUNET_free (rctx->labels[i]);
  GNUNET_array_grow (rctx->labels,
                     rctx->labels_cnt,
                     0);
  json_decref (rctx->data_groups);
  GNUNET_free (rctx);
}


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

  rctx->tc = NULL;
  GNUNET_CONTAINER_DLL_remove (rctx_head,
                               rctx_tail,
                               rctx);
  rctx->suspended = GNUNET_NO;
  MHD_resume_connection (rctx->hc->connection);
  TALER_MHD_daemon_trigger ();
  if (TALER_EC_NONE != tr->ec)
  {
    rctx->http_status
      = TALER_ErrorCode_get_http_status (tr->ec);
    rctx->response
      = TALER_MHD_make_error (tr->ec,
                              tr->details.hint);
    return;
  }
  rctx->http_status
    = MHD_HTTP_OK;
  rctx->response
    = TALER_MHD_response_from_pdf_file (tr->details.filename);
}


/**
 * Typically called by `lookup_statistics_amount_by_bucket2`.
 *
 * @param[in,out] cls our `struct ResponseContext` to update
 * @param bucket_start start time of the bucket
 * @param amounts_len the length of @a amounts array
 * @param amounts the cumulative amounts in the bucket
 */
static void
amount_by_bucket (void *cls,
                  struct GNUNET_TIME_Timestamp bucket_start,
                  unsigned int amounts_len,
                  const struct TALER_Amount amounts[static amounts_len])
{
  struct ResponseContext *rctx = cls;
  json_t *values;

  for (unsigned int i = 0; i<amounts_len; i++)
  {
    bool found = false;

    for (unsigned int j = 0; j<rctx->labels_cnt; j++)
    {
      if (0 == strcmp (amounts[i].currency,
                       rctx->labels[j]))
      {
        found = true;
        break;
      }
    }
    if (! found)
    {
      GNUNET_array_append (rctx->labels,
                           rctx->labels_cnt,
                           GNUNET_strdup (amounts[i].currency));
    }
  }

  values = json_array ();
  GNUNET_assert (NULL != values);
  for (unsigned int i = 0; i<rctx->labels_cnt; i++)
  {
    const char *label = rctx->labels[i];
    double d = 0.0;

    for (unsigned int j = 0; j<amounts_len; j++)
    {
      const struct TALER_Amount *a = &amounts[j];

      if (0 != strcmp (amounts[j].currency,
                       label))
        continue;
      d = a->value * 1.0
          + (a->fraction * 1.0 / TALER_AMOUNT_FRAC_BASE);
      break;
    } /* for all amounts */
    GNUNET_assert (0 ==
                   json_array_append_new (values,
                                          json_real (d)));
  } /* for all labels */

  {
    json_t *dg;

    dg = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_timestamp ("start_date",
                                  bucket_start),
      GNUNET_JSON_pack_array_steal ("values",
                                    values));
    GNUNET_assert (0 ==
                   json_array_append_new (rctx->data_groups,
                                          dg));

  }
}


/**
 * Create the transaction volume report.
 *
 * @param[in,out] rctx request context to use
 * @param[in,out] charts JSON chart array to expand
 * @return #GNUNET_OK on success,
 *         #GNUNET_NO to end with #MHD_YES,
 *         #GNUNET_NO to end with #MHD_NO.
 */
static enum GNUNET_GenericReturnValue
make_transaction_volume_report (struct ResponseContext *rctx,
                                json_t *charts)
{
  const char *bucket_name = "deposits-received";
  enum GNUNET_DB_QueryStatus qs;
  json_t *chart;
  json_t *labels;

  rctx->data_groups = json_array ();
  GNUNET_assert (NULL != rctx->data_groups);
  qs = TMH_db->lookup_statistics_amount_by_bucket2 (
    TMH_db->cls,
    rctx->hc->instance->settings.id,
    bucket_name,
    rctx->granularity,
    rctx->count,
    &amount_by_bucket,
    rctx);
  if (0 > qs)
  {
    GNUNET_break (0);
    return (MHD_YES ==
            TALER_MHD_reply_with_error (
              rctx->hc->connection,
              MHD_HTTP_INTERNAL_SERVER_ERROR,
              TALER_EC_GENERIC_DB_FETCH_FAILED,
              "lookup_statistics_amount_by_bucket2"))
        ? GNUNET_NO : GNUNET_SYSERR;
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    json_decref (rctx->data_groups);
    rctx->data_groups = NULL;
    return GNUNET_OK;
  }

  labels = json_array ();
  GNUNET_assert (NULL != labels);
  for (unsigned int i=0; i<rctx->labels_cnt; i++)
  {
    GNUNET_assert (0 ==
                   json_array_append_new (labels,
                                          json_string (rctx->labels[i])));
    GNUNET_free (rctx->labels[i]);
  }
  GNUNET_array_grow (rctx->labels,
                     rctx->labels_cnt,
                     0);
  chart = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("chart_name",
                             "Sales volume"),
    GNUNET_JSON_pack_string ("y_label",
                             "Sales"),
    GNUNET_JSON_pack_array_steal ("data_groups",
                                  rctx->data_groups),
    GNUNET_JSON_pack_array_steal ("labels",
                                  labels),
    GNUNET_JSON_pack_bool ("cumulative",
                           false));
  rctx->data_groups = NULL;
  GNUNET_assert (0 ==
                 json_array_append_new (charts,
                                        chart));
  return GNUNET_OK;
}


/**
 * Typically called by `lookup_statistics_counter_by_bucket2`.
 *
 * @param[in,out] cls our `struct ResponseContext` to update
 * @param bucket_start start time of the bucket
 * @param counters_len the length of @a cumulative_amounts
 * @param descriptions description for the counter in the bucket
 * @param counters the counters in the bucket
 */
static void
count_by_bucket (void *cls,
                 struct GNUNET_TIME_Timestamp bucket_start,
                 unsigned int counters_len,
                 const char *descriptions[static counters_len],
                 uint64_t counters[static counters_len])
{
  struct ResponseContext *rctx = cls;
  json_t *values;

  for (unsigned int i = 0; i<counters_len; i++)
  {
    bool found = false;

    for (unsigned int j = 0; j<rctx->labels_cnt; j++)
    {
      if (0 == strcmp (descriptions[i],
                       rctx->labels[j]))
      {
        found = true;
        break;
      }
    }
    if (! found)
    {
      GNUNET_array_append (rctx->labels,
                           rctx->labels_cnt,
                           GNUNET_strdup (descriptions[i]));
    }
  }

  values = json_array ();
  GNUNET_assert (NULL != values);
  for (unsigned int i = 0; i<rctx->labels_cnt; i++)
  {
    const char *label = rctx->labels[i];
    uint64_t v = 0;

    for (unsigned int j = 0; j<counters_len; j++)
    {
      if (0 != strcmp (descriptions[j],
                       label))
        continue;
      v = counters[j];
      break;
    } /* for all amounts */
    GNUNET_assert (0 ==
                   json_array_append_new (values,
                                          json_integer (v)));
  } /* for all labels */

  {
    json_t *dg;

    dg = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_timestamp ("start_date",
                                  bucket_start),
      GNUNET_JSON_pack_array_steal ("values",
                                    values));
    GNUNET_assert (0 ==
                   json_array_append_new (rctx->data_groups,
                                          dg));

  }
}


/**
 * Create the transaction count report.
 *
 * @param[in,out] rctx request context to use
 * @param[in,out] charts JSON chart array to expand
 * @return #GNUNET_OK on success,
 *         #GNUNET_NO to end with #MHD_YES,
 *         #GNUNET_NO to end with #MHD_NO.
 */
static enum GNUNET_GenericReturnValue
make_transaction_count_report (struct ResponseContext *rctx,
                               json_t *charts)
{
  const char *prefix = "orders-paid";
  enum GNUNET_DB_QueryStatus qs;
  json_t *chart;
  json_t *labels;

  rctx->data_groups = json_array ();
  GNUNET_assert (NULL != rctx->data_groups);
  qs = TMH_db->lookup_statistics_counter_by_bucket2 (
    TMH_db->cls,
    rctx->hc->instance->settings.id,
    prefix,   /* prefix to match against bucket name */
    rctx->granularity,
    rctx->count,
    &count_by_bucket,
    rctx);
  if (0 > qs)
  {
    GNUNET_break (0);
    return (MHD_YES ==
            TALER_MHD_reply_with_error (
              rctx->hc->connection,
              MHD_HTTP_INTERNAL_SERVER_ERROR,
              TALER_EC_GENERIC_DB_FETCH_FAILED,
              "lookup_statistics_counter_by_bucket2"))
        ? GNUNET_NO : GNUNET_SYSERR;
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    json_decref (rctx->data_groups);
    rctx->data_groups = NULL;
    return GNUNET_OK;
  }
  labels = json_array ();
  GNUNET_assert (NULL != labels);
  for (unsigned int i=0; i<rctx->labels_cnt; i++)
  {
    const char *label = rctx->labels[i];

    /* This condition should always hold. */
    if (0 ==
        strncmp (prefix,
                 label,
                 strlen (prefix)))
      label += strlen (prefix);
    GNUNET_assert (0 ==
                   json_array_append_new (labels,
                                          json_string (label)));
    GNUNET_free (rctx->labels[i]);
  }
  GNUNET_array_grow (rctx->labels,
                     rctx->labels_cnt,
                     0);
  chart = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("chart_name",
                             "Transaction counts"),
    GNUNET_JSON_pack_string ("y_label",
                             "Number of transactions"),
    GNUNET_JSON_pack_array_steal ("data_groups",
                                  rctx->data_groups),
    GNUNET_JSON_pack_array_steal ("labels",
                                  labels),
    GNUNET_JSON_pack_bool ("cumulative",
                           false));
  rctx->data_groups = NULL;
  GNUNET_assert (0 ==
                 json_array_append_new (charts,
                                        chart));
  return GNUNET_OK;
}


/**
 * Handle a GET "/private/statistics-report/transactions" 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_statistics_report_transactions (
  const struct TMH_RequestHandler *rh,
  struct MHD_Connection *connection,
  struct TMH_HandlerContext *hc)
{
  struct ResponseContext *rctx = hc->ctx;
  struct TMH_MerchantInstance *mi = hc->instance;
  json_t *charts;

  if (NULL != rctx)
  {
    GNUNET_assert (GNUNET_YES != rctx->suspended);
    if (GNUNET_SYSERR == rctx->suspended)
      return MHD_NO;
    if (NULL == rctx->response)
    {
      GNUNET_break (0);
      return MHD_NO;
    }
    return MHD_queue_response (connection,
                               rctx->http_status,
                               rctx->response);
  }
  rctx = GNUNET_new (struct ResponseContext);
  rctx->hc = hc;
  rctx->now = GNUNET_TIME_timestamp_get ();
  hc->ctx = rctx;
  hc->cc = &free_rc;
  GNUNET_assert (NULL != mi);

  rctx->granularity = MHD_lookup_connection_value (connection,
                                                   MHD_GET_ARGUMENT_KIND,
                                                   "granularity");
  if (NULL == rctx->granularity)
  {
    rctx->granularity = "day";
    rctx->period = GNUNET_TIME_UNIT_DAYS;
    rctx->count = 95;
  }
  else
  {
    const struct
    {
      const char *name;
      struct GNUNET_TIME_Relative period;
      uint64_t default_counter;
    } map[] = {
      {
        .name = "second",
        .period = GNUNET_TIME_UNIT_SECONDS,
        .default_counter = 120,
      },
      {
        .name = "minute",
        .period = GNUNET_TIME_UNIT_MINUTES,
        .default_counter = 120,
      },
      {
        .name = "hour",
        .period = GNUNET_TIME_UNIT_HOURS,
        .default_counter = 48,
      },
      {
        .name = "day",
        .period = GNUNET_TIME_UNIT_DAYS,
        .default_counter = 95,
      },
      {
        .name = "month",
        .period = GNUNET_TIME_UNIT_MONTHS,
        .default_counter = 36,
      },
      {
        .name = "quarter",
        .period = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MONTHS,
                                                 3),
        .default_counter = 40,
      },
      {
        .name = "year",
        .period = GNUNET_TIME_UNIT_YEARS,
        .default_counter = 10
      },
      {
        .name = NULL
      }
    };

    rctx->count = 0;
    for (unsigned int i = 0; map[i].name != NULL; i++)
    {
      if (0 == strcasecmp (map[i].name,
                           rctx->granularity))
      {
        rctx->count = map[i].default_counter;
        rctx->period = map[i].period;
        break;
      }
    }
    if (0 == rctx->count)
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "granularity");
    }
  } /* end handling granularity */

  /* Figure out desired output format */
  {
    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,
                     "application/json"))
    {
      rctx->format = RCF_JSON;
    }
    else if (0 == strcmp (mime,
                          "application/pdf"))
    {

      rctx->format = RCF_PDF;
    }
    else
    {
      GNUNET_break_op (0);
      return TALER_MHD_REPLY_JSON_PACK (
        connection,
        MHD_HTTP_NOT_ACCEPTABLE,
        GNUNET_JSON_pack_string ("hint",
                                 mime));
    }
  } /* end of determine output format */

  TALER_MHD_parse_request_number (connection,
                                  "count",
                                  &rctx->count);

  /* create charts */
  charts = json_array ();
  GNUNET_assert (NULL != charts);
  {
    enum GNUNET_GenericReturnValue ret;

    ret = make_transaction_volume_report (rctx,
                                          charts);
    if (GNUNET_OK != ret)
      return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    ret = make_transaction_count_report (rctx,
                                         charts);
    if (GNUNET_OK != ret)
      return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
  }

  /* generate response */
  {
    struct GNUNET_TIME_Timestamp start_date;
    struct GNUNET_TIME_Timestamp end_date;
    json_t *root;

    end_date = rctx->now;
    start_date
      = GNUNET_TIME_absolute_to_timestamp (
          GNUNET_TIME_absolute_subtract (
            end_date.abs_time,
            GNUNET_TIME_relative_multiply (rctx->period,
                                           rctx->count)));
    root = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_string ("business_name",
                               mi->settings.name),
      GNUNET_JSON_pack_timestamp ("start_date",
                                  start_date),
      GNUNET_JSON_pack_timestamp ("end_date",
                                  end_date),
      GNUNET_JSON_pack_time_rel ("bucket_period",
                                 rctx->period),
      GNUNET_JSON_pack_array_steal ("charts",
                                    charts));

    switch (rctx->format)
    {
    case RCF_JSON:
      return TALER_MHD_reply_json (connection,
                                   root,
                                   MHD_HTTP_OK);
    case RCF_PDF:
      {
        struct TALER_MHD_TypstDocument doc = {
          .form_name = "transactions",
          .form_version = "0.0.0",
          .data = root
        };

        rctx->tc = TALER_MHD_typst (TALER_MERCHANT_project_data (),
                                    TMH_cfg,
                                    false, /* remove on exit */
                                    "merchant",
                                    1, /* one document, length of "array"! */
                                    &doc,
                                    &pdf_cb,
                                    rctx);
        json_decref (root);
        if (NULL == rctx->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 (rctx_head,
                                     rctx_tail,
                                     rctx);
        rctx->suspended = GNUNET_YES;
        MHD_suspend_connection (connection);
        return MHD_YES;
      }
    } /* end switch */
  }
  GNUNET_assert (0);
  return MHD_NO;
}


/* end of taler-merchant-httpd_get-private-statistics-report-transactions.c */
