/*
 This file is part of GNU Taler
 (C) 2022-2025 Taler Systems S.A.

 GNU 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.

 GNU 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
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

import {
  AbsoluteTime,
  buildCodecForObject,
  Codec,
  codecForAbsoluteTime,
  codecForAny,
  codecForBoolean,
  codecForKycRules,
  codecForList,
  codecForMap,
  codecForNumber,
  codecForString,
  codecOptional,
  codecOptionalDefault,
  KycRule,
  MeasureInformation,
  TalerExchangeApi,
} from "@gnu-taler/taler-util";
import {
  buildStorageKey,
  FormErrors,
  useLocalStorage,
} from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
export interface AccountAttributes {
  data: object;
  formId: string | undefined;
  formVersion: number;
  expiration: AbsoluteTime | undefined;
  errors: FormErrors<object> | undefined;
}
/**
 * Information that will be ask to the AML officer
 * in order to build up the decision.
 *
 * With all of this we need to create a AmlDecisionRequest
 */
export interface DecisionRequest {
  original: TalerExchangeApi.AmlDecision | undefined;
  /**
   * Next legitimization rules
   */
  rules: KycRule[] | undefined;
  /**
   * Next active measure
   */
  new_measures: string[] | undefined;
  /**
   * Next measure after deadline
   */
  onExpire_measure: string | undefined;
  /**
   * Deadline of this decision
   */
  deadline: AbsoluteTime | undefined;
  /**
   * New information about the account
   */
  attributes: AccountAttributes | undefined;
  /**
   * Relevate state of the account
   */
  properties: Record<string, boolean | string> | undefined;
  /**
   * Errors on the properties
   */
  properties_errors: object | undefined;
  /**
   * If given all the information, this account need to be investigated
   */
  keep_investigating: boolean | undefined;
  /**
   * Description of the decision
   */
  justification: string | undefined;
  /**
   * Name of the account holder if this is an unknown account to the exchange
   */
  accountName: string | undefined;
  /**
   * Custom properties not listed on GANA
   */
  custom_properties: Record<string, string> | undefined;
  /**
   * Supported events to be triggered
   */
  triggering_events: string[] | undefined;
  /**
   * Custom unsupported events to be triggered
   */
  custom_events: string[] | undefined;

  /**
   * Custom measures defined by the officer
   */
  custom_measures: Record<string, MeasureInformation> | undefined;
}

export const codecForMeasure = (): Codec<MeasureInformation> =>
  buildCodecForObject<MeasureInformation>()
    .property("check_name", codecOptionalDefault(codecForString(), "SKIP"))
    .property("prog_name", codecOptional(codecForString()))
    .property("context", codecOptional(codecForMap(codecForAny())))
    .build("MeasureInformation");

export const codecForAccountAttributes = (): Codec<AccountAttributes> =>
  buildCodecForObject<AccountAttributes>()
    .property("expiration", codecOptional(codecForAbsoluteTime))
    .property("formId", codecForString())
    .property("formVersion", codecForNumber())
    .property("data", codecForAny())
    .property("errors", codecForAny())
    .build("AccountAttributes");

export const codecForDecisionRequest = (): Codec<DecisionRequest> =>
  buildCodecForObject<DecisionRequest>()
    .property("original", codecOptional(codecForAny()))
    .property("rules", codecOptional(codecForList(codecForKycRules())))
    .property("deadline", codecOptional(codecForAbsoluteTime))
    .property("properties", codecOptional(codecForMap(codecForAny())))
    .property("properties_errors", codecForAny())
    .property("attributes", codecOptional(codecForAccountAttributes()))
    .property("custom_properties", codecForAny())
    .property("justification", codecOptional(codecForString()))
    .property("accountName", codecOptional(codecForString()))
    .property("custom_events", codecOptional(codecForList(codecForString())))
    .property("custom_measures", codecOptional(codecForMap(codecForMeasure())))
    .property(
      "triggering_events",
      codecOptional(codecForList(codecForString())),
    )
    .property("keep_investigating", codecOptional(codecForBoolean()))
    .property("new_measures", codecOptional(codecForList(codecForString())))
    .property("onExpire_measure", codecOptional(codecForString()))
    .build("DecisionRequest");

const DECISION_REQUEST_EMPTY: DecisionRequest = {
  original: undefined,
  deadline: undefined,
  custom_properties: undefined,
  onExpire_measure: undefined,
  custom_events: undefined,
  attributes: undefined,
  accountName: undefined,
  properties_errors: undefined,
  triggering_events: undefined,
  justification: undefined,
  keep_investigating: undefined,
  new_measures: undefined,
  custom_measures: undefined,
  properties: undefined,
  rules: undefined,
};

const DECISION_REQUEST_KEY = buildStorageKey(
  "aml-decision-request",
  codecForDecisionRequest(),
);
/**
 * This helpers is used to add support for multiple calls on the
 * same update function that update and state with partial object.
 *
 */
class ConcurrentUpdateHelper<T> {
  prevValue: T | undefined;
  public reset() {
    this.prevValue = undefined;
  }
  public mergeWithLatestOrDefault(
    defValue: T,
    newValue: Partial<T>,
  ): { old: T; merged: T } {
    const latest = this.prevValue === undefined ? defValue : this.prevValue;
    const mergedValue = { ...latest, ...newValue };
    this.prevValue = mergedValue;
    return { old: latest, merged: mergedValue };
  }
}

const mark = new ConcurrentUpdateHelper<DecisionRequest>();
/**
 * User preferences.
 *
 */
export function useCurrentDecisionRequest(): [
  Readonly<DecisionRequest>,
  (l: string, s: Partial<DecisionRequest>) => void,
  (s?: Partial<DecisionRequest>) => void,
  () => void,
] {
  const [currentDef, setDefault] = useState(DECISION_REQUEST_EMPTY);

  const { value: request, update: setRequest } = useLocalStorage(
    DECISION_REQUEST_KEY,
    DECISION_REQUEST_EMPTY,
  );

  mark.reset();

  function updateValue(logLabel: string, newValue: Partial<DecisionRequest>) {
    /**
     * "request" may not be te latest, it could happen that
     * we already call "setRequest" but that call didn't update "request" yet.
     * The caller didn't wait for a preact re-render.
     *
     * So we use the "mark" to get always an up-to-date "request". In this case
     * is important since we are doing a merge update.
     */
    // const old = mark.getLatestOrDefault(request)
    // const mergedValue = { ...old, ...newValue };
    const { old, merged } = mark.mergeWithLatestOrDefault(request, newValue);
    console.log("UPDATING DECISION REQUEST", {
      logLabel,
      old,
      merged,
    });
    setRequest(merged);
  }

  function start(d: Partial<DecisionRequest> | undefined) {
    const v = d ?? DECISION_REQUEST_EMPTY;
    const newDef = { ...DECISION_REQUEST_EMPTY, ...v };
    setDefault(newDef);
    console.log("STARTING NEW DECISION REQUEST", newDef);
    updateValue("starting", newDef);
  }
  function reset() {
    console.log("RESETTING TO DEFAULT");
    updateValue("resetting", currentDef);
  }

  return [request, updateValue, start, reset];
}
