/*
  This file is part of TALER
  Copyright (C) 2023, 2024, 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 lib/exchange_api_post-aml-OFFICER_PUB-decision.c
 * @brief functions to add an AML decision by an AML officer
 * @author Christian Grothoff
 */
#include "taler/taler_json_lib.h"
#include <microhttpd.h>
#include <gnunet/gnunet_curl_lib.h>
#include "taler/exchange/post-aml-OFFICER_PUB-decision.h"
#include "exchange_api_curl_defaults.h"
#include "taler/taler_signatures.h"
#include "taler/taler_curl_lib.h"


/**
 * @brief A POST /aml/$OFFICER_PUB/decision Handle
 */
struct TALER_EXCHANGE_PostAmlDecisionHandle
{

  /**
   * The base URL of the exchange.
   */
  char *base_url;

  /**
   * The full URL for this request.
   */
  char *url;

  /**
   * Minor context that holds body and headers.
   */
  struct TALER_CURL_PostContext post_ctx;

  /**
   * Handle for the request.
   */
  struct GNUNET_CURL_Job *job;

  /**
   * Function to call with the result.
   */
  TALER_EXCHANGE_PostAmlDecisionCallback cb;

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

  /**
   * Reference to the execution context.
   */
  struct GNUNET_CURL_Context *ctx;

  /**
   * Public key of the AML officer.
   */
  struct TALER_AmlOfficerPublicKeyP officer_pub;

  /**
   * Private key of the AML officer.
   */
  struct TALER_AmlOfficerPrivateKeyP officer_priv;

  /**
   * Hash of the payto URI of the account the decision is about.
   */
  struct TALER_NormalizedPaytoHashP h_payto;

  /**
   * When was the decision made.
   */
  struct GNUNET_TIME_Timestamp decision_time;

  /**
   * Human-readable justification.
   */
  char *justification;

  /**
   * True to keep the investigation open.
   */
  bool keep_investigating;

  /**
   * Pre-built new_rules JSON object.
   */
  json_t *new_rules;

  /**
   * Optional: full payto URI string, may be NULL.
   */
  char *payto_uri_str;

  /**
   * Optional: space-separated list of measures to trigger immediately
   * (from options, may be NULL).
   */
  char *new_measures;

  /**
   * Optional: JSON object with account properties
   * (from options, may be NULL).
   */
  json_t *properties;

  /**
   * Optional: JSON array of events to trigger
   * (from options; may be NULL).
   */
  json_t *jevents;

  /**
   * Optional: JSON object with KYC attributes
   * (from options; may be NULL).
   */
  json_t *attributes;

  /**
   * Optional: expiration time for KYC attributes.
   * Only meaningful if @e attributes is non-NULL.
   */
  struct GNUNET_TIME_Timestamp attributes_expiration;

};


/**
 * Function called when we're done processing the
 * HTTP POST /aml/$OFFICER_PUB/decision request.
 *
 * @param cls the `struct TALER_EXCHANGE_PostAmlDecisionHandle *`
 * @param response_code HTTP response code, 0 on error
 * @param response response body, NULL if not in JSON
 */
static void
handle_post_aml_decision_finished (void *cls,
                                   long response_code,
                                   const void *response)
{
  struct TALER_EXCHANGE_PostAmlDecisionHandle *padh = cls;
  const json_t *json = response;
  struct TALER_EXCHANGE_PostAmlDecisionResponse pr = {
    .hr.http_status = (unsigned int) response_code,
    .hr.reply = json
  };

  padh->job = NULL;
  switch (response_code)
  {
  case 0:
    pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    pr.hr.hint = "server offline?";
    break;
  case MHD_HTTP_NO_CONTENT:
    break;
  case MHD_HTTP_FORBIDDEN:
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_CONFLICT:
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  default:
    GNUNET_break_op (0);
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected response code %u/%d for POST AML decision\n",
                (unsigned int) response_code,
                (int) pr.hr.ec);
    break;
  }
  if (NULL != padh->cb)
  {
    padh->cb (padh->cb_cls,
              &pr);
    padh->cb = NULL;
  }
  TALER_EXCHANGE_post_aml_decision_cancel (padh);
}


