/*
 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,
  AmountString,
  Amounts,
  CurrencySpecification,
  FRAC_SEPARATOR,
  HttpStatusCode,
  PaytoString,
  PaytoUri,
  TalerCorebankApi,
  TalerErrorCode,
  TranslatedString,
  assertUnreachable,
  buildPayto,
  parsePaytoUri,
  stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import {
  InternationalizationAPI,
  LocalNotificationBanner,
  RouteDefinition,
  ShowInputErrorLabel,
  notifyInfo,
  useBankCoreApiContext,
  useLocalNotification,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, Ref, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { mutate } from "swr";
import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
import { useBankState } from "../hooks/bank-state.js";
import { useSessionState } from "../hooks/session.js";
import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js";

interface Props {
  focus?: boolean;
  withAccount?: string;
  withSubject?: string;
  withAmount?: string;
  onSuccess: () => void;
  onAuthorizationRequired: () => void;
  routeCancel?: RouteDefinition;
  routeCashout?: RouteDefinition;
  routeHere: RouteDefinition<{
    account?: string;
    subject?: string;
    amount?: string;
  }>;
  limit: AmountJson;
  balance: AmountJson;
}

export function PaytoWireTransferForm({
  focus,
  withAccount,
  withSubject,
  withAmount,
  onSuccess,
  routeCancel,
  routeCashout,
  routeHere,
  onAuthorizationRequired,
  limit,
  balance,
}: Props): VNode {
  const [inputType, setInputType] = useState<"form" | "payto" | "qr">("form");
  const isRawPayto = inputType !== "form";

  const { state: credentials } = useSessionState();
  const {
    lib: { bank: api },
    config,
    url,
  } = useBankCoreApiContext();

  const sendingToFixedAccount = withAccount !== undefined;

  const [account, setAccount] = useState<string | undefined>(withAccount);
  const [subject, setSubject] = useState<string | undefined>(withSubject);
  const [amount, setAmount] = useState<string | undefined>(withAmount);
  const [, updateBankState] = useBankState();

  const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
    undefined,
  );
  const { i18n } = useTranslationContext();

  const trimmedAmountStr = amount?.trim();
  const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
  const [notification, notify, handleError] = useLocalNotification();

  const paytoType =
    config.wire_type === "X_TALER_BANK"
      ? ("x-taler-bank" as const)
      : ("iban" as const);

  const wireFee =
    config.wire_transfer_fees === undefined
      ? Amounts.zeroOfCurrency(config.currency)
      : Amounts.parseOrThrow(config.wire_transfer_fees);

  const limitWithFee =
    Amounts.cmp(limit, wireFee) === 1
      ? Amounts.sub(limit, wireFee).amount
      : Amounts.zeroOfAmount(limit);

  const errorsWire = undefinedIfEmpty({
    account: !account
      ? i18n.str`Required`
      : paytoType === "iban"
        ? validateIBAN(account, i18n)
        : paytoType === "x-taler-bank"
          ? validateTalerBank(account, i18n)
          : undefined,
    subject: !subject ? i18n.str`Required` : validateSubject(subject, i18n),
    amount: !trimmedAmountStr
      ? i18n.str`Required`
      : !parsedAmount
        ? i18n.str`Not valid`
        : validateAmount(parsedAmount, limitWithFee, i18n),
  });

  const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);

  const errorsPayto = undefinedIfEmpty({
    rawPaytoInput: !rawPaytoInput
      ? i18n.str`Required`
      : !parsed
        ? i18n.str`Does not follow the pattern`
        : validateRawPayto(parsed, limitWithFee, url.host, i18n, paytoType),
  });

  async function doSend() {
    let payto_uri: PaytoString | undefined;
    let sendingAmount: AmountString | undefined;

    if (credentials.status !== "loggedIn") return;
    let acName: string | undefined;
    if (isRawPayto) {
      const p = parsePaytoUri(rawPaytoInput!);
      if (!p) return;
      sendingAmount = p.params.amount as AmountString;
      delete p.params.amount;
      // if this payto is valid then it already have message
      payto_uri = stringifyPaytoUri(p);
      acName = !p.isKnown
        ? undefined
        : p.targetType === "iban"
          ? p.iban
          : p.targetType === "bitcoin"
            ? p.address
            : p.targetType === "x-taler-bank"
              ? p.account
              : assertUnreachable(p);
    } else {
      if (!account || !subject) return;
      let payto;
      acName = account;
      switch (paytoType) {
        case "x-taler-bank": {
          payto = buildPayto("x-taler-bank", url.host, account);

          break;
        }
        case "iban": {
          payto = buildPayto("iban", account, undefined);
          break;
        }
        default:
          assertUnreachable(paytoType);
      }

      payto.params.message = encodeURIComponent(subject);
      payto_uri = stringifyPaytoUri(payto);
      sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString;
    }
    const puri = payto_uri;
    const sAmount = sendingAmount;

    await handleError(async function createTransactionHandleError() {
      const request: TalerCorebankApi.CreateTransactionRequest = {
        payto_uri: puri,
        amount: sAmount,
      };
      const check = IdempotencyRetry.tryFiveTimes();
      const resp = await api.createTransaction(credentials, request, check);
      mutate(() => true);
      if (resp.type === "fail") {
        switch (resp.case) {
          case HttpStatusCode.BadRequest:
            return notify({
              type: "error",
              title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          case HttpStatusCode.Unauthorized:
            return notify({
              type: "error",
              title: i18n.str`Not enough permission to complete the operation.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          case TalerErrorCode.BANK_ADMIN_CREDITOR:
            return notify({
              type: "error",
              title: i18n.str`Bank administrator can't be the transfer creditor.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
            return notify({
              type: "error",
              title: i18n.str`The destination account "${
                acName ?? puri
              }" was not found.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          case TalerErrorCode.BANK_SAME_ACCOUNT:
            return notify({
              type: "error",
              title: i18n.str`The origin and the destination of the transfer can't be the same.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          case TalerErrorCode.BANK_UNALLOWED_DEBIT:
            return notify({
              type: "error",
              title: i18n.str`Your balance is not enough.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          case HttpStatusCode.NotFound:
            return notify({
              type: "error",
              title: i18n.str`The origin account "${puri}" was not found.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: {
            return notify({
              type: "error",
              title: i18n.str`Tried to create the transaction ${check.maxTries} times with different UID but failed.`,
              description: resp.detail.hint as TranslatedString,
              debug: resp.detail,
              when: AbsoluteTime.now(),
            });
          }
          case HttpStatusCode.Accepted: {
            updateBankState("currentChallenge", {
              operation: "create-transaction",
              id: String(resp.body.challenge_id),
              location: routeHere.url({
                account: account ?? "",
                amount,
                subject,
              }),
              sent: AbsoluteTime.never(),
              request,
            });
            return onAuthorizationRequired();
          }
          default:
            assertUnreachable(resp);
        }
      }
      notifyInfo(i18n.str`Wire transfer created!`);
      onSuccess();
      setAmount(undefined);
      setAccount(undefined);
      setSubject(undefined);
      rawPaytoInputSetter(undefined);
    });
  }

  return (
    <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 my-4 md:grid-cols-3 bg-gray-100 px-4 pb-4 rounded-lg">
      <div>
        <fieldset class="px-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
          <legend class="sr-only">
            <i18n.Translate>Input wire transfer detail</i18n.Translate>
          </legend>
          <div class="-space-y-px rounded-md ">
            <label
              data-checked={inputType === "form"}
              class="group rounded-tl-md rounded-tr-md relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
            >
              <input
                type="radio"
                name="input-type"
                onChange={() => {
                  if (parsed && parsed.isKnown) {
                    switch (parsed.targetType) {
                      case "iban": {
                        setAccount(parsed.iban);
                        break;
                      }
                      case "x-taler-bank": {
                        setAccount(parsed.account);
                        break;
                      }
                      case "bitcoin": {
                        break;
                      }
                      default: {
                        assertUnreachable(parsed);
                      }
                    }
                    const amountStr = !parsed.params
                      ? undefined
                      : parsed.params["amount"];
                    if (amountStr) {
                      const amount = Amounts.parse(amountStr);
                      if (amount) {
                        setAmount(Amounts.stringifyValue(amount));
                      }
                    }
                    const subject = parsed.params["message"];
                    if (subject) {
                      setSubject(subject);
                    }
                  }
                  setInputType("form");
                }}
                checked={inputType === "form"}
                value="form"
                class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
              />
              <span class="ml-3 flex flex-col">
                {/* <!-- Checked: "text-indigo-900", Not Checked: "text-gray-900" --> */}
                <span
                  data-checked={inputType === "form"}
                  class="block text-sm font-medium data-[checked=true]:text-indigo-900"
                >
                  <i18n.Translate>Using a form</i18n.Translate>
                </span>
              </span>
            </label>
            {sendingToFixedAccount ? undefined : (
              <Fragment>
                <label
                  data-checked={inputType === "payto"}
                  class="relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
                >
                  <input
                    type="radio"
                    name="input-type"
                    onChange={() => {
                      if (account) {
                        let payto;
                        switch (paytoType) {
                          case "x-taler-bank": {
                            payto = buildPayto(
                              "x-taler-bank",
                              url.host,
                              account,
                            );
                            if (parsedAmount) {
                              payto.params["amount"] =
                                Amounts.stringify(parsedAmount);
                            }
                            if (subject) {
                              payto.params["message"] = subject;
                            }
                            break;
                          }
                          case "iban": {
                            payto = buildPayto("iban", account, undefined);
                            if (parsedAmount) {
                              payto.params["amount"] =
                                Amounts.stringify(parsedAmount);
                            }
                            if (subject) {
                              payto.params["message"] = subject;
                            }
                            break;
                          }
                          default:
                            assertUnreachable(paytoType);
                        }
                        rawPaytoInputSetter(stringifyPaytoUri(payto));
                      }
                      setInputType("payto");
                    }}
                    checked={inputType === "payto"}
                    value="payto"
                    class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
                  />
                  <span class="ml-3 flex flex-col">
                    <span
                      data-checked={inputType === "payto"}
                      class="block  font-medium data-[checked=true]:text-indigo-900"
                    >
                      payto:// URI
                    </span>
                    <span
                      data-checked={inputType === "payto"}
                      class="block text-sm text-gray-500 data-[checked=true]:text-indigo-600"
                    >
                      <i18n.Translate>
                        A special URI that indicate the transfer amount and
                        account target.
                      </i18n.Translate>
                    </span>
                  </span>
                </label>
                {
                  //FIXME: add QR support
                  false && (
                    <label
                      data-checked={inputType === "qr"}
                      class="rounded-bl-md rounded-br-md relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
                    >
                      <input
                        type="radio"
                        name="input-type"
                        onChange={() => {
                          setInputType("qr");
                        }}
                        checked={inputType === "qr"}
                        value="qr"
                        class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
                      />
                      <span class="ml-3 flex flex-col">
                        <span
                          data-checked={inputType === "qr"}
                          class="block font-medium data-[checked=true]:text-indigo-900"
                        >
                          <i18n.Translate>QR code</i18n.Translate>
                        </span>
                        <span
                          data-checked={inputType === "qr"}
                          class="block text-sm text-gray-500 data-[checked=true]:text-indigo-600"
                        >
                          <i18n.Translate>
                            If you have a camera in this device you can import a
                            payto:// URI from a QR code.
                          </i18n.Translate>
                        </span>
                      </span>
                    </label>
                  )
                }
              </Fragment>
            )}
          </div>
          {routeCashout ? (
            <a
              name="do cashout"
              href={routeCashout.url({})}
              class="bg-white p-4 rounded-lg text-sm font-semibold leading-6 text-gray-900"
            >
              <i18n.Translate>Cashout</i18n.Translate>
            </a>
          ) : undefined}
        </fieldset>
      </div>

      <form
        class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md sm:rounded-xl md:col-span-2 w-fit mx-auto"
        autoCapitalize="none"
        autoCorrect="off"
        onSubmit={(e) => {
          e.preventDefault();
        }}
      >
        <div class="m-4">
          {!isRawPayto ? (
            <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 ">
              {(() => {
                switch (paytoType) {
                  case "x-taler-bank": {
                    return (
                      <TextField
                        id="x-taler-bank"
                        required
                        label={i18n.str`Recipient`}
                        help={i18n.str`Id of the recipient's account`}
                        error={errorsWire?.account}
                        onChange={setAccount}
                        value={account}
                        placeholder={i18n.str`username`}
                        focus={focus}
                        disabled={sendingToFixedAccount}
                      />
                    );
                  }
                  case "iban": {
                    return (
                      <TextField
                        id="iban"
                        required
                        label={i18n.str`Recipient`}
                        help={i18n.str`IBAN of the recipient's account`}
                        placeholder={"CC0123456789" as TranslatedString}
                        error={errorsWire?.account}
                        onChange={(v) => setAccount(v.toUpperCase())}
                        value={account}
                        focus={focus}
                        disabled={sendingToFixedAccount}
                      />
                    );
                  }
                  default:
                    assertUnreachable(paytoType);
                }
              })()}

              <div class="sm:col-span-5">
                <label
                  for="subject"
                  class="block text-sm font-medium leading-6 text-gray-900"
                >
                  {i18n.str`Transfer subject`}
                  <b style={{ color: "red" }}> *</b>
                </label>
                <div class="mt-2">
                  <input
                    type="text"
                    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"
                    name="subject"
                    id="subject"
                    autocomplete="off"
                    placeholder={i18n.str`Subject`}
                    value={subject ?? ""}
                    required
                    onInput={(e): void => {
                      setSubject(e.currentTarget.value);
                    }}
                  />
                  <ShowInputErrorLabel
                    message={errorsWire?.subject}
                    isDirty={subject !== undefined}
                  />
                </div>
                <p class="mt-2 text-sm text-gray-500">
                  <i18n.Translate>
                    Some text to identify the transfer
                  </i18n.Translate>
                </p>
              </div>

              <div class="sm:col-span-5">
                <label
                  for="amount"
                  class="block text-sm font-medium leading-6 text-gray-900"
                >
                  {i18n.str`Amount`}
                  <b style={{ color: "red" }}> *</b>
                </label>
                <InputAmount
                  name="amount"
                  left
                  currency={limit.currency}
                  value={trimmedAmountStr}
                  onChange={(d) => {
                    setAmount(d);
                  }}
                />
                <ShowInputErrorLabel
                  message={errorsWire?.amount}
                  isDirty={trimmedAmountStr !== undefined}
                />
                <p class="mt-2 text-sm text-gray-500">
                  <i18n.Translate>Amount to transfer</i18n.Translate>
                </p>
              </div>
            </div>
          ) : (
            <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full">
              <div class="sm:col-span-6">
                <label
                  for="address"
                  class="block text-sm font-medium leading-6 text-gray-900"
                >
                  {i18n.str`Payto URI:`}
                  <b style={{ color: "red" }}> *</b>
                </label>
                <div class="mt-2">
                  <textarea
                    ref={focus ? doAutoFocus : undefined}
                    name="address"
                    id="address"
                    type="textarea"
                    rows={5}
                    class="block overflow-hidden w-44 sm:w-96 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"
                    value={rawPaytoInput ?? ""}
                    required
                    title={i18n.str`Uniform resource identifier of the target account`}
                    placeholder={((): TranslatedString => {
                      switch (paytoType) {
                        case "x-taler-bank":
                          return i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`;
                        case "iban":
                          return i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`;
                      }
                    })()}
                    onInput={(e): void => {
                      rawPaytoInputSetter(e.currentTarget.value);
                    }}
                  />
                  <ShowInputErrorLabel
                    message={errorsPayto?.rawPaytoInput}
                    isDirty={rawPaytoInput !== undefined}
                  />
                </div>
              </div>
            </div>
          )}
          {Amounts.cmp(limitWithFee, balance) > 0 ? (
            <p class="mt-2 text-sm text-gray-900">
              <i18n.Translate>
                You can transfer{" "}
                <RenderAmount
                  value={limitWithFee}
                  spec={config.currency_specification}
                />
              </i18n.Translate>
            </p>
          ) : undefined}
        </div>
        {Amounts.isZero(wireFee) ? undefined : (
          <div class="px-4 my-4">
            <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
              <div class="sm:col-span-6">
                <dl class="mt-4 space-y-4">
                  <Fragment>
                    <div class="flex items-center justify-between ">
                      <dt class="flex items-center text-sm text-gray-600">
                        <span>
                          <i18n.Translate>Cost</i18n.Translate>
                        </span>
                      </dt>
                      <dd class="text-sm text-gray-900">
                        <RenderAmount
                          value={wireFee}
                          negative
                          withColor
                          spec={config.currency_specification}
                        />
                      </dd>
                    </div>
                  </Fragment>
                </dl>
              </div>
            </div>
          </div>
        )}
        <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
          {routeCancel ? (
            <a
              name="cancel"
              href={routeCancel.url({})}
              class="text-sm font-semibold leading-6 text-gray-900"
            >
              <i18n.Translate>Cancel</i18n.Translate>
            </a>
          ) : (
            <div />
          )}
          <button
            type="submit"
            name="send"
            class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
            disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
            onClick={(e) => {
              e.preventDefault();
              doSend();
            }}
          >
            <i18n.Translate>Send</i18n.Translate>
          </button>
        </div>
        <LocalNotificationBanner notification={notification} />
      </form>
    </div>
  );
}

