/*
 This file is part of GNU Taler
 (C) 2021-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/>
 */

/**
 *
 * @author Sebastian Javier Marchano (sebasjm)
 */

import {
  AbsoluteTime,
  AmountString,
  Amounts,
  Duration,
  HttpStatusCode,
  OrderVersion,
  TalerMerchantApi,
  TalerProtocolDuration,
  durationAdd,
} from "@gnu-taler/taler-util";
import {
  ButtonBetterBulma,
  LocalNotificationBannerBulma,
  useLocalNotificationBetter,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format, isFuture } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import {
  FormErrors,
  FormProvider,
  TalerForm,
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputDate } from "../../../../components/form/InputDate.js";
import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputGroup } from "../../../../components/form/InputGroup.js";
import { InputLocation } from "../../../../components/form/InputLocation.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
import { InputToggle } from "../../../../components/form/InputToggle.js";
import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js";
import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js";
import { ProductList } from "../../../../components/product/ProductList.js";
import { useSessionContext } from "../../../../context/session.js";
import { WithId } from "../../../../declaration.js";
import { UIElement, usePreference } from "../../../../hooks/preference.js";
import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js";

const TALER_SCREEN_ID = 42;

export interface Props {
  onCreated: (id: string) => void;
  onBack?: () => void;
  instanceConfig: InstanceConfig;
  instanceInventory: (TalerMerchantApi.ProductDetail & WithId)[];
}
interface InstanceConfig {
  use_stefan: boolean;
  default_pay_delay: TalerProtocolDuration;
  default_refund_delay: TalerProtocolDuration;
  default_wire_transfer_delay: TalerProtocolDuration;
}

function with_defaults(
  config: InstanceConfig,
  _currency: string,
): Partial<Entity> {
  const defaultPayDelay = Duration.fromTalerProtocolDuration(
    config.default_pay_delay,
  );
  const defaultWireDelay = Duration.fromTalerProtocolDuration(
    config.default_wire_transfer_delay,
  );
  const defaultRefundDelay = Duration.fromTalerProtocolDuration(
    config.default_refund_delay,
  );

  return {
    inventoryProducts: {},
    products: [],
    pricing: {},
    payments: {
      max_fee: undefined,
      createToken: true,
      pay_delay: defaultPayDelay,
      refund_delay: defaultRefundDelay,
      wire_transfer_delay: defaultWireDelay,
    },
    shipping: {},
    extra: {},
  };
}

interface ProductAndQuantity {
  product: TalerMerchantApi.ProductDetail & WithId;
  quantity: number;
}
export interface ProductMap {
  [id: string]: ProductAndQuantity;
}

interface Pricing extends TalerForm {
  products_price: string;
  order_price: string;
  summary: string;
}
interface Shipping extends TalerForm {
  delivery_date?: Date;
  delivery_location?: TalerMerchantApi.Location;
  fulfillment_url?: string;
  fulfillment_message?: string;
}
interface Payments extends TalerForm {
  pay_delay: Duration;
  refund_delay: Duration;
  wire_transfer_delay: Duration;
  auto_refund_delay?: Duration;
  max_fee?: string;
  createToken: boolean;
  minimum_age?: number;
}
interface Entity extends TalerForm {
  inventoryProducts: ProductMap;
  products: TalerMerchantApi.Product[];
  pricing: Partial<Pricing>;
  payments: Partial<Payments>;
  shipping: Partial<Shipping>;
  extra: Record<string, string>;
}

function isInvalidUrl(url: string) {
  try {
    const asd = new URL("./", url);
    return false;
  } catch (e) {
    return true;
  }
}

