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

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

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

import {
  AbsoluteTime,
  AmountJson,
  assertUnreachable,
  TalerExchangeApi,
  TranslatedString,
} from "@gnu-taler/taler-util";
import { useMemo, useState } from "preact/hooks";
import {
  FormDesign,
  FormMetadata,
  InternationalizationAPI,
  UIFieldHandler,
  UIFormElementConfig,
  useTranslationContext,
} from "../index.browser.js";

/**
 * Underlying state model for the form UI.
 */
export interface FormModel {
  /**
   * Get a handler for an UI field based on the field identifier.
   */
  getHandlerForUiField(fieldId: string): UIFieldHandler;

  /**
   * Get the field handler for an attribute.
   *
   * If there are multiple handlers for the same attribute path,
   * an arbitrary handler is returned.
   *
   * (In the future, this might be changed to return the only currently
   * visible handler.)
   */
  getHandlerForAttributeKey(attributeKey: string): UIFieldHandler;

  /**
   * Check if a section of the form is hidden.
   */
  isSectionHidden(sectionName: string): boolean;
}

/**
 * Implementation of {@link FormModel}.
 */
class FormModelImpl implements FormModel {
  public fieldHandlers: { [x: string]: UIFieldHandler } = {};
  public hiddenSections: Set<string | number> = new Set();

  getHandlerForUiField(fieldId: string): UIFieldHandler {
    return this.fieldHandlers[fieldId];
  }

  getHandlerForAttributeKey(attributeKey: string): UIFieldHandler {
    for (const h of Object.values(this.fieldHandlers)) {
      if (h.name === attributeKey) {
        return h;
      }
    }
    throw Error(`no handler for attribute path ${attributeKey}`);
  }

  isSectionHidden(sectionName: string): boolean {
    return this.hiddenSections.has(sectionName);
  }
}