/**
 * Build the new_rules JSON object from rules and measures arrays.
 *
 * @param successor_measure optional successor measure name
 * @param expiration_time when the new rules expire
 * @param num_rules length of @a rules
 * @param rules the rules array
 * @param num_measures length of @a measures
 * @param measures the measures array
 * @return new JSON object (caller owns reference), NULL on error
 */
static json_t *
build_new_rules (
  const char *successor_measure,
  struct GNUNET_TIME_Timestamp expiration_time,
  unsigned int num_rules,
  const struct TALER_EXCHANGE_AccountRule rules[static num_rules],
  unsigned int num_measures,
  const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures])
{
  json_t *jrules;
  json_t *jmeasures;

  jrules = json_array ();
  GNUNET_assert (NULL != jrules);
  for (unsigned int i = 0; i < num_rules; i++)
  {
    const struct TALER_EXCHANGE_AccountRule *al = &rules[i];
    json_t *ameasures;
    json_t *rule;

    ameasures = json_array ();
    GNUNET_assert (NULL != ameasures);
    for (unsigned int j = 0; j < al->num_measures; j++)
      GNUNET_assert (0 ==
                     json_array_append_new (ameasures,
                                            json_string (al->measures[j])));
    rule = GNUNET_JSON_PACK (
      TALER_JSON_pack_kycte ("operation_type",
                             al->operation_type),
      TALER_JSON_pack_amount ("threshold",
                              &al->threshold),
      GNUNET_JSON_pack_time_rel ("timeframe",
                                 al->timeframe),
      GNUNET_JSON_pack_array_steal ("measures",
                                    ameasures),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_string ("rule_name",
                                 al->rule_name)),
      GNUNET_JSON_pack_bool ("exposed",
                             al->exposed),
      GNUNET_JSON_pack_bool ("is_and_combinator",
                             al->is_and_combinator),
      GNUNET_JSON_pack_uint64 ("display_priority",
                               al->display_priority)
      );
    GNUNET_break (0 ==
                  json_array_append_new (jrules,
                                         rule));
  }

  jmeasures = json_object ();
  GNUNET_assert (NULL != jmeasures);
  for (unsigned int i = 0; i < num_measures; i++)
  {
    const struct TALER_EXCHANGE_MeasureInformation *mi = &measures[i];
    json_t *measure;

    measure = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_string ("check_name",
                               mi->check_name),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_string ("prog_name",
                                 mi->prog_name)),
      /* We pack "NONE" for unknown, NULL would also be OK in that case. */
      TALER_JSON_pack_kycte ("operation_type",
                             mi->operation_type),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_object_incref ("context",
                                        (json_t *) mi->context))
      );
    GNUNET_break (0 ==
                  json_object_set_new (jmeasures,
                                       mi->measure_name,
                                       measure));
  }

  return GNUNET_JSON_PACK (
    GNUNET_JSON_pack_timestamp ("expiration_time",
                                expiration_time),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("successor_measure",
                               successor_measure)),
    GNUNET_JSON_pack_array_steal ("rules",
                                  jrules),
    GNUNET_JSON_pack_object_steal ("custom_measures",
                                   jmeasures)
    );
}


