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

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU Affero 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 Affero Public License for more details.

 You should have received a copy of the GNU Affero Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

import {
  buildCodecForObject,
  buildCodecForUnion,
  Codec,
  codecForAny,
  codecForBoolean,
  codecForConstString,
  codecForLazy,
  codecForList,
  codecForNumber,
  codecForString,
  codecForStringURL,
  codecForTimestamp,
  codecOptional,
  codecOptionalDefault,
  Integer,
  TalerProtocolTimestamp,
  TranslatedString,
} from "@gnu-taler/taler-util";

export type FormDesign = DoubleColumnFormDesign | SingleColumnFormDesign;

/**
 * Form with multiple sections.
 */
export type DoubleColumnFormDesign = {
  type: "double-column";
  title?: string;
  sections: DoubleColumnFormSection[];
};

/**
 * Single section form.
 */
export type SingleColumnFormDesign = {
  type: "single-column";
  fields: UIFormElementConfig[];
};

export type DoubleColumnFormSection = {
  title: string;
  description?: string;
  fields: UIFormElementConfig[];

  /**
   * Function to decide whether the form element
   * should be hidden.
   *
   * Hidden form elements and their children are not validated.
   *
   * @param root the root of the form (values after conversion)
   *
   * @returns true if the field should be hidden.
   */
  hide?: (root: Record<string, any>) => boolean | undefined;
};

export type UIFormElementConfig =
  | UIFormElementGroup
  | UIFormElementCaption
  | UIFormElementDownloadLink
  | UIFormElementExternalLink
  | UIFormElementHtmlIframe
  | UIFormFieldAbsoluteTime
  | UIFormFieldIsoDate
  | UIFormFieldAmount
  | UIFormFieldArray
  | UIFormFieldChoiceHorizontal
  | UIFormFieldChoiceStacked
  | UIFormFieldFile
  | UIFormFieldInteger
  | UIFormFieldSecret
  | UIFormFieldSelectMultiple
  | UIFormFieldDuration
  | UIFormFieldDurationText
  | UIFormFieldSelectOne
  | UIFormFieldText
  | UIFormFieldTextArea
  | UIFormFieldToggle
  | UIFormFieldDrilldown
  | UIFormVoid;

type UIFormVoid = {
  type: "void";
} & UIFormFieldBaseConfig;

type UIFormFieldAbsoluteTime = {
  type: "absoluteTimeText";
  max?: TalerProtocolTimestamp;
  min?: TalerProtocolTimestamp;
  pattern: string;
} & UIFormFieldBaseConfig;

type UIFormFieldIsoDate = {
  type: "isoDateText";
  /**
   * wrong! value is not part of design
   * @deprecated
   */
  defaultValue?: string;
  max?: TalerProtocolTimestamp;
  min?: TalerProtocolTimestamp;
  pattern: string;
  defaultCalendarValue?: string;
} & UIFormFieldBaseConfig;

type UIFormFieldAmount = {
  type: "amount";
  max?: Integer;
  min?: Integer;
  currency: string;
} & UIFormFieldBaseConfig;

type UIFormFieldArray = {
  type: "array";

  /**
   * id of the field shown when the array is collapsed.
   */
  labelFieldId: string;

  /**
   * Validator for the field.
   *
   * @returns an error message if the value is not valid,
   *   undefined if there is no error.
   */
  validator?: (array: Array<object>, form: any) => TranslatedString | undefined;

  fields: UIFormElementConfig[];
} & UIFormFieldBaseConfig;

type UIFormElementCaption = { type: "caption" } & UIFieldElementDescription;

type UIFormElementDownloadLink = {
  type: "download-link";
  url: string;
  media?: string;
} & UIFormFieldBaseConfig;

type UIFormElementExternalLink = {
  type: "external-link";
  url: string;
  media?: string;
} & UIFormFieldBaseConfig;

type UIFormElementHtmlIframe = {
  type: "htmlIframe";
  url: string;
} & UIFieldElementDescription;