export function CreatePage({
  onCreated,
  onBack,
  instanceConfig,
  instanceInventory,
}: Props): VNode {
  const { config, lib, state: session } = useSessionContext();
  const instance_default = with_defaults(instanceConfig, config.currency);
  const [value, valueHandler] = useState(instance_default);
  const zero = Amounts.zeroOfCurrency(config.currency);
  const inventoryList = Object.values(value.inventoryProducts || {});
  const productList = Object.values(value.products || {});

  const { i18n } = useTranslationContext();

  const parsedPrice = !value.pricing?.order_price
    ? undefined
    : Amounts.parse(value.pricing.order_price);

  const errors = undefinedIfEmpty<FormErrors<Entity>>({
    pricing: undefinedIfEmpty<FormErrors<Pricing>>({
      summary: !value.pricing?.summary ? i18n.str`Required` : undefined,
      order_price: !value.pricing?.order_price
        ? i18n.str`Required`
        : !parsedPrice
          ? i18n.str`Invalid`
          : Amounts.isZero(parsedPrice)
            ? i18n.str`Must be greater than 0`
            : undefined,
    }),
    payments: undefinedIfEmpty({
      wire_transfer_delay: !value.payments?.wire_transfer_delay
        ? i18n.str`Required`
        : undefined,
      // refund_delay: !value.payments?.refund_delay ? i18n.str`Required` : undefined,
      pay_delay: !value.payments?.pay_delay ? i18n.str`Required` : undefined,
      auto_refund_delay: !value.payments?.auto_refund_delay
        ? undefined
        : !value.payments?.refund_delay
          ? i18n.str`Must have a refund deadline`
          : Duration.cmp(
                value.payments.refund_delay,
                value.payments.auto_refund_delay,
              ) == -1
            ? i18n.str`Auto refund can't be after refund deadline`
            : undefined,
    }),
    shipping: undefinedIfEmpty({
      delivery_date: !value.shipping?.delivery_date
        ? undefined
        : !isFuture(value.shipping.delivery_date)
          ? i18n.str`Must be in the future`
          : undefined,
      fulfillment_message:
        !!value.shipping?.fulfillment_message &&
        !!value.shipping?.fulfillment_url
          ? i18n.str`Either fulfillment url or fulfillment message must be specified.`
          : undefined,
      fulfillment_url:
        !!value.shipping?.fulfillment_message &&
        !!value.shipping?.fulfillment_url
          ? i18n.str`Either fulfillment url or fulfillment message must be specified.`
          : !!value.shipping?.fulfillment_url &&
              isInvalidUrl(value.shipping.fulfillment_url)
            ? i18n.str`is not a valid URL`
            : undefined,
    }),
  });

  const order = value;
  const price = order.pricing?.order_price as AmountString | undefined;
  const summary = order.pricing?.summary;

  const payDelay = !value.payments?.pay_delay
    ? Duration.getZero()
    : value.payments.pay_delay;

  const refundDelay = !value.payments?.refund_delay
    ? Duration.getZero()
    : Duration.add(
        payDelay,
        !value.payments?.refund_delay
          ? Duration.getZero()
          : value.payments.refund_delay,
      );

  const autoRefundDelay = !value.payments?.auto_refund_delay
    ? Duration.getZero()
    : Duration.add(
        payDelay,
        !value.payments?.auto_refund_delay
          ? Duration.getZero()
          : value.payments.auto_refund_delay,
      );

  const wireDelay = !value.payments?.wire_transfer_delay
    ? Duration.getZero()
    : Duration.add(
        refundDelay,
        !value.payments?.wire_transfer_delay
          ? Duration.getZero()
          : value.payments.wire_transfer_delay,
      );

  const pay_deadline = !value.payments?.pay_delay
    ? undefined
    : AbsoluteTime.toProtocolTimestamp(
        AbsoluteTime.addDuration(AbsoluteTime.now(), payDelay),
      );

  const refund_deadline = !value.payments?.refund_delay
    ? undefined
    : AbsoluteTime.toProtocolTimestamp(
        AbsoluteTime.addDuration(AbsoluteTime.now(), refundDelay),
      );

  const wire_transfer_deadline =
    !value.payments || !value.payments.wire_transfer_delay
      ? undefined
      : AbsoluteTime.toProtocolTimestamp(
          AbsoluteTime.addDuration(AbsoluteTime.now(), wireDelay),
        );

  const request: undefined | TalerMerchantApi.PostOrderRequest =
    !value.payments || !value.shipping || !price || !summary
      ? undefined
      : {
          order: {
            version: OrderVersion.V0,
            amount: price,
            // version: OrderVersion.V1,
            // choices: [{
            //   amount: price,
            //   max_fee: value.payments.max_fee as AmountString,
            // }],
            summary: summary,
            products: productList,
            extra: undefinedIfEmpty(value.extra),
            pay_deadline,
            refund_deadline,
            wire_transfer_deadline,
            auto_refund: value.payments.auto_refund_delay
              ? Duration.toTalerProtocolDuration(
                  value.payments.auto_refund_delay,
                )
              : undefined,
            delivery_date: value.shipping.delivery_date
              ? { t_s: value.shipping.delivery_date.getTime() / 1000 }
              : undefined,
            delivery_location: value.shipping.delivery_location,
            fulfillment_url: value.shipping.fulfillment_url,
            fulfillment_message: value.shipping.fulfillment_message,
            minimum_age: value.payments.minimum_age,
          },
          inventory_products: inventoryList.map((p) => ({
            product_id: p.product.id,
            quantity: p.quantity,
          })),
          create_token: value.payments.createToken,
        };
  const [notification, safeFunctionHandler] = useLocalNotificationBetter();

  const create = safeFunctionHandler(
    lib.instance.createOrder.bind(lib.instance),
    !session.token || !request ? undefined : [session.token, request],
  );
  create.onSuccess = (resp) => {
    onCreated(resp.order_id);
  };
  create.onFail = (fail) => {
    switch (fail.case) {
      case HttpStatusCode.Unauthorized:
        return i18n.str`Unauthorized`;
      case HttpStatusCode.NotFound:
        return i18n.str`Not found`;
      case HttpStatusCode.Conflict:
        return i18n.str`Conflict`;
      case HttpStatusCode.Gone:
        return i18n.str`Product with ID "${fail.body.product_id}" is out of stock.`;
      case HttpStatusCode.UnavailableForLegalReasons:
        return i18n.str`No exchange would accept a payment because of KYC requirements.`;
    }
  };
  const addProductToTheInventoryList = (
    product: TalerMerchantApi.ProductDetail & WithId,
    quantity: number,
  ) => {
    valueHandler((v) => {
      const inventoryProducts = { ...v.inventoryProducts };
      inventoryProducts[product.id] = { product, quantity };
      return { ...v, inventoryProducts };
    });
  };

  const removeProductFromTheInventoryList = (id: string) => {
    valueHandler((v) => {
      const inventoryProducts = { ...v.inventoryProducts };
      delete inventoryProducts[id];
      return { ...v, inventoryProducts };
    });
  };

  const addNewProduct = async (product: TalerMerchantApi.Product) => {
    return valueHandler((v) => {
      const products = v.products ? [...v.products, product] : [];
      return { ...v, products };
    });
  };

  const removeFromNewProduct = (index: number) => {
    valueHandler((v) => {
      const products = v.products ? [...v.products] : [];
      products.splice(index, 1);
      return { ...v, products };
    });
  };

  const [editingProduct, setEditingProduct] = useState<
    TalerMerchantApi.Product | undefined
  >(undefined);

  const totalPriceInventory = inventoryList.reduce((prev, cur) => {
    const p = Amounts.parseOrThrow(cur.product.price);
    return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount;
  }, zero);

  const totalPriceProducts = productList.reduce((prev, cur) => {
    if (!cur.price) return zero;
    const p = Amounts.parseOrThrow(cur.price);
    return Amounts.add(prev, Amounts.mult(p, cur.quantity ?? 0).amount).amount;
  }, zero);

  const hasProducts = inventoryList.length > 0 || productList.length > 0;
  const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts);

  const totalAsString = Amounts.stringify(totalPrice.amount);
  const allProducts = productList.concat(inventoryList.map(asProduct));

  const [newField, setNewField] = useState("");

  useEffect(() => {
    valueHandler((v) => {
      return {
        ...v,
        pricing: {
          ...v.pricing,
          products_price: hasProducts ? totalAsString : undefined,
          order_price: hasProducts ? totalAsString : undefined,
        },
      };
    });
  }, [hasProducts, totalAsString]);

  const discountOrRise = rate(
    parsedPrice ?? Amounts.zeroOfCurrency(config.currency),
    totalPrice.amount,
  );
  const discountOrRiseRounded = Math.round((discountOrRise - 1) * 100);

  const minAgeByProducts = inventoryList.reduce(
    (cur, prev) =>
      !prev.product.minimum_age || cur > prev.product.minimum_age
        ? cur
        : prev.product.minimum_age,
    0,
  );

  // if there is no default pay deadline
  const noDefault_payDeadline =
    !instance_default.payments || !instance_default.payments.pay_delay;
  // and there is no default wire deadline
  const noDefault_wireDeadline =
    !instance_default.payments ||
    !instance_default.payments.wire_transfer_delay;
  // user required to set the taler options
  const requiresSomeTalerOptions =
    noDefault_payDeadline || noDefault_wireDeadline;

  return (
    <div>
      <LocalNotificationBannerBulma notification={notification} />
      <section class="section is-main-section">
        <div class="columns">
          <div class="column" />
          <div class="column is-four-fifths">
            {/* // FIXME: translating plural singular */}
            <InputGroup
              name="inventory_products"
              label={i18n.str`Manage products in order`}
              alternative={
                allProducts.length > 0 && (
                  <p>
                    <i18n.Translate>
                      {allProducts.length} products with a total price of{" "}
                      {totalAsString}.
                    </i18n.Translate>
                  </p>
                )
              }
              tooltip={i18n.str`Manage list of products in the order.`}
            >
              <InventoryProductForm
                currentProducts={value.inventoryProducts || {}}
                onAddProduct={addProductToTheInventoryList}
                inventory={instanceInventory}
              />

              <FragmentPersonaFlag
                point={UIElement.option_advanceOrderCreation}
              >
                <NonInventoryProductFrom
                  productToEdit={editingProduct}
                  onAddProduct={(p) => {
                    setEditingProduct(undefined);
                    return addNewProduct(p);
                  }}
                />
              </FragmentPersonaFlag>

              {allProducts.length > 0 && (
                <ProductList
                  list={allProducts}
                  actions={[
                    {
                      name: i18n.str`Remove`,
                      tooltip: i18n.str`Remove this product from the order.`,
                      handler: (e, index) => {
                        if (e.product_id) {
                          removeProductFromTheInventoryList(e.product_id);
                        } else {
                          removeFromNewProduct(index);
                          setEditingProduct(e);
                        }
                      },
                    },
                  ]}
                />
              )}
            </InputGroup>

            <FormProvider<Entity>
              errors={errors}
              object={value}
              valueHandler={valueHandler as any}
            >
              {hasProducts ? (
                <Fragment>
                  <InputCurrency
                    name="pricing.products_price"
                    label={i18n.str`Products price sum`}
                    readonly
                    tooltip={i18n.str`Total product price added up`}
                  />
                  <InputCurrency
                    name="pricing.order_price"
                    label={i18n.str`Order price`}
                    addonAfter={
                      discountOrRiseRounded > 0 &&
                      (discountOrRiseRounded < 1
                        ? `discount of ${discountOrRiseRounded}%`
                        : `rise of ${discountOrRiseRounded}%`)
                    }
                    tooltip={i18n.str`Amount to be paid by the customer`}
                  />
                </Fragment>
              ) : (
                <InputCurrency
                  name="pricing.order_price"
                  label={i18n.str`Order price`}
                  tooltip={i18n.str`Final order price`}
                />
              )}

              <Input
                name="pricing.summary"
                inputType="multiline"
                label={i18n.str`Summary`}
                tooltip={i18n.str`Title of the order to be shown to the customer`}
              />

              <FragmentPersonaFlag
                point={UIElement.option_advanceOrderCreation}
                showAnywayIf={errors?.shipping !== undefined}
              >
                <InputGroup
                  name="shipping"
                  label={i18n.str`Shipping and fulfillment`}
                  initialActive
                >
                  <InputDate
                    name="shipping.delivery_date"
                    label={i18n.str`Delivery date`}
                    tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
                  />
                  <InputGroup
                    name="shipping.delivery_location"
                    label={i18n.str`Delivery location`}
                    tooltip={i18n.str`Address where the products will be delivered`}
                  >
                    <InputLocation name="shipping.delivery_location" />
                  </InputGroup>
                  <Input
                    name="shipping.fulfillment_url"
                    label={i18n.str`Fulfillment URL`}
                    tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
                  />
                  <Input
                    name="shipping.fulfillment_message"
                    label={i18n.str`Fulfillment message`}
                    tooltip={i18n.str`Message shown to the customer after paying for the order.`}
                  />
                </InputGroup>
              </FragmentPersonaFlag>

              <FragmentPersonaFlag
                point={UIElement.option_advanceOrderCreation}
                showAnywayIf={
                  requiresSomeTalerOptions || errors?.payments !== undefined
                }
              >
                <InputGroup
                  name="payments"
                  label={i18n.str`Taler payment options`}
                  tooltip={i18n.str`Override default Taler payment settings for this order`}
                >
                  <FragmentPersonaFlag
                    point={UIElement.option_advanceOrderCreation}
                    showAnywayIf={
                      noDefault_payDeadline ||
                      errors?.payments?.pay_delay !== undefined
                    }
                  >
                    <InputDuration
                      name="payments.pay_delay"
                      label={i18n.str`Payment time`}
                      help={<DeadlineHelp duration={payDelay} />}
                      tooltip={i18n.str`Time for the customer to pay before the offer expires. Inventory products will be reserved until this deadline. The time starts after the order is created.`}
                      side={
                        <span>
                          <button
                            type="button"
                            class="button"
                            onClick={() => {
                              const c = {
                                ...value,
                                payments: {
                                  ...(value.payments ?? {}),
                                  pay_delay:
                                    instance_default.payments?.pay_delay,
                                },
                              };
                              valueHandler(c);
                            }}
                          >
                            <i18n.Translate>Default</i18n.Translate>
                          </button>
                        </span>
                      }
                    />
                  </FragmentPersonaFlag>
                  <FragmentPersonaFlag
                    point={UIElement.option_advanceOrderCreation}
                    showAnywayIf={errors?.payments?.refund_delay !== undefined}
                  >
                    <InputDuration
                      name="payments.refund_delay"
                      label={i18n.str`Refund time`}
                      help={<DeadlineHelp duration={refundDelay} />}
                      tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`}
                      side={
                        <span>
                          <button
                            type="button"
                            class="button"
                            onClick={() => {
                              valueHandler({
                                ...value,
                                payments: {
                                  ...(value.payments ?? {}),
                                  refund_delay:
                                    instance_default.payments?.refund_delay,
                                },
                              });
                            }}
                          >
                            <i18n.Translate>Default</i18n.Translate>
                          </button>
                        </span>
                      }
                    />
                  </FragmentPersonaFlag>

                  <FragmentPersonaFlag
                    point={UIElement.option_advanceOrderCreation}
                    showAnywayIf={
                      noDefault_wireDeadline ||
                      errors?.payments?.wire_transfer_delay !== undefined
                    }
                  >
                    <InputDuration
                      name="payments.wire_transfer_delay"
                      label={i18n.str`Wire transfer time`}
                      help={<DeadlineHelp duration={wireDelay} />}
                      tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`}
                      side={
                        <span>
                          <button
                            type="button"
                            class="button"
                            onClick={() => {
                              valueHandler({
                                ...value,
                                payments: {
                                  ...(value.payments ?? {}),
                                  wire_transfer_delay:
                                    instance_default.payments
                                      ?.wire_transfer_delay,
                                },
                              });
                            }}
                          >
                            <i18n.Translate>Default</i18n.Translate>
                          </button>
                        </span>
                      }
                    />
                  </FragmentPersonaFlag>
                  <FragmentPersonaFlag
                    point={UIElement.option_advanceOrderCreation}
                    showAnywayIf={
                      errors?.payments?.auto_refund_delay !== undefined
                    }
                  >
                    <InputDuration
                      name="payments.auto_refund_delay"
                      label={i18n.str`Auto-refund time`}
                      help={<DeadlineHelp duration={autoRefundDelay} />}
                      tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
                      withForever
                    />
                  </FragmentPersonaFlag>
                  <FragmentPersonaFlag
                    point={UIElement.option_advanceOrderCreation}
                    showAnywayIf={errors?.payments?.max_fee !== undefined}
                  >
                    <InputCurrency
                      name="payments.max_fee"
                      label={i18n.str`Maximum fee`}
                      tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
                    />
                  </FragmentPersonaFlag>
                  <FragmentPersonaFlag
                    point={UIElement.option_advanceOrderCreation}
                    showAnywayIf={errors?.payments?.createToken !== undefined}
                  >
                    <InputToggle
                      name="payments.createToken"
                      label={i18n.str`Create token`}
                      tooltip={i18n.str`If the order ID is easy to guess, the token will prevent other users from claiming the order.`}
                    />
                  </FragmentPersonaFlag>
                  <FragmentPersonaFlag
                    point={UIElement.option_ageRestriction}
                    showAnywayIf={errors?.payments?.minimum_age !== undefined}
                  >
                    <InputNumber
                      name="payments.minimum_age"
                      label={i18n.str`Minimum age required`}
                      tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
                      help={
                        minAgeByProducts > 0
                          ? i18n.str`Minimum age defined by the products is ${minAgeByProducts}`
                          : i18n.str`No product with age restriction in this order`
                      }
                    />
                  </FragmentPersonaFlag>
                </InputGroup>
              </FragmentPersonaFlag>

              <FragmentPersonaFlag
                point={UIElement.option_advanceOrderCreation}
                showAnywayIf={
                  requiresSomeTalerOptions || errors?.extra !== undefined
                }
              >
                <InputGroup
                  name="extra"
                  label={i18n.str`Additional information`}
                  tooltip={i18n.str`Custom information to be included in the contract for this order.`}
                >
                  {Object.keys(value.extra ?? {}).map((key, idx) => {
                    return (
                      <Input
                        name={`extra.${key}`}
                        key={String(idx)}
                        inputType="multiline"
                        label={key}
                        tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
                        side={
                          <button
                            type="button"
                            class="button"
                            onClick={(e) => {
                              if (
                                value.extra &&
                                value.extra[key] !== undefined
                              ) {
                                delete value.extra[key];
                              }
                              valueHandler({
                                ...value,
                              });
                              e.preventDefault();
                            }}
                          >
                            <i18n.Translate>remove</i18n.Translate>
                          </button>
                        }
                      />
                    );
                  })}
                  <div class="field is-horizontal">
                    <div class="field-label is-normal">
                      <label class="label">
                        <i18n.Translate>Custom field name</i18n.Translate>
                        <span
                          class="icon has-tooltip-right"
                          data-tooltip={"new extra field"}
                        >
                          <i class="mdi mdi-information" />
                        </span>
                      </label>
                    </div>
                    <div class="field-body is-flex-grow-3">
                      <div class="field">
                        <p class="control">
                          <input
                            class="input "
                            value={newField}
                            onChange={(e) => setNewField(e.currentTarget.value)}
                          />
                        </p>
                      </div>
                    </div>
                    <button
                      type="button"
                      class="button"
                      onClick={(e) => {
                        setNewField("");
                        valueHandler({
                          ...value,
                          extra: {
                            ...(value.extra ?? {}),
                            [newField]: "",
                          },
                        });
                        e.preventDefault();
                      }}
                    >
                      <i18n.Translate>Add</i18n.Translate>
                    </button>
                  </div>
                </InputGroup>
              </FragmentPersonaFlag>
            </FormProvider>

            <div class="buttons is-right mt-5">
              {onBack && (
                <button class="button" onClick={onBack}>
                  <i18n.Translate>Cancel</i18n.Translate>
                </button>
              )}
              <ButtonBetterBulma
                class="button is-success"
                type="submit"
                onClick={create}
              >
                <i18n.Translate>Confirm</i18n.Translate>
              </ButtonBetterBulma>
            </div>
          </div>
          <div class="column" />
        </div>
      </section>
    </div>
  );
}

