/*
 This file is part of GNU Taler
 (C) 2022-2024 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,
  AmountJson,
  Amounts,
  Codec,
  CurrencySpecification,
  HttpStatusCode,
  OperationFail,
  OperationOk,
  TalerError,
  TalerErrorDetail,
  TalerExchangeApi,
  TranslatedString,
  assertUnreachable,
  buildCodecForObject,
  codecForNumber,
  codecForString,
  codecOptional,
} from "@gnu-taler/taler-util";
import {
  Attention,
  DefaultForm,
  FormMetadata,
  InternationalizationAPI,
  Loading,
  ShowInputErrorLabel,
  Time,
  useExchangeApiContext,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format, formatDuration, intervalToDuration } from "date-fns";
import { Fragment, Ref, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useUiFormsContext } from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.js";
import { useAccountInformation } from "../hooks/account.js";
import { useAccountDecisions } from "../hooks/decisions.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
import { useOfficer } from "../hooks/officer.js";

export type AmlEvent =
  | AmlFormEvent
  | AmlFormEventError
  | KycCollectionEvent
  | KycExpirationEvent;

type AmlFormEvent = {
  type: "aml-form";
  when: AbsoluteTime;
  title: TranslatedString;
  justification: Justification;
  metadata: FormMetadata;
  state: TalerExchangeApi.AmlState;
  threshold: AmountJson;
};
type AmlFormEventError = {
  type: "aml-form-error";
  when: AbsoluteTime;
  title: TranslatedString;
  justification: undefined;
  metadata: undefined;
  state: TalerExchangeApi.AmlState;
  threshold: AmountJson;
};
type KycCollectionEvent = {
  type: "kyc-collection";
  when: AbsoluteTime;
  title: TranslatedString;
  values: object;
  provider?: string;
};
type KycExpirationEvent = {
  type: "kyc-expiration";
  when: AbsoluteTime;
  title: TranslatedString;
  fields: string[];
};

type WithTime = { when: AbsoluteTime };

function selectSooner(a: WithTime, b: WithTime) {
  return AbsoluteTime.cmp(a.when, b.when);
}

function titleForJustification(
  op: ReturnType<typeof parseJustification>,
  i18n: InternationalizationAPI,
): TranslatedString {
  if (op.type === "ok") {
    return op.body.justification.label as TranslatedString;
  }
  switch (op.case) {
    case "not-json":
      return i18n.str`error: the justification is not a form`;
    case "id-not-found":
      return i18n.str`error: justification form's id not found`;
    case "version-not-found":
      return i18n.str`error: justification form's version not found`;
    case "form-not-found":
      return i18n.str`error: justification form not found`;
    default: {
      assertUnreachable(op.case);
    }
  }
}

export function getEventsFromAmlHistory(
  events: TalerExchangeApi.KycAttributeCollectionEvent[],
  i18n: InternationalizationAPI,
  forms: FormMetadata[],
): AmlEvent[] {
  // const ae: AmlEvent[] = aml.map((a) => {
  //   const just = parseJustification(a.justification, forms);
  //   return {
  //     type: just.type === "ok" ? "aml-form" : "aml-form-error",
  //     state: a.new_state,
  //     threshold: Amounts.parseOrThrow(a.new_threshold),
  //     title: titleForJustification(just, i18n),
  //     metadata: just.type === "ok" ? just.body.metadata : undefined,
  //     justification: just.type === "ok" ? just.body.justification : undefined,
  //     when: {
  //       t_ms:
  //         a.decision_time.t_s === "never"
  //           ? "never"
  //           : a.decision_time.t_s * 1000,
  //     },
  //   } as AmlEvent;
  // });
  // const ke = kyc.reduce((prev, k) => {
  //   prev.push({
  //     type: "kyc-collection",
  //     title: i18n.str`collection`,
  //     when: AbsoluteTime.fromProtocolTimestamp(k.collection_time),
  //     values: !k.attributes ? {} : k.attributes,
  //     provider: k.provider_section,
  //   });
  //   prev.push({
  //     type: "kyc-expiration",
  //     title: i18n.str`expiration`,
  //     when: AbsoluteTime.fromProtocolTimestamp(k.expiration_time),
  //     fields: !k.attributes ? [] : Object.keys(k.attributes),
  //   });
  //   return prev;
  // }, [] as AmlEvent[]);

  const ke = events.map((event) => {
    return {
      type: "kyc-collection",
      title: i18n.str`collection`,
      when: AbsoluteTime.fromProtocolTimestamp(event.collection_time),
      values: !event.attributes ? {} : event.attributes,
      provider: event.provider_name,
    } as AmlEvent;
  });
  return ke.sort(selectSooner);
}

export function CaseDetails({ account }: { account: string }) {
  const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
  const [showForm, setShowForm] = useState<{
    justification: Justification;
    metadata: FormMetadata;
  }>();
  const { config, lib } = useExchangeApiContext();
  const officer = useOfficer();
  const session = officer.state === "ready" ? officer.account : undefined;

  const { i18n } = useTranslationContext();
  const details = useAccountInformation(account);
  const history = useAccountDecisions(account);

  const { forms } = useUiFormsContext();

  const allForms = [...forms, ...preloadedForms(i18n)];
  if (!details || !history) {
    return <Loading />;
  }
  if (details instanceof TalerError) {
    return <ErrorLoadingWithDebug error={details} />;
  }
  if (details.type === "fail") {
    switch (details.case) {
      // case HttpStatusCode.Unauthorized:
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return <div />;
      default:
        assertUnreachable(details);
    }
  }
  if (history instanceof TalerError) {
    return <ErrorLoadingWithDebug error={history} />;
  }
  if (history.type === "fail") {
    switch (history.case) {
      // case HttpStatusCode.Unauthorized:
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
      case HttpStatusCode.Conflict:
        return <div />;
      default:
        assertUnreachable(history);
    }
  }
  const { details: accountDetails } = details.body;
  const activeDecision = history.body.find((d) => d.is_active);
  const restDecisions = !activeDecision
    ? history.body
    : history.body.filter((d) => d.rowid !== activeDecision.rowid);

  const events = getEventsFromAmlHistory(accountDetails, i18n, allForms);

  if (showForm !== undefined) {
    return (
      <DefaultForm
        readOnly={true}
        initial={showForm.justification.value}
        form={showForm.metadata as any} // FIXME: HERE
      >
        <div class="mt-6 flex items-center justify-end gap-x-6">
          <button
            class="text-sm font-semibold leading-6 text-gray-900"
            onClick={() => {
              setShowForm(undefined);
            }}
          >
            <i18n.Translate>Cancel</i18n.Translate>
          </button>
        </div>
      </DefaultForm>
    );
  }

  return (
    <div class="min-w-60">
      <header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8">
        <h1 class="text-base font-semibold leading-7 text-black">
          <i18n.Translate>
            Case history for account{" "}
            <span title={account}>{account.substring(0, 16)}...</span>
          </i18n.Translate>
        </h1>
      </header>

      {!activeDecision || !activeDecision.to_investigate ? undefined : (
        <Attention title={i18n.str`Under investigation`} type="warning">
          <i18n.Translate>
            This account requires a manual review and is waiting for a decision
            to be made.
          </i18n.Translate>
        </Attention>
      )}

      <div>
        <button
          onClick={async () => {
            if (!session) return;
            lib.exchange.makeAmlDesicion(session, {
              decision_time: AbsoluteTime.toProtocolTimestamp(
                AbsoluteTime.now(),
              ),
              h_payto: account,
              justification: "",
              keep_investigating: false,
              properties: {},
              new_rules: {
                custom_measures: {},
                expiration_time: AbsoluteTime.toProtocolTimestamp(
                  AbsoluteTime.never(),
                ),
                rules: FREEZE_RULES(config.currency),
                successor_measure: "verboten",
              },
            });
          }}
          class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        >
          <i18n.Translate>Freeze account</i18n.Translate>
        </button>
        <button
          onClick={async () => {
            if (!session) return;
            lib.exchange.makeAmlDesicion(session, {
              decision_time: AbsoluteTime.toProtocolTimestamp(
                AbsoluteTime.now(),
              ),
              h_payto: account,
              justification: "",
              keep_investigating: false,
              properties: {},
              new_rules: {
                custom_measures: {},
                expiration_time: AbsoluteTime.toProtocolTimestamp(
                  AbsoluteTime.never(),
                ),
                rules: THRESHOLD_100_HOUR(config.currency),
                successor_measure: "verboten",
              },
            });
          }}
          class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        >
          <i18n.Translate>Set threshold to 100 / hour</i18n.Translate>
        </button>
        <button
          onClick={async () => {
            if (!session) return;
            lib.exchange.makeAmlDesicion(session, {
              decision_time: AbsoluteTime.toProtocolTimestamp(
                AbsoluteTime.now(),
              ),
              h_payto: account,
              justification: "",
              keep_investigating: false,
              properties: {},
              new_rules: {
                custom_measures: {},
                expiration_time: AbsoluteTime.toProtocolTimestamp(
                  AbsoluteTime.never(),
                ),
                rules: THRESHOLD_2000_WEEK(config.currency),
                successor_measure: "verboten",
              },
            });
          }}
          class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        >
          <i18n.Translate>Set threshold to 2000 / week</i18n.Translate>
        </button>
        <button
          onClick={async () => {
            if (!session) return;
            lib.exchange.makeAmlDesicion(session, {
              decision_time: AbsoluteTime.toProtocolTimestamp(
                AbsoluteTime.now(),
              ),
              h_payto: account,
              justification: "",
              keep_investigating: false,
              properties: {},
              new_measure: "m2",
              new_rules: {
                custom_measures: {},
                expiration_time: AbsoluteTime.toProtocolTimestamp(
                  AbsoluteTime.never(),
                ),
                rules: FREEZE_RULES(config.currency),
                successor_measure: "verboten",
              },
            });
          }}
          class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        >
          <i18n.Translate>Ask for more information</i18n.Translate>
        </button>
      </div>

      {!activeDecision ? (
        <Attention title={i18n.str`No active rules found`} type="warning" />
      ) : (
        <Fragment>
          <h1 class="my-4 text-base font-semibold leading-6 text-black">
            <i18n.Translate>Current active rules</i18n.Translate>
          </h1>
          <ShowDecisionInfo decision={activeDecision} startOpen />
        </Fragment>
      )}
      <h1 class="my-4 text-base font-semibold leading-6 text-black">
        <i18n.Translate>KYC collection events</i18n.Translate>
      </h1>
      <ShowTimeline
        history={events}
        onSelect={(e) => {
          switch (e.type) {
            case "aml-form": {
              const { justification, metadata } = e;
              setShowForm({ justification, metadata });
              break;
            }
            case "kyc-collection":
            case "kyc-expiration": {
              setSelected(e.when);
              break;
            }
            case "aml-form-error":
          }
        }}
      />
      {/* {selected && <ShowEventDetails event={selected} />} */}
      {selected && <ShowConsolidated history={events} until={selected} />}
      {restDecisions.length > 0 ? (
        <Fragment>
          <h1 class="my-4 text-base font-semibold leading-6 text-black">
            <i18n.Translate>Previous AML decisions</i18n.Translate>
          </h1>
          {restDecisions.map((d) => {
            return <ShowDecisionInfo decision={d} />;
          })}
        </Fragment>
      ) : (
        <div />
      )}
    </div>
  );
}