type UIFormElementGroup = {
  type: "group";
  fields: UIFormElementConfig[];
} & UIFieldElementDescription;

type UIFormFieldChoiceHorizontal = {
  type: "choiceHorizontal";
  choices: Array<SelectUiChoice>;
} & UIFormFieldBaseConfig;

type UIFormFieldDrilldown = {
  type: "drilldown";
  choices: any;
} & UIFormFieldBaseConfig;

type UIFormFieldChoiceStacked = {
  type: "choiceStacked";
  choices: Array<SelectUiChoice>;
} & UIFormFieldBaseConfig;

type UIFormFieldFile = {
  type: "file";
  maxBytes?: Integer;
  minBytes?: Integer;
  // comma-separated list of one or more file types
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers
  accept?: string;
} & UIFormFieldBaseConfig;

type UIFormFieldInteger = {
  type: "integer";
  max?: Integer;
  min?: Integer;
} & UIFormFieldBaseConfig;

type UIFormFieldSecret = {
  type: "secret";
} & UIFormFieldBaseConfig;

export interface SelectUiChoice {
  label: string;
  description?: string;
  value: string | boolean;
}

type UIFormFieldSelectMultiple = {
  type: "selectMultiple";
  max?: Integer;
  min?: Integer;
  unique?: boolean;
  choices: Array<SelectUiChoice>;
} & UIFormFieldBaseConfig;

type UIFormFieldDuration = {
  type: "duration";
} & UIFormFieldBaseConfig;

type UIFormFieldDurationText = {
  type: "durationText";
} & UIFormFieldBaseConfig;

type UIFormFieldSelectOne = {
  type: "selectOne";
  choices: Array<SelectUiChoice>;
  preferredChoiceVals?: string[];
} & UIFormFieldBaseConfig;

type UIFormFieldText = { type: "text" } & UIFormFieldBaseConfig;

type UIFormFieldTextArea = { type: "textArea" } & UIFormFieldBaseConfig;

type UIFormFieldToggle = {
  type: "toggle";
  threeState?: boolean;

  /**
   * When the toggle is true use the field is going to have this value.
   */
  trueValue?: any;

  /**
   * When the toggle is false use the field is going to have this value.
   */
  falseValue?: any;

  /**
   * Only true value, when the toggle is false the field is going to be undefined.
   */
  onlyTrueValue?: boolean;
} & UIFormFieldBaseConfig;

export type ComputableFieldConfig = {
  /* if the field should be initially hidden */
  hidden?: boolean;

  /**
   * show a mark as required
   */
  required?: boolean;

  /**
   * readonly and dim
   */
  disabled?: boolean;

  /*
  update field config based on form state
   */
  updateProps?: (
    value: any,
    root?: any,
  ) => Partial<ComputableFieldConfig> | undefined;
};

export type UIFieldElementDescription = ComputableFieldConfig & {
  /* label if the field, visible for the user */
  label: string;

  /* long text to be shown on user demand */
  tooltip?: string;

  /* short text to be shown close to the field, usually below and dimmer*/
  help?: string;

  /* ui element to show before */
  addonBeforeId?: string;

  /* ui element to show after */
  addonAfterId?: string;

  /**
   * Return if the field should be hidden.
   * Receives the value after conversion and the root of the form.
   */
  hide?: (value: any, root?: any) => boolean | undefined;
};

export type UIFormFieldBaseConfig = UIFieldElementDescription & {
  /** Example to be shown inside the field */
  placeholder?: string;

  /**
   * Conversion id to convert the string into the value type.
   * The id should be known to the ui implementation.
   */
  converterId?: string;

  /**
   * Return an error message if the value is not valid,
   * undefined otherwise.
   */
  validator?: (text: string, form: any) => TranslatedString | undefined;

  /** Property id of the form */
  id: UIHandlerId;
};

export interface FileFieldData {
  /**
   * File contents storage type.
   *
   * We only support base64, but might support more in the future.
   */
  ENCODING: "base64";