function asProduct(p: ProductAndQuantity): TalerMerchantApi.Product {
  return {
    product_id: p.product.id,
    product_name: p.product.product_name,
    image: p.product.image,
    price: p.product.price,
    unit: p.product.unit,
    quantity: p.quantity,
    description: p.product.description,
    taxes: p.product.taxes,
  };
}

function DeadlineHelp({ duration }: { duration?: Duration }): VNode {
  const { i18n } = useTranslationContext();
  const [now, setNow] = useState(AbsoluteTime.now());
  useEffect(() => {
    const iid = setInterval(() => {
      setNow(AbsoluteTime.now());
    }, 60 * 1000);
    return () => {
      clearInterval(iid);
    };
  });
  if (!duration || !duration.d_ms)
    return <i18n.Translate>Disabled</i18n.Translate>;
  const when = AbsoluteTime.addDuration(now, duration);
  if (when.t_ms === "never")
    return <i18n.Translate>No deadline</i18n.Translate>;
  return (
    <i18n.Translate>
      Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}
    </i18n.Translate>
  );
}

function getAll(s: object): string[] {
  return Object.entries(s).flatMap(([key, value]) => {
    if (typeof value === "object")
      return getAll(value).map((v) => `${key}.${v}`);
    if (!value) return [];
    return key;
  });
}

function describeMissingFields(errors: object | undefined): string[] {
  if (!errors) return [];
  return getAll(errors);
}