function ShowDecisionInfo({
  decision,
  startOpen
}: {
  decision: TalerExchangeApi.AmlDecision;
  startOpen?: boolean,
}): VNode {
  const { i18n } = useTranslationContext();
  const { config } = useExchangeApiContext();
  const [opened, setOpened] = useState(startOpen ?? false)


  function Header() {
    return <ul role="list" class="divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl">
      <li class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6">
        <div class="flex min-w-0 gap-x-4">
          <div class="flex mt-2 rounded-md  shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
            <div class="pointer-events-none bg-gray-200 inset-y-0 flex items-center px-3">
              <i18n.Translate>Since</i18n.Translate>
            </div>
            <div class="p-2  disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900  placeholder:text-gray-400  sm:text-sm sm:leading-6">
              <Time
                format="dd/MM/yyyy HH:mm:ss"
                timestamp={AbsoluteTime.fromProtocolTimestamp(
                  decision.decision_time,
                )}
              />
            </div>
          </div>
        </div>
        <div class="flex shrink-0 items-center gap-x-4">
          <div class="flex mt-2 rounded-md  shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
            <div class="pointer-events-none bg-gray-200 inset-y-0 flex items-center px-3">
              {AbsoluteTime.isExpired(
                AbsoluteTime.fromProtocolTimestamp(
                  decision.limits.expiration_time,
                ),
              ) ? (
                <i18n.Translate>Expired</i18n.Translate>
              ) : (
                <i18n.Translate>Expires</i18n.Translate>
              )}
            </div>
            <div class="p-2  disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900  placeholder:text-gray-400  sm:text-sm sm:leading-6">
              <Time
                format="dd/MM/yyyy HH:mm:ss"
                timestamp={AbsoluteTime.fromProtocolTimestamp(
                  decision.limits.expiration_time,
                )}
              />
            </div>
          </div>
        </div>
      </li>
    </ul>
  }

  if (!opened) {
    return <div class="mt-4 cursor-pointer" onClick={() => setOpened(true)}>
      <Header />
    </div>
  }
  const balanceLimit = decision.limits.rules.find(r => r.operation_type === "BALANCE")

  return (
    <div>

      <div class="mt-4 cursor-pointer" onClick={() => setOpened(false)}>
        <Header />
      </div>

      {!decision.justification ? undefined :
        <div>
          <label for="comment" class="block text-sm font-medium leading-6 text-gray-900">Description</label>
          <div class="mt-2">
            <textarea rows={2} name="comment" id="comment" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
              {decision.justification}
            </textarea>
          </div>
        </div>
      }

      {!balanceLimit ? undefined :
        <div class="px-4">
          <div class="flex mt-2 rounded-md w-fit  shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
            <div class="whitespace-nowrap pointer-events-none bg-gray-200 inset-y-0 items-center px-3 flex">
              <i18n.Translate>Max balance</i18n.Translate>
            </div>
            <div class="p-2  disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left py-1.5 pl-3 text-gray-900  placeholder:text-gray-400  sm:text-sm sm:leading-6">
              <RenderAmount
                value={Amounts.parseOrThrow(balanceLimit.threshold)}
                spec={config.currency_specification}
              />

            </div>
          </div>
        </div>

      }

      <div class="w-full px-4 pt-4">

        <table class="min-w-full divide-y divide-gray-300">
          <thead class="bg-gray-50">
            <tr>
              <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"><i18n.Translate>Operation</i18n.Translate></th>
              <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"><i18n.Translate>Timeframe</i18n.Translate></th>
              <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6"><i18n.Translate>Amount</i18n.Translate></th>
            </tr>
          </thead>
          <tbody class="divide-y divide-gray-200">
            {decision.limits.rules.map((r) => {
              if (r.operation_type === "BALANCE") return;
              return (
                <tr>
                  <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 text-left">{r.operation_type}</td>
                  <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
                    {r.timeframe.d_us === "forever"
                      ? ""
                      : formatDuration(
                        intervalToDuration({
                          start: 0,
                          end: r.timeframe.d_us / 1000,
                        }),
                      )}
                  </td>
                  <td class=" relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 text-right">
                    <RenderAmount
                      value={Amounts.parseOrThrow(r.threshold)}
                      spec={config.currency_specification}
                    />
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

    </div>
  );
}

export function RenderAmount({
  value,
  spec,
  negative,
  withColor,
  hideSmall,
}: {
  spec: CurrencySpecification;
  value: AmountJson;
  hideSmall?: boolean;
  negative?: boolean;
  withColor?: boolean;
}): VNode {
  const neg = !!negative; // convert to true or false

  const { currency, normal, small } = Amounts.stringifyValueWithSpec(
    value,
    spec,
  );

  return (
    <span
      data-negative={withColor ? neg : undefined}
      class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
    >
      {negative ? "- " : undefined}
      {currency} {normal}{" "}
      {!hideSmall && small && <sup class="-ml-1">{small}</sup>}
    </span>
  );
}

function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode {
  switch (state) {
    case TalerExchangeApi.AmlState.normal: {
      return (
        <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
          Normal
        </span>
      );
    }
    case TalerExchangeApi.AmlState.pending: {
      return (
        <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
          Pending
        </span>
      );
    }
    case TalerExchangeApi.AmlState.frozen: {
      return (
        <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
          Frozen
        </span>
      );
    }
  }
  assertUnreachable(state);
}

function ShowTimeline({
  history,
  onSelect,
}: {
  onSelect: (e: AmlEvent) => void;
  history: AmlEvent[];
}): VNode {
  return (
    <div class="flow-root">
      <ul role="list">
        {history.map((e, idx) => {
          const isLast = history.length - 1 === idx;
          return (
            <li
              key={idx}
              data-ok={e.type !== "aml-form-error"}
              class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer"
              onClick={() => {
                onSelect(e);
              }}
            >
              <div class="relative pb-6">
                {!isLast ? (
                  <span
                    class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
                    aria-hidden="true"
                  ></span>
                ) : undefined}
                <div class="relative flex space-x-3">
                  {(() => {
                    switch (e.type) {
                      case "aml-form-error":
                      case "aml-form": {
                        return (
                          <div>
                            <AmlStateBadge state={e.state} />
                            <span class="inline-flex items-center  px-2 py-1 text-xs font-medium text-gray-700 ">
                              {e.threshold.currency}{" "}
                              {Amounts.stringifyValue(e.threshold)}
                            </span>
                          </div>
                        );
                      }
                      case "kyc-collection": {
                        return (
                          // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
                          <svg
                            xmlns="http://www.w3.org/2000/svg"
                            fill="none"
                            viewBox="0 0 24 24"
                            stroke-width="1.5"
                            stroke="currentColor"
                            class="w-6 h-6"
                          >
                            <path
                              stroke-linecap="round"
                              stroke-linejoin="round"
                              d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
                            />
                          </svg>
                        );
                      }
                      case "kyc-expiration": {
                        // return <ClockIcon class="h-8 w-8 text-gray-700" />;
                        return (
                          <svg
                            xmlns="http://www.w3.org/2000/svg"
                            fill="none"
                            viewBox="0 0 24 24"
                            stroke-width="1.5"
                            stroke="currentColor"
                            class="w-6 h-6"
                          >
                            <path
                              stroke-linecap="round"
                              stroke-linejoin="round"
                              d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
                            />
                          </svg>
                        );
                      }
                    }
                    assertUnreachable(e);
                  })()}
                  <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
                    {e.type === "aml-form" ? (
                      <span
                        // href={Pages.newFormEntry.url({ account })}
                        class="block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
                      >
                        {e.title}
                      </span>
                    ) : (
                      <p class="text-sm text-gray-900">{e.title}</p>
                    )}
                    <div class="whitespace-nowrap text-right text-sm text-gray-500">
                      {e.when.t_ms === "never" ? (
                        "never"
                      ) : (
                        <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
                          {format(e.when.t_ms, "dd MMM yyyy HH:mm:ss")}
                        </time>
                      )}
                    </div>
                  </div>
                </div>
              </div>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

function InputAmount(
  {
    currency,
    name,
    value,
    left,
    onChange,
  }: {
    currency: string;
    name: string;
    left?: boolean | undefined;
    value: string | undefined;
    onChange?: (s: string) => void;
  },
  ref: Ref<HTMLInputElement>,
): VNode {
  const FRAC_SEPARATOR = ",";
  const { config } = useExchangeApiContext();
  return (
    <div class="mt-2">
      <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
        <div class="pointer-events-none inset-y-0 flex items-center px-3">
          <span class="text-gray-500 sm:text-sm">{currency}</span>
        </div>
        <input
          type="number"
          data-left={left}
          class="disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900  placeholder:text-gray-400  sm:text-sm sm:leading-6"
          placeholder="0.00"
          aria-describedby="price-currency"
          ref={ref}
          name={name}
          id={name}
          autocomplete="off"
          value={value ?? ""}
          disabled={!onChange}
          onInput={(e) => {
            if (!onChange) return;
            const l = e.currentTarget.value.length;
            const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR);
            if (
              sep_pos !== -1 &&
              l - sep_pos - 1 >
              config.currency_specification.num_fractional_input_digits
            ) {
              e.currentTarget.value = e.currentTarget.value.substring(
                0,
                sep_pos +
                config.currency_specification.num_fractional_input_digits +
                1,
              );
            }
            onChange(e.currentTarget.value);
          }}
        />
      </div>
    </div>
  );
}

export type Justification<T = Record<string, unknown>> = {
  // form values
  value: T;
} & Omit<Omit<FormMetadata, "icon">, "config">;

type SimpleFormMetadata = {
  version?: number;
  id?: string;
};

export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
  buildCodecForObject<SimpleFormMetadata>()
    .property("id", codecOptional(codecForString()))
    .property("version", codecOptional(codecForNumber()))
    .build("SimpleFormMetadata");

type ParseJustificationFail =
  | "not-json"
  | "id-not-found"
  | "form-not-found"
  | "version-not-found";

function parseJustification(
  s: string,
  listOfAllKnownForms: FormMetadata[],
):
  | OperationOk<{
    justification: Justification;
    metadata: FormMetadata;
  }>
  | OperationFail<ParseJustificationFail> {
  try {
    const justification = JSON.parse(s);
    const info = codecForSimpleFormMetadata().decode(justification);
    if (!info.id) {
      return {
        type: "fail",
        case: "id-not-found",
        detail: {} as TalerErrorDetail,
      };
    }
    if (!info.version) {
      return {
        type: "fail",
        case: "version-not-found",
        detail: {} as TalerErrorDetail,
      };
    }
    const found = listOfAllKnownForms.find((f) => {
      return f.id === info.id && f.version === info.version;
    });
    if (!found) {
      return {
        type: "fail",
        case: "form-not-found",
        detail: {} as TalerErrorDetail,
      };
    }
    return {
      type: "ok",
      body: {
        justification,
        metadata: found,
      },
    };
  } catch (e) {
    return {
      type: "fail",
      case: "not-json",
      detail: {} as TalerErrorDetail,
    };
  }
}

const THRESHOLD_2000_WEEK: (currency: string) => TalerExchangeApi.KycRule[] = (
  currency,
) => [
    {
      operation_type: "WITHDRAW",
      threshold: `${currency}:2000`,
      timeframe: {
        d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "DEPOSIT",
      threshold: `${currency}:2000`,
      timeframe: {
        d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "AGGREGATE",
      threshold: `${currency}:2000`,
      timeframe: {
        d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "MERGE",
      threshold: `${currency}:2000`,
      timeframe: {
        d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "BALANCE",
      threshold: `${currency}:2000`,
      timeframe: {
        d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "CLOSE",
      threshold: `${currency}:2000`,
      timeframe: {
        d_us: 7 * 24 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
  ];

const THRESHOLD_100_HOUR: (currency: string) => TalerExchangeApi.KycRule[] = (
  currency,
) => [
    {
      operation_type: "WITHDRAW",
      threshold: `${currency}:100`,
      timeframe: {
        d_us: 1 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "DEPOSIT",
      threshold: `${currency}:100`,
      timeframe: {
        d_us: 1 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "AGGREGATE",
      threshold: `${currency}:100`,
      timeframe: {
        d_us: 1 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "MERGE",
      threshold: `${currency}:100`,
      timeframe: {
        d_us: 1 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "BALANCE",
      threshold: `${currency}:100`,
      timeframe: {
        d_us: 1 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "CLOSE",
      threshold: `${currency}:100`,
      timeframe: {
        d_us: 1 * 60 * 60 * 1000 * 1000,
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
  ];

const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = (
  currency,
) => [
    {
      operation_type: "WITHDRAW",
      threshold: `${currency}:0`,
      timeframe: {
        d_us: "forever",
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "DEPOSIT",
      threshold: `${currency}:0`,
      timeframe: {
        d_us: "forever",
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "AGGREGATE",
      threshold: `${currency}:0`,
      timeframe: {
        d_us: "forever",
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "MERGE",
      threshold: `${currency}:0`,
      timeframe: {
        d_us: "forever",
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "BALANCE",
      threshold: `${currency}:0`,
      timeframe: {
        d_us: "forever",
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
    {
      operation_type: "CLOSE",
      threshold: `${currency}:0`,
      timeframe: {
        d_us: "forever",
      },
      measures: ["verboten"],
      display_priority: 1,
      exposed: true,
      is_and_combinator: true,
    },
  ];