  /**
   * Contents of the file, as a "data:base64,"
   * URI without the MIME type in the filename.
   */
  CONTENTS: string;

  /**
   * Filename of the uploaded file.
   */
  FILENAME?: string;

  /**
   * Mime-type of the uploaded file.
   */
  MIME_TYPE?: string;
}

export type UIHandlerId = string;

// FIXME: validate well formed ui field id
const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>;

const codecForUIFormFieldBaseDescriptionTemplate = <
  T extends UIFieldElementDescription,
>() =>
  buildCodecForObject<T>()
    .property("addonAfterId", codecOptional(codecForString()))
    .property("addonBeforeId", codecOptional(codecForString()))
    .property("hidden", codecOptional(codecForBoolean()))
    .property("help", codecOptional(codecForString()))
    .property("label", codecForString())
    .property("tooltip", codecOptional(codecForString()));

const codecForUIFormFieldBaseConfigTemplate = <
  T extends UIFormFieldBaseConfig,
>() =>
  codecForUIFormFieldBaseDescriptionTemplate<T>()
    .property("id", codecForUiFieldId())
    .property("converterId", codecOptional(codecForString()))
    .property("disabled", codecOptional(codecForBoolean()))
    .property("required", codecOptional(codecForBoolean()))
    .property("placeholder", codecOptional(codecForString()));

const codecForUiFormFieldAbsoluteTime = (): Codec<UIFormFieldAbsoluteTime> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldAbsoluteTime>()
    .property("type", codecForConstString("absoluteTimeText"))
    .property("pattern", codecForString())
    .property("max", codecOptional(codecForTimestamp))
    .property("min", codecOptional(codecForTimestamp))
    .build("UIFormFieldAbsoluteTime");

const codecForUiFormFieldIsoDate = (): Codec<UIFormFieldIsoDate> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldIsoDate>()
    .property("type", codecForConstString("isoDateText"))
    .property("pattern", codecForString())
    .property("max", codecOptional(codecForTimestamp))
    .property("min", codecOptional(codecForTimestamp))
    .build("UIFormFieldIsoTime");

const codecForUiFormFieldAmount = (): Codec<UIFormFieldAmount> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldAmount>()
    .property("type", codecForConstString("amount"))
    .property("currency", codecForString())
    .property("max", codecOptional(codecForNumber()))
    .property("min", codecOptional(codecForNumber()))
    .build("UIFormFieldAmount");

const codecForUiFormFieldArray = (): Codec<UIFormFieldArray> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldArray>()
    .property("type", codecForConstString("array"))
    .property("labelFieldId", codecForUiFieldId())
    .property("tooltip", codecOptional(codecForString()))
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    .property("fields", codecForList(codecForUiFormField()))
    .build("UIFormFieldArray");

const codecForUiFormFieldCaption = (): Codec<UIFormElementCaption> =>
  codecForUIFormFieldBaseDescriptionTemplate<UIFormElementCaption>()
    .property("type", codecForConstString("caption"))
    .build("UIFormFieldCaption");

const codecForUIFormElementLink = (): Codec<UIFormElementDownloadLink> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormElementDownloadLink>()
    .property("type", codecForConstString("download-link"))
    .property("url", codecForString())
    .property("media", codecOptional(codecForString()))
    .build("UIFormElementLink");

const codecForUIFormElementExternalLink =
  (): Codec<UIFormElementExternalLink> =>
    codecForUIFormFieldBaseConfigTemplate<UIFormElementExternalLink>()
      .property("type", codecForConstString("external-link"))
      .property("url", codecForString())
      .property("media", codecOptional(codecForString()))
      .build("UIFormElementExternalLink");

const codecForUiFormFieldHtmlIFrame = (): Codec<UIFormElementHtmlIframe> =>
  codecForUIFormFieldBaseDescriptionTemplate<UIFormElementHtmlIframe>()
    .property("type", codecForConstString("htmlIframe"))
    .property("url", codecForStringURL())
    .build("codecForUiFormFieldHtmlIFrame");

