/*
   This file is part of TALER
   Copyright (C) 2026 Taler Systems SA

   TALER is free software; you can redistribute it and/or modify it under the
   terms of the GNU 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 src/exchangedb/iterate_aml_history_above_serial_id.c
 * @brief Implementation of the iterate_aml_history_above_serial_id function for Postgres
 * @author Christian Grothoff
 */
#include "taler/taler_pq_lib.h"
#include "helper.h"
#include "exchange-database/iterate_aml_history_above_serial_id.h"


/**
 * Closure for #aml_decision_cb().
 */
struct AmlDecisionContext
{
  /**
   * Function to call for each decision.
   */
  TALER_EXCHANGEDB_AmlDecisionCallback cb;

  /**
   * Closure for @e cb.
   */
  void *cb_cls;

  /**
   * Query status to return.
   */
  enum GNUNET_DB_QueryStatus qs;
};


/**
 * Helper function for
 * #TALER_EXCHANGEDB_iterate_aml_history_above_serial_id().
 * To be called with the results of a SELECT statement
 * that has returned @a num_results results.
 *
 * @param cls closure of type `struct AmlDecisionContext *`
 * @param result the postgres result
 * @param num_results the number of results in @a result
 */
static void
aml_decision_cb (void *cls,
                 PGresult *result,
                 unsigned int num_results)
{
  struct AmlDecisionContext *adc = cls;

  for (unsigned int i = 0; i < num_results; i++)
  {
    uint64_t rowid;
    struct TALER_NormalizedPaytoHashP h_payto;
    char *justification = NULL;
    struct TALER_AmlOfficerPublicKeyP decider_pub;
    struct TALER_AmlOfficerSignatureP decider_sig;
    struct GNUNET_TIME_Timestamp decision_time;
    json_t *jproperties = NULL;
    json_t *jnew_rules = NULL;
    char *new_measure_name = NULL;
    bool to_investigate;
    struct GNUNET_TIME_Timestamp attributes_expiration
      = GNUNET_TIME_UNIT_ZERO_TS;
    struct GNUNET_HashCode h_attributes;
    bool no_decider_pub;
    bool no_decider_sig;
    bool no_new_rules;
    bool no_measure;
    bool no_attributes_expiration;
    bool no_h_attributes;
    struct GNUNET_PQ_ResultSpec rs[] = {
      GNUNET_PQ_result_spec_uint64 ("aml_history_serial_id",
                                    &rowid),
      GNUNET_PQ_result_spec_auto_from_type ("h_payto",
                                            &h_payto),
      GNUNET_PQ_result_spec_string ("justification",
                                    &justification),
      /* decider_pub, decider_sig, kyc_attributes_hash and jnew_rules all
         lack a NOT NULL constraint.  An honest decision has all of them;
         reporting the ones that do not is the auditor's job, so they must
         not abort the query here. */
      GNUNET_PQ_result_spec_allow_null (
        GNUNET_PQ_result_spec_auto_from_type ("decider_pub",
                                              &decider_pub),
        &no_decider_pub),
      GNUNET_PQ_result_spec_allow_null (
        GNUNET_PQ_result_spec_auto_from_type ("decider_sig",
                                              &decider_sig),
        &no_decider_sig),
      GNUNET_PQ_result_spec_timestamp ("decision_time",
                                       &decision_time),
      GNUNET_PQ_result_spec_allow_null (
        TALER_PQ_result_spec_json ("jproperties",
                                   &jproperties),
        NULL),
      GNUNET_PQ_result_spec_allow_null (
        TALER_PQ_result_spec_json ("jnew_rules",
                                   &jnew_rules),
        &no_new_rules),
      GNUNET_PQ_result_spec_allow_null (
        GNUNET_PQ_result_spec_string ("new_measure_name",
                                      &new_measure_name),
        &no_measure),
      GNUNET_PQ_result_spec_bool ("to_investigate",
                                  &to_investigate),
      GNUNET_PQ_result_spec_allow_null (
        GNUNET_PQ_result_spec_timestamp ("attributes_expiration",
                                         &attributes_expiration),
        &no_attributes_expiration),
      GNUNET_PQ_result_spec_allow_null (
        GNUNET_PQ_result_spec_auto_from_type ("kyc_attributes_hash",
                                              &h_attributes),
        &no_h_attributes),
      GNUNET_PQ_result_spec_end
    };
    enum GNUNET_GenericReturnValue rval;

    if (GNUNET_OK !=
        GNUNET_PQ_extract_result (result,
                                  rs,
                                  i))
    {
      GNUNET_break (0);
      adc->qs = GNUNET_DB_STATUS_HARD_ERROR;
      return;
    }
    if (no_attributes_expiration)
      attributes_expiration = GNUNET_TIME_UNIT_ZERO_TS;
    adc->qs = i + 1;
    rval = adc->cb (adc->cb_cls,
                    rowid,
                    &h_payto,
                    justification,
                    no_decider_pub ? NULL : &decider_pub,
                    no_decider_sig ? NULL : &decider_sig,
                    decision_time,
                    jproperties,
                    no_new_rules ? NULL : jnew_rules,
                    no_measure ? NULL : new_measure_name,
                    to_investigate,
                    attributes_expiration,
                    no_h_attributes ? NULL : &h_attributes);
    GNUNET_PQ_cleanup_result (rs);
    if (GNUNET_OK != rval)
      break;
  }
}


enum GNUNET_DB_QueryStatus
TALER_EXCHANGEDB_iterate_aml_history_above_serial_id (
  struct TALER_EXCHANGEDB_PostgresContext *pg,
  uint64_t serial_id,
  TALER_EXCHANGEDB_AmlDecisionCallback cb,
  TALER_EXCHANGEDB_AML_DECISION_RESULT_CLOSURE *cb_cls)
{
  struct GNUNET_PQ_QueryParam params[] = {
    GNUNET_PQ_query_param_uint64 (&serial_id),
    GNUNET_PQ_query_param_end
  };
  struct AmlDecisionContext adc = {
    .cb = cb,
    .cb_cls = cb_cls
  };
  enum GNUNET_DB_QueryStatus qs;

  /* legitimization_outcomes is joined on a NOT NULL column with a foreign
     key, so an inner join here cannot silently drop a decision.  The
     attributes are optional, hence the LEFT JOIN for their expiration. */
  PREPARE (pg,
           "iterate_aml_history_above_serial_id",
           "SELECT"
           " ah.aml_history_serial_id"
           ",ah.h_payto"
           ",ah.justification"
           ",ah.decider_pub"
           ",ah.decider_sig"
           ",ah.kyc_attributes_hash"
           ",lo.decision_time"
           /* JSONB comes back with a binary version prefix, so it has to
              be cast; the cast is what TALER_json_hash() then re-reads. */
           ",lo.jproperties::TEXT"
           ",lo.jnew_rules::TEXT"
           ",lo.new_measure_name"
           ",lo.to_investigate"
           ",ka.expiration_time AS attributes_expiration"
           " FROM aml_history ah"
           " JOIN legitimization_outcomes lo"
           "   USING (outcome_serial_id)"
           " LEFT JOIN kyc_attributes ka"
           "   ON (ah.kyc_attributes_serial_id = ka.kyc_attributes_serial_id)"
           " WHERE ah.aml_history_serial_id>=$1"
           " ORDER BY ah.aml_history_serial_id ASC;");
  qs = GNUNET_PQ_eval_prepared_multi_select (
    pg->conn,
    "iterate_aml_history_above_serial_id",
    params,
    &aml_decision_cb,
    &adc);
  if (qs > 0)
    return adc.qs;
  GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR != qs);
  return qs;
}