export type FormValues<T> = {
  [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
};

export type RecursivePartial<T> = {
  [k in keyof T]?: T[k] extends string
    ? string
    : T[k] extends AmountJson
      ? T[k]
      : T[k] extends Array<any>
        ? T[k]
        : T[k] extends TalerExchangeApi.AmlState
          ? T[k]
          : RecursivePartial<T[k]>;
};

export type ErrorAndLabel = {
  message: TranslatedString;
  label: TranslatedString;
  section: TranslatedString | undefined;
};

export type FormErrors<T> = {
  [k in keyof T]?: T[k] extends string
    ? ErrorAndLabel
    : T[k] extends AmountJson
      ? ErrorAndLabel
      : T[k] extends AbsoluteTime
        ? ErrorAndLabel
        : T[k] extends TalerExchangeApi.AmlState
          ? ErrorAndLabel
          : FormErrors<T[k]>;
};

export type FormStatus<T> =
  | {
      status: "ok";
      result: T;
      errors: undefined;
    }
  | {
      status: "fail";
      result: RecursivePartial<T>;
      errors: FormErrors<T>;
    };

/**
 * FIMXE: Consider renaming this to FormModel and folding the current FormModel into it.
 */
export type FormState<T> = {
  design: FormDesign;
  model: FormModel;
  status: FormStatus<T>;
  update: (f: FormValues<T>) => void;
};

/**
 * Hook to instantiate a form from its metadata.
 */
export function useFormMeta<T>(
  form: FormMetadata,
  formContext: any,
  initialValue: RecursivePartial<FormValues<T>>,
): FormState<T> {
  let formDesign: FormDesign;
  if (typeof form.config === "function") {
    const config = form.config;
    formDesign = useMemo(() => config(formContext), [form, formContext]);
  } else {
    formDesign = form.config;
  }
  return useForm(formDesign, initialValue);
}

/**
 * Hook to instantiate a form from its design.
 */
export function useForm<T>(
  design: FormDesign,
  initialValue: RecursivePartial<FormValues<T>>,
): FormState<T> {
  const { i18n } = useTranslationContext();
  const [formValue, formUpdateHandler] =
    useState<RecursivePartial<FormValues<T>>>(initialValue);

  const { model, result, errors } = constructFormHandler(
    design,
    formValue,
    formUpdateHandler,
    i18n,
  );

  const status = {
    status: errors === undefined ? "ok" : "fail",
    result,
    errors,
  } as FormStatus<T>;

  return {
    model,
    status,
    update: (f) => {
      formUpdateHandler(f as any);
    },
    design,
  };
}

/**
 * Use {@link path} to get the value of {@link object}.
 * Return {@link fallbackValue} if the target property is undefined
 */
export function getValueFromPath(
  object: any,
  path: string[],
  fallbackValue?: any,
): any {
  if (path.length === 0) return object;
  const [head, ...rest] = path;
  if (!head) {
    return getValueFromPath(object, rest, fallbackValue);
  }
  if (object === undefined) {
    return fallbackValue;
  }
  return getValueFromPath(object[head], rest, fallbackValue);
}

/**
 * Use $path to set the value $value into $object
 * Don't modify $object, returns a new value
 * returns undefined if the object is empty
 */
function setValueIntoPath(object: any, path: string[], value: any): any {
  if (path.length === 0) return value;
  const [head, ...rest] = path;
  if (!head) {
    return setValueIntoPath(object, rest, value);
  }
  if (object === undefined) {
    return undefinedIfEmpty({ [head]: setValueIntoPath({}, rest, value) });
  }
  return undefinedIfEmpty({
    ...object,
    [head]: setValueIntoPath(object[head] ?? {}, rest, value),
  });
}

export function undefinedIfEmpty<T extends object | undefined>(
  obj: T,
): T | undefined {
  if (obj === undefined) return undefined;
  return Object.keys(obj).some(
    (k) => (obj as Record<string, T>)[k] !== undefined,
  )
    ? obj
    : undefined;
}

function checkFormFieldIsValid(
  formElement: UIFormElementConfig,
  currentValue: string | undefined,
  i18n: InternationalizationAPI,
  secitonTitle: string | undefined,
  form: any,
): ErrorAndLabel | undefined {
  if (!("id" in formElement)) {
    return undefined;
  }

  if (formElement.required && currentValue === undefined) {
    return {
      label: formElement.label as TranslatedString,
      message: i18n.str`required`,
      section: secitonTitle as TranslatedString,
    };
  } else if (formElement.validator) {
    try {
      const message = formElement.validator(currentValue as any, form);
      if (message !== undefined) {
        return {
          label: formElement.label as TranslatedString,
          message,
          section: secitonTitle as TranslatedString,
        };
      }
    } catch (e) {
      console.error(e);
      const message = i18n.str`Validation function failed. Contact developers ${String(
        e,
      )}`;
      console.log(message);
      return {
        label: formElement.label as TranslatedString,
        message,
        section: secitonTitle as TranslatedString,
      };
    }
  }
  return undefined;
}

/**
 * @param formValue Plain, unprocessed form contents.
 */
function constructFormHandler<T>(
  design: FormDesign,
  formValue: RecursivePartial<FormValues<T>>,
  onValueChange: (d: RecursivePartial<FormValues<T>>) => void,
  i18n: InternationalizationAPI,
): {
  model: FormModel;
  result: FormStatus<T>;
  errors: FormErrors<T> | undefined;
} {
  let model: FormModelImpl = new FormModelImpl();
  let result = {} as FormStatus<T>;
  let errors: FormErrors<T> | undefined = undefined;

  function createFieldHandler(
    formElement: UIFormElementConfig,
    hiddenSection: boolean | undefined,
    handlerUiPath: string,
    secitonTitle: string | undefined,
  ): void {
    let field: UIFieldHandler;

    if ("id" in formElement) {
      const path = formElement.id.split(".");

      const currentValue = getValueFromPath(formValue as any, path, undefined);

      // compute prop based on state
      const hidden =
        hiddenSection ||
        formElement.hidden ||
        (formElement.hide && formElement.hide(currentValue, result));

      const currentError: ErrorAndLabel | undefined = !hidden
        ? checkFormFieldIsValid(
            formElement,
            currentValue,
            i18n,
            secitonTitle,
            formValue,
          )
        : undefined;

      if (currentError !== undefined) {
        errors = setValueIntoPath(errors, path, currentError);
      }

      function updater(newValue: unknown) {
        const updated = setValueIntoPath(formValue, path, newValue) ?? {};
        onValueChange(updated);
      }

      field = {
        name: formElement.id,
        error: currentError?.message,
        value: currentValue,
        onChange: updater,
        formRootResult: result,
        hidden,
      };
      if (!hidden) {
        result = setValueIntoPath(result, path, field.value) ?? {};
      }
    } else {
      const hidden =
        hiddenSection ||
        formElement.hidden ||
        (formElement.hide && formElement.hide({}, result));
      field = {
        name: "<none>",
        value: undefined,
        onChange: () => {},
        formRootResult: result,
        hidden,
      };
    }

    model.fieldHandlers[handlerUiPath] = field;
  }

  switch (design.type) {
    case "double-column": {
      design.sections.forEach((sec, secIndex) => {
        const hidden = sec.hide && sec.hide(result);
        if (hidden) {
          model.hiddenSections.add(`${secIndex}`);
        }

        sec.fields.forEach((f, fieldIndex) =>
          createFieldHandler(f, hidden, `${secIndex}.${fieldIndex}`, sec.title),
        );
      });
      break;
    }
    case "single-column": {
      design.fields.forEach((f, fieldIndex) =>
        createFieldHandler(f, undefined, `root.${fieldIndex}`, undefined),
      );
      break;
    }
    default: {
      assertUnreachable(design);
    }
  }

  return { model, result, errors };
}