const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> =>
  buildCodecForObject<SelectUiChoice>()
    .property("description", codecOptional(codecForString()))
    .property("label", codecForString())
    .property("value", codecForString())
    .build("SelectUiChoice");

const codecForUiFormFieldChoiceHorizontal =
  (): Codec<UIFormFieldChoiceHorizontal> =>
    codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiceHorizontal>()
      .property("type", codecForConstString("choiceHorizontal"))
      .property("choices", codecForList(codecForUiFormSelectUiChoice()))
      .build("UIFormFieldChoiseHorizontal");

const codecForUiFormFieldChoiceStacked = (): Codec<UIFormFieldChoiceStacked> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiceStacked>()
    .property("type", codecForConstString("choiceStacked"))
    .property("choices", codecForList(codecForUiFormSelectUiChoice()))
    .build("UIFormFieldChoiseStacked");

const codecForUiFormFieldFile = (): Codec<UIFormFieldFile> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldFile>()
    .property("type", codecForConstString("file"))
    .property("accept", codecOptional(codecForString()))
    .property("maxBytes", codecOptional(codecForNumber()))
    .property("minBytes", codecOptional(codecForNumber()))
    .build("UIFormFieldFile");

const codecForUiFormFieldGroup = (): Codec<UIFormElementGroup> =>
  codecForUIFormFieldBaseDescriptionTemplate<UIFormElementGroup>()
    .property("type", codecForConstString("group"))
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    .property("fields", codecForList(codecForUiFormField()))
    .build("UiFormFieldGroup");

const codecForUiFormVoid = (): Codec<UIFormVoid> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormVoid>()
    .property("type", codecForConstString("void"))
    .build("UIFormVoid");

const codecForUiFormFieldInteger = (): Codec<UIFormFieldInteger> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldInteger>()
    .property("type", codecForConstString("integer"))
    // .property("properties", codecForUIFormFieldBaseConfig())
    .property("max", codecOptional(codecForNumber()))
    .property("min", codecOptional(codecForNumber()))
    .build("UIFormFieldInteger");

const codecForUiFormFieldSecret = (): Codec<UIFormFieldSecret> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldSecret>()
    .property("type", codecForConstString("secret"))
    .build("UIFormFieldSecret");

const codecForUiFormFieldDuration = (): Codec<UIFormFieldDuration> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldDuration>()
    .property("type", codecForConstString("duration"))
    .build("UiFormFieldDuration");

const codecForUiFormFieldDurationText = (): Codec<UIFormFieldDurationText> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldDurationText>()
    .property("type", codecForConstString("durationText"))
    .build("UiFormFieldDuration");

const codecForUiFormFieldSelectMultiple =
  (): Codec<UIFormFieldSelectMultiple> =>
    codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectMultiple>()
      .property("type", codecForConstString("selectMultiple"))
      .property("max", codecOptional(codecForNumber()))
      .property("min", codecOptional(codecForNumber()))
      .property("unique", codecOptional(codecForBoolean()))
      .property("choices", codecForList(codecForUiFormSelectUiChoice()))
      .build("UiFormFieldSelectMultiple");

const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldSelectOne> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectOne>()
    .property("type", codecForConstString("selectOne"))
    .property("choices", codecForList(codecForUiFormSelectUiChoice()))
    .build("UIFormFieldSelectOne");

const codecForUiFormFieldText = (): Codec<UIFormFieldText> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldText>()
    .property("type", codecForConstString("text"))
    .build("UIFormFieldText");

const codecForUiFormFieldTextArea = (): Codec<UIFormFieldTextArea> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldTextArea>()
    .property("type", codecForConstString("textArea"))
    .build("UIFormFieldTextArea");

