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

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU Affero General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-exchange-sanctionscheck.c
 * @brief Process that checks all existing customers against a sanctions list
 * @author Christian Grothoff
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#include <pthread.h>
#include <microhttpd.h>
#include "taler_exchangedb_lib.h"
#include "taler_exchangedb_plugin.h"
#include "taler_json_lib.h"
#include "taler_kyclogic_lib.h"
#include <math.h>

/**
 * Account we are currently checking.
 */
struct Account
{
  /**
   * Kept in a DLL.
   */
  struct Account *next;

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

  /**
   * Original properties of the account.
   */
  json_t *properties;

  /**
   * Evaluation entry with the sanction list checker.
   */
  struct TALER_KYCLOGIC_EvaluationEntry *ee;

  /**
   * Row of the attributes in the kyc_attributes table.
   */
  uint64_t row_id;

  /**
   * Hash of the normalized payto:// URI of the account.
   */
  struct TALER_NormalizedPaytoHashP h_payto;

};


/**
 * The exchange's configuration (global)
 */
static const struct GNUNET_CONFIGURATION_Handle *cfg;

/**
 * Our DB plugin.
 */
static struct TALER_EXCHANGEDB_Plugin *db_plugin;

/**
 * Helper process for the actual rating.
 */
static struct TALER_KYCLOGIC_SanctionRater *sr;

/**
 * Value to return from main(). 0 on success, non-zero on
 * on serious errors.
 */
static int global_ret;

/**
 * Key used to encrypt KYC attribute data in our database.
 */
static struct TALER_AttributeEncryptionKeyP attribute_key;

/**
 * Account rules to set if we have a good match against the
 * sanction list and should freeze an account immediately.
 */
static json_t *freeze_rules;

/**
 * Head of DLL of accounts we are currently checking.
 */
static struct Account *acc_head;

/**
 * Tail of DLL of accounts we are currently checking.
 */
static struct Account *acc_tail;


/**
 * We're being aborted with CTRL-C (or SIGTERM). Shut down.
 *
 * @param cls closure
 */
static void
shutdown_task (void *cls)
{
  struct Account *acc;

  (void) cls;
  while (NULL != (acc = acc_head))
  {
    GNUNET_CONTAINER_DLL_remove (acc_head,
                                 acc_tail,
                                 acc);
    json_decref (acc->properties);
    GNUNET_free (acc);
  }

  if (NULL != sr)
  {
    TALER_KYCLOGIC_sanction_rater_stop (sr);
    sr = NULL;
  }
  TALER_EXCHANGEDB_plugin_unload (db_plugin);
  db_plugin = NULL;
  cfg = NULL;
}


/**
 * Convert double value @a d to numeric score.
 *
 * @param d score from [0,1]
 * @return value from 0 to 1 billion proportional to @a d
 */
static uint64_t
double_to_billion (double d)
{
  double r;

  GNUNET_break (d >= 0);
  GNUNET_break (d <= 1.0);
  r = round (d * 1000000000);
  return (uint64_t) r;
}


/**
 * Function called with the result of a sanction evaluation.
 *
 * @param cls closure
 * @param ec error code, #TALER_EC_NONE on success
 * @param best_match identifies the sanction list entry with the best match
 * @param expires when does the sanction list entry expire
 * @param rating likelihood of the match, from 0 (none) to 1 (perfect)
 * @param confidence confidence in the evaluation, from 0 (none) to 1 (perfect)
 */