struct TALER_EXCHANGE_PostAmlDecisionHandle *
TALER_EXCHANGE_post_aml_decision_create (
  struct GNUNET_CURL_Context *ctx,
  const char *url,
  const struct TALER_NormalizedPaytoHashP *h_payto,
  struct GNUNET_TIME_Timestamp decision_time,
  const char *successor_measure,
  struct GNUNET_TIME_Timestamp expiration_time,
  unsigned int num_rules,
  const struct TALER_EXCHANGE_AccountRule rules[static num_rules],
  unsigned int num_measures,
  const struct TALER_EXCHANGE_MeasureInformation measures[static num_measures],
  bool keep_investigating,
  const char *justification,
  const struct TALER_AmlOfficerPrivateKeyP *officer_priv)
{
  struct TALER_EXCHANGE_PostAmlDecisionHandle *padh;
  json_t *new_rules;

  new_rules = build_new_rules (successor_measure,
                               expiration_time,
                               num_rules,
                               rules,
                               num_measures,
                               measures);
  if (NULL == new_rules)
  {
    GNUNET_break (0);
    return NULL;
  }

  padh = GNUNET_new (struct TALER_EXCHANGE_PostAmlDecisionHandle);
  padh->ctx = ctx;
  padh->base_url = GNUNET_strdup (url);
  padh->h_payto = *h_payto;
  padh->decision_time = decision_time;
  padh->justification = GNUNET_strdup (justification);
  padh->keep_investigating = keep_investigating;
  padh->new_rules = new_rules;
  padh->officer_priv = *officer_priv;
  GNUNET_CRYPTO_eddsa_key_get_public (&officer_priv->eddsa_priv,
                                      &padh->officer_pub.eddsa_pub);
  return padh;
}