const codecForUiFormFieldToggle = (): Codec<UIFormFieldToggle> =>
  codecForUIFormFieldBaseConfigTemplate<UIFormFieldToggle>()
    .property("threeState", codecOptionalDefault(codecForBoolean(), false))
    .property("falseValue", codecForAny())
    .property("trueValue", codecForAny())
    .property("onlyTrueValue", codecOptionalDefault(codecForBoolean(), false))
    .property("type", codecForConstString("toggle"))
    .build("UIFormFieldToggle");

const codecForUiFormField = (): Codec<UIFormElementConfig> =>
  buildCodecForUnion<UIFormElementConfig>()
    .discriminateOn("type")
    .alternative("array", codecForLazy(codecForUiFormFieldArray))
    .alternative("group", codecForLazy(codecForUiFormFieldGroup))
    .alternative("void", codecForUiFormVoid())
    .alternative("download-link", codecForUIFormElementLink())
    .alternative("external-link", codecForUIFormElementExternalLink())
    .alternative("absoluteTimeText", codecForUiFormFieldAbsoluteTime())
    .alternative("isoDateText", codecForUiFormFieldIsoDate())
    .alternative("amount", codecForUiFormFieldAmount())
    .alternative("caption", codecForUiFormFieldCaption())
    .alternative("htmlIframe", codecForUiFormFieldHtmlIFrame())
    .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal())
    .alternative("choiceStacked", codecForUiFormFieldChoiceStacked())
    .alternative("file", codecForUiFormFieldFile())
    .alternative("integer", codecForUiFormFieldInteger())
    .alternative("secret", codecForUiFormFieldSecret())
    .alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
    .alternative("duration", codecForUiFormFieldDuration())
    .alternative("durationText", codecForUiFormFieldDurationText())
    .alternative("selectOne", codecForUiFormFieldSelectOne())
    .alternative("text", codecForUiFormFieldText())
    .alternative("textArea", codecForUiFormFieldTextArea())
    .alternative("toggle", codecForUiFormFieldToggle())
    .build("UIFormField");

const codecForDoubleColumnFormSection = (): Codec<DoubleColumnFormSection> =>
  buildCodecForObject<DoubleColumnFormSection>()
    .property("title", codecForString())
    .property("description", codecOptional(codecForString()))
    .property("fields", codecForList(codecForUiFormField()))
    .build("DoubleColumnFormSection");

const codecForDoubleColumnFormDesign = (): Codec<DoubleColumnFormDesign> =>
  buildCodecForObject<DoubleColumnFormDesign>()
    .property("type", codecForConstString("double-column"))
    .property("sections", codecForList(codecForDoubleColumnFormSection()))
    .build("DoubleColumnFormDesign");

const codecForSingleColumnFormDesign = (): Codec<SingleColumnFormDesign> =>
  buildCodecForObject<SingleColumnFormDesign>()
    .property("type", codecForConstString("single-column"))
    .property("fields", codecForList(codecForUiFormField()))
    .build("SingleColumnFormDesign");

const codecForFormDesign = (): Codec<FormDesign> =>
  buildCodecForUnion<FormDesign>()
    .discriminateOn("type")
    .alternative("double-column", codecForDoubleColumnFormDesign())
    .alternative("single-column", codecForSingleColumnFormDesign())
    .build<FormDesign>("FormDesign");

const codecForFormMetadata = (): Codec<FormMetadata> =>
  buildCodecForObject<FormMetadata>()
    .property("label", codecForString())
    .property("description", codecOptional(codecForString()))
    .property("id", codecForString())
    .property("version", codecForNumber())
    .property("config", codecForFormDesign())
    .build("FormMetadata");

export const codecForUIForms = (): Codec<UiForms> =>
  buildCodecForObject<UiForms>()
    .property("forms", codecForList(codecForFormMetadata()))
    .build("UiForms");

export type FormMetadata = {
  label: string;
  description?: string;
  id: string;
  version: number;
  config: FormDesign | ((context: any) => FormDesign);
};

export interface UiForms {
  forms: Array<FormMetadata>;
}