static void
sanction_cb (void *cls,
             enum TALER_ErrorCode ec,
             const char *best_match,
             struct GNUNET_TIME_Timestamp expires,
             double rating,
             double confidence)
{
  struct Account *acc = cls;
  bool freeze = false;
  bool investigate = false;

  // FIXME-#9053: formulas to be improved and customized via configuration
  if ( (rating > 0.95) &&
       (confidence > 0.95) )
  {
    freeze = true;
    investigate = true;
  }
  else if (rating > 0.95 - confidence)
  {
    investigate = true;
  }
  if (freeze || investigate)
  {
    static const char *freeze_event[] = {
      "sanction-list-hit-account-frozen",
    };
    static const char *partial_match_event[] = {
      "sanction-list-hit-partial-account-investigated",
    };
    enum GNUNET_DB_QueryStatus qs;
    json_t *properties;
    const char **events;
    json_t *new_rules;

    new_rules = freeze ? freeze_rules : NULL;
    events = freeze ? freeze_event : partial_match_event;
    properties = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_string ("SANCTION_LIST_BEST_MATCH",
                               best_match),
      GNUNET_JSON_pack_uint64 ("SANCTION_LIST_RATING",
                               double_to_billion (rating)),
      GNUNET_JSON_pack_uint64 ("SANCTION_LIST_CONFIDENCE",
                               double_to_billion (confidence)),
      GNUNET_JSON_pack_string ("AML_INVESTIGATION_STATE",
                               "INVESTIGATION_PENDING"),
      GNUNET_JSON_pack_string ("AML_INVESTIGATION_TIGGER",
                               "SANCTION_LIST_MATCH"));
    GNUNET_assert (0 ==
                   json_object_update_missing (properties,
                                               acc->properties));
    qs = db_plugin->insert_sanction_list_hit (db_plugin->cls,
                                              &acc->h_payto,
                                              investigate,
                                              new_rules,
                                              properties,
                                              expires,
                                              1,
                                              events);
    json_decref (properties);
    if (qs < 0)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to insert sanction list evaluation result\n");
      global_ret = EXIT_FAILURE;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
  }
  GNUNET_CONTAINER_DLL_remove (acc_head,
                               acc_tail,
                               acc);
  json_decref (acc->properties);
  GNUNET_free (acc);
  if (NULL != acc_head)
    return; /* more work */
  {
    enum GNUNET_DB_QueryStatus qs;

    qs = db_plugin->commit (db_plugin->cls);
    if (qs < 0)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to commit DB transaction\n");
      global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
  }
  GNUNET_SCHEDULER_shutdown ();
}


/**
 * Function called on each account.
 *
 * @param cls closure
 * @param row_id row of the attributes in the database
 * @param h_payto account for which the attribute data is stored
 * @param provider_name provider that must be checked
 * @param collection_time when was the data collected
 * @param expiration_time when does the data expire
 * @param properties properties that were set for @a h_payto
 * @param enc_attributes_size number of bytes in @a enc_attributes
 * @param enc_attributes encrypted attribute data
 * @return true to continue to iterate
 */
static bool
account_cb (void *cls,
            uint64_t row_id,
            const struct TALER_NormalizedPaytoHashP *h_payto,
            const char *provider_name,
            struct GNUNET_TIME_Timestamp collection_time,
            struct GNUNET_TIME_Timestamp expiration_time,
            const json_t *properties,
            size_t enc_attributes_size,
            const void *enc_attributes)
{
  json_t *attributes;
  struct Account *acc;

  (void) cls;
  if (json_boolean_value (json_object_get (properties,
                                           "SANCTION_LIST_SUPPRESS")))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Skipping %llu as suppressed by staff as false-positive\n",
                (unsigned long long) row_id);
    return true;
  }
  attributes = TALER_CRYPTO_kyc_attributes_decrypt (&attribute_key,
                                                    enc_attributes,
                                                    enc_attributes_size);
  if (NULL == attributes)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to decrypt attributes at row #%llu\n",
                (unsigned long long) row_id);
    return true;
  }
  acc = GNUNET_new (struct Account);
  acc->row_id = row_id;
  acc->h_payto = *h_payto;
  acc->properties = json_incref ((json_t *) properties);
  acc->ee = TALER_KYCLOGIC_sanction_rater_eval (sr,
                                                attributes,
                                                &sanction_cb,
                                                acc);
  if (NULL == acc->ee)
  {
    GNUNET_free (acc);
    return false;
  }
  GNUNET_CONTAINER_DLL_insert (acc_head,
                               acc_tail,
                               acc);
  return true;
}


/**
 * Initialize JSON rules for freezing an account.
 *
 * @return true on success
 */