enum GNUNET_GenericReturnValue
TALER_EXCHANGE_post_aml_decision_set_options_ (
  struct TALER_EXCHANGE_PostAmlDecisionHandle *padh,
  unsigned int num_options,
  const struct TALER_EXCHANGE_PostAmlDecisionOptionValue options[])
{
  for (unsigned int i = 0; i < num_options; i++)
  {
    const struct TALER_EXCHANGE_PostAmlDecisionOptionValue *opt = &options[i];

    switch (opt->option)
    {
    case TALER_EXCHANGE_POST_AML_DECISION_OPTION_END:
      return GNUNET_OK;
    case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PAYTO_URI:
      GNUNET_free (padh->payto_uri_str);
      padh->payto_uri_str =
        (NULL != opt->details.payto_uri.full_payto)
        ? GNUNET_strdup (opt->details.payto_uri.full_payto)
        : NULL;
      break;
    case TALER_EXCHANGE_POST_AML_DECISION_OPTION_NEW_MEASURES:
      GNUNET_free (padh->new_measures);
      padh->new_measures =
        (NULL != opt->details.new_measures)
        ? GNUNET_strdup (opt->details.new_measures)
        : NULL;
      break;
    case TALER_EXCHANGE_POST_AML_DECISION_OPTION_PROPERTIES:
      if (NULL != padh->properties)
        json_decref (padh->properties);
      padh->properties =
        (NULL != opt->details.properties)
        ? json_incref ((json_t *) opt->details.properties)
        : NULL;
      break;
    case TALER_EXCHANGE_POST_AML_DECISION_OPTION_EVENTS:
      {
        if (NULL != padh->jevents)
          json_decref (padh->jevents);
        if (0 == opt->details.events.num_events)
        {
          padh->jevents = NULL;
        }
        else
        {
          padh->jevents = json_array ();
          GNUNET_assert (NULL != padh->jevents);
          for (unsigned int j = 0; j < opt->details.events.num_events; j++)
            GNUNET_assert (0 ==
                           json_array_append_new (
                             padh->jevents,
                             json_string (opt->details.events.events[j])));
        }
      }
      break;
    case TALER_EXCHANGE_POST_AML_DECISION_OPTION_ATTRIBUTES:
      if (NULL != padh->attributes)
        json_decref (padh->attributes);
      padh->attributes =
        (NULL != opt->details.attributes)
        ? json_incref ((json_t *) opt->details.attributes)
        : NULL;
      break;
    case TALER_EXCHANGE_POST_AML_DECISION_OPTION_ATTRIBUTES_EXPIRATION:
      padh->attributes_expiration = opt->details.attributes_expiration;
      break;
    default:
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
  }
  return GNUNET_OK;
}


enum TALER_ErrorCode
TALER_EXCHANGE_post_aml_decision_start (
  struct TALER_EXCHANGE_PostAmlDecisionHandle *padh,
  TALER_EXCHANGE_PostAmlDecisionCallback cb,
  TALER_EXCHANGE_POST_AML_DECISION_RESULT_CLOSURE *cb_cls)
{
  CURL *eh;
  struct TALER_AmlOfficerSignatureP officer_sig;
  json_t *body;
  char *path;
  char opus[sizeof (padh->officer_pub) * 2];
  char *end;
  struct TALER_FullPayto payto_uri_val = {
    .full_payto = padh->payto_uri_str
  };

  padh->cb = cb;
  padh->cb_cls = cb_cls;
  TALER_officer_aml_decision_sign (padh->justification,
                                   padh->decision_time,
                                   &padh->h_payto,
                                   padh->new_rules,
                                   padh->properties,
                                   padh->new_measures,
                                   padh->keep_investigating,
                                   &padh->officer_priv,
                                   &officer_sig);

  end = GNUNET_STRINGS_data_to_string (
    &padh->officer_pub,
    sizeof (padh->officer_pub),
    opus,
    sizeof (opus));
  *end = '\0';
  GNUNET_asprintf (&path,
                   "aml/%s/decision",
                   opus);
  padh->url = TALER_url_join (padh->base_url,
                              path,
                              NULL);
  GNUNET_free (path);
  if (NULL == padh->url)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Could not construct request URL.\n");
    return TALER_EC_GENERIC_CONFIGURATION_INVALID;
  }

  body = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("justification",
                             padh->justification),
    GNUNET_JSON_pack_data_auto ("h_payto",
                                &padh->h_payto),
    GNUNET_JSON_pack_allow_null (
      TALER_JSON_pack_full_payto ("payto_uri",
                                  payto_uri_val)),
    GNUNET_JSON_pack_object_incref ("new_rules",
                                    padh->new_rules),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_object_incref ("properties",
                                      padh->properties)),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("new_measures",
                               padh->new_measures)),
    GNUNET_JSON_pack_bool ("keep_investigating",
                           padh->keep_investigating),
    GNUNET_JSON_pack_data_auto ("officer_sig",
                                &officer_sig),
    GNUNET_JSON_pack_timestamp ("decision_time",
                                padh->decision_time),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_array_incref ("events",
                                     padh->jevents)),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_object_incref ("attributes",
                                      padh->attributes))
    );
  if (NULL != padh->attributes)
  {
    GNUNET_assert (
      0 ==
      json_object_set_new (
        body,
        "attributes_expiration",
        GNUNET_JSON_from_timestamp (padh->attributes_expiration)));
  }

  eh = TALER_EXCHANGE_curl_easy_get_ (padh->url);
  if ( (NULL == eh) ||
       (GNUNET_OK !=
        TALER_curl_easy_post (&padh->post_ctx,
                              eh,
                              body)) )
  {
    GNUNET_break (0);
    if (NULL != eh)
      curl_easy_cleanup (eh);
    json_decref (body);
    GNUNET_free (padh->url);
    padh->url = NULL;
    return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
  }
  json_decref (body);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Requesting URL '%s'\n",
              padh->url);
  padh->job = GNUNET_CURL_job_add2 (padh->ctx,
                                    eh,
                                    padh->post_ctx.headers,
                                    &handle_post_aml_decision_finished,
                                    padh);
  if (NULL == padh->job)
  {
    TALER_curl_easy_post_finished (&padh->post_ctx);
    GNUNET_free (padh->url);
    padh->url = NULL;
    return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
  }
  return TALER_EC_NONE;
}


void
TALER_EXCHANGE_post_aml_decision_cancel (
  struct TALER_EXCHANGE_PostAmlDecisionHandle *padh)
{
  if (NULL != padh->job)
  {
    GNUNET_CURL_job_cancel (padh->job);
    padh->job = NULL;
  }
  TALER_curl_easy_post_finished (&padh->post_ctx);
  json_decref (padh->new_rules);
  if (NULL != padh->properties)
    json_decref (padh->properties);
  if (NULL != padh->jevents)
    json_decref (padh->jevents);
  if (NULL != padh->attributes)
    json_decref (padh->attributes);
  GNUNET_free (padh->url);
  GNUNET_free (padh->base_url);
  GNUNET_free (padh->justification);
  GNUNET_free (padh->payto_uri_str);
  GNUNET_free (padh->new_measures);
  GNUNET_free (padh);
}


/* end of exchange_api_post-aml-OFFICER_PUB-decision.c */