/**
 * Show the element when the load ended
 * @param element
 */
export function doAutoFocus(element: HTMLElement | null) {
  if (element) {
    setTimeout(() => {
      element.focus({ preventScroll: true });
      element.scrollIntoView({
        behavior: "smooth",
        block: "center",
        inline: "center",
      });
    }, 100);
  }
}

export function InputAmount(
  {
    currency,
    name,
    value,
    error,
    left,
    onChange,
  }: {
    error?: string;
    currency: string;
    name: string;
    left?: boolean | undefined;
    value: string | undefined;
    onChange?: (s: string) => void;
  },
  ref: Ref<HTMLInputElement>,
): VNode {
  const { config } = useBankCoreApiContext();
  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>
      <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
    </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 validateRawPayto(
  parsed: PaytoUri,
  limit: AmountJson,
  host: string,
  i18n: InternationalizationAPI,
  type: "iban" | "x-taler-bank",
): TranslatedString | undefined {
  if (!parsed.isKnown) {
    return i18n.str`The target type is unknown, use "${type}"`;
  }
  let result: TranslatedString | undefined;
  switch (type) {
    case "x-taler-bank": {
      if (parsed.targetType !== "x-taler-bank") {
        return i18n.str`Only "x-taler-bank" target are supported`;
      }

      if (parsed.host !== host) {
        return i18n.str`Only this host is allowed. Use "${host}"`;
      }

      if (!parsed.account) {
        return i18n.str`Missing account name`;
      }
      const result = validateTalerBank(parsed.account, i18n);
      if (result) return result;
      break;
    }
    case "iban": {
      if (parsed.targetType !== "iban") {
        return i18n.str`Only "IBAN" target are supported`;
      }
      const result = validateIBAN(parsed.iban, i18n);
      if (result) return result;
      break;
    }
    default:
      assertUnreachable(type);
  }
  if (!parsed.params.amount) {
    return i18n.str`Missing "amount" parameter to specify the amount to be transferred`;
  }
  const amount = Amounts.parse(parsed.params.amount);
  if (!amount) {
    return i18n.str`The "amount" parameter is not valid`;
  }
  result = validateAmount(amount, limit, i18n);
  if (result) return result;

  if (!parsed.params.message) {
    return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`;
  }
  const subject = parsed.params.message;
  result = validateSubject(subject, i18n);
  if (result) return result;

  return undefined;
}

function validateAmount(
  amount: AmountJson,
  limit: AmountJson,
  i18n: InternationalizationAPI,
): TranslatedString | undefined {
  if (amount.currency !== limit.currency) {
    return i18n.str`The only currency allowed is "${limit.currency}"`;
  }
  if (Amounts.isZero(amount)) {
    return i18n.str`Can't transfer zero amount`;
  }
  if (Amounts.cmp(limit, amount) === -1) {
    return i18n.str`Balance is not enough`;
  }
  return undefined;
}

function validateSubject(
  text: string,
  i18n: InternationalizationAPI,
): TranslatedString | undefined {
  if (text.length < 2) {
    return i18n.str`Use a longer subject`;
  }
  return undefined;
}

interface PaytoFieldProps {
  id: string;
  label: TranslatedString;
  required?: boolean;
  help?: TranslatedString;
  placeholder?: TranslatedString;
  error: string | undefined;
  value: string | undefined;
  rightIcons?: VNode;
  onChange: (p: string) => void;
  focus?: boolean;
  disabled?: boolean;
}

function Wrapper({
  withIcon,
  children,
}: {
  withIcon: boolean;
  children: ComponentChildren;
}): VNode {
  if (withIcon) {
    return <div class="flex justify-between">{children}</div>;
  }
  return <Fragment>{children}</Fragment>;
}

export function TextField({
  id,
  label,
  help,
  focus,
  disabled,
  onChange,
  placeholder,
  rightIcons,
  required,
  value,
  error,
}: PaytoFieldProps): VNode {
  return (
    <div class="sm:col-span-5">
      <label for={id} class="block text-sm font-medium leading-6 text-gray-900">
        {label}
        {required && <b style={{ color: "red" }}> *</b>}
      </label>
      <div class="mt-2">
        <Wrapper withIcon={rightIcons !== undefined}>
          <input
            ref={focus ? doAutoFocus : undefined}
            type="text"
            class="block w-full disabled:bg-gray-200 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"
            name={id}
            id={id}
            disabled={disabled}
            value={value ?? ""}
            placeholder={placeholder}
            autocomplete="off"
            required
            onInput={(e): void => {
              onChange(e.currentTarget.value);
            }}
          />
          {rightIcons}
        </Wrapper>
        <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
      </div>
      {help && <p class="mt-2 text-sm text-gray-500">{help}</p>}
    </div>
  );
}