static bool
init_freeze (void)
{
  char *currency;
  struct TALER_Amount zero;
  json_t *rules;
  json_t *verboten;

  if (GNUNET_OK !=
      TALER_config_get_currency (cfg,
                                 "exchange",
                                 &currency))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "exchange",
                               "currency");
    return false;
  }
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (currency,
                                        &zero));
  verboten = json_array ();
  GNUNET_assert (NULL != verboten);
  GNUNET_assert (0 ==
                 json_array_append_new (verboten,
                                        json_string ("verboten")));
  rules = json_array ();
  GNUNET_assert (NULL != rules);
  for (enum TALER_KYCLOGIC_KycTriggerEvent et =
         TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW;
       et <= TALER_KYCLOGIC_KYC_TRIGGER_REFUND;
       et++)
  {
    json_t *rule;

    rule = GNUNET_JSON_PACK (
      TALER_JSON_pack_kycte ("operation_type",
                             et),
      TALER_JSON_pack_amount ("threshold",
                              &zero),
      GNUNET_JSON_pack_time_rel ("timeframe",
                                 GNUNET_TIME_UNIT_YEARS),
      GNUNET_JSON_pack_array_incref ("measures",
                                     verboten),
      GNUNET_JSON_pack_uint64 ("display_priority",
                               1),
      GNUNET_JSON_pack_bool ("exposed",
                             false),
      GNUNET_JSON_pack_bool ("is_and_combinator",
                             false));
    GNUNET_assert (0 ==
                   json_array_append_new (rules,
                                          rule));
  }
  json_decref (verboten);
  freeze_rules =
    GNUNET_JSON_PACK (
      GNUNET_JSON_pack_timestamp ("expiration_time",
                                  GNUNET_TIME_UNIT_FOREVER_TS),
      GNUNET_JSON_pack_array_steal ("rules",
                                    rules));
  return true;
}


/**
 * First task.
 *
 * @param cls closure, NULL
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be NULL!)
 * @param c configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *c)
{
  enum GNUNET_DB_QueryStatus qs;

  (void) cls;
  (void) cfgfile;
  cfg = c;
  if (NULL == args[0])
  {
    fprintf (stderr,
             "You must pass the name of the sanction list helper program as an argument!\n");
    global_ret = EXIT_INVALIDARGUMENT;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (! init_freeze ())
    return;
  {
    char *attr_enc_key_str;

    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (cfg,
                                               "exchange",
                                               "ATTRIBUTE_ENCRYPTION_KEY",
                                               &attr_enc_key_str))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 "exchange",
                                 "ATTRIBUTE_ENCRYPTION_KEY");
      global_ret = EXIT_NOTCONFIGURED;
      return;
    }
    GNUNET_CRYPTO_hash (attr_enc_key_str,
                        strlen (attr_enc_key_str),
                        &attribute_key.hash);
    GNUNET_free (attr_enc_key_str);
  }
  if (NULL ==
      (db_plugin = TALER_EXCHANGEDB_plugin_load (cfg,
                                                 false)))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to initialize DB subsystem\n");
    global_ret = EXIT_NOTCONFIGURED;
    return;
  }
  GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
                                 cls);
  sr = TALER_KYCLOGIC_sanction_rater_start (args[0],
                                            args);
  if (NULL == sr)
  {
    global_ret = EXIT_INVALIDARGUMENT;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      db_plugin->start (db_plugin->cls,
                        "sanctionscheck"))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to begin DB transaction\n");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  qs = db_plugin->select_all_kyc_attributes (db_plugin->cls,
                                             &account_cb,
                                             NULL);
  if (qs < 0)
  {
    global_ret = EXIT_FAILURE;
    GNUNET_break (0);
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    db_plugin->rollback (db_plugin->cls);
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  GNUNET_assert (NULL != acc_head);
}


/**
 * The main function of taler-exchange-sanctionscheck
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, non-zero on error
 */
int
main (int argc,
      char *const *argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION),
    GNUNET_GETOPT_OPTION_END
  };
  enum GNUNET_GenericReturnValue ret;

  ret = GNUNET_PROGRAM_run (
    TALER_EXCHANGE_project_data (),
    argc, argv,
    "taler-exchange-sanctionscheck -- HELPER [HELPER ARGS]",
    gettext_noop (
      "process that checks all existing customer accounts against a sanctions list"),
    options,
    &run, NULL);
  json_decref (freeze_rules);
  freeze_rules = NULL;
  if (GNUNET_SYSERR == ret)
    return EXIT_INVALIDARGUMENT;
  if (GNUNET_NO == ret)
    return EXIT_SUCCESS;
  return global_ret;
}


/* end of taler-exchange-sanctionscheck.c */
