import { FAIdentifier } from "../../types/fa-icons";
import { OSLabel } from "../label";
import { Calendar } from "primereact/calendar";
import {
  Dropdown,
  DropdownChangeEvent,
  DropdownFilterEvent,
} from "primereact/dropdown";
import { InputNumber, InputNumberChangeEvent } from "primereact/inputnumber";
import { InputText } from "primereact/inputtext";
import { SelectItemOptionsType } from "primereact/selectitem";
import { Nullable } from "primereact/ts-helpers";
import React, {
  ReactNode,
  useEffect,
  useState,
  forwardRef,
  useImperativeHandle,
  useRef,
} from "react";
import { InputTextarea } from "primereact/inputtextarea";
import { goPost } from "../../utils/goFetch";
import { InputSwitch, InputSwitchChangeEvent } from "primereact/inputswitch";
import { Toast } from "primereact/toast";
import { OSInputType, OSInputSize } from "./Enums";
import { normalizeDate as normalizeDateHelper } from "../../helpers/DateHelper";
import { KeyFilterType } from "primereact/keyfilter";
import { InputMask, InputMaskChangeEvent } from "primereact/inputmask";
import { ListBox, ListBoxChangeEvent } from "primereact/listbox";
import { MultiSelect, MultiSelectChangeEvent } from "primereact/multiselect";
import CustomToggleButton from "../input/CustomToggleButton";
import { DropdownProps } from "primereact/dropdown";
import {
  confirmDialog,
  ConfirmDialogOptions,
  ConfirmDialogProps,
} from "primereact/confirmdialog";
import { IconType } from "primereact/utils";
import { DialogProps } from "primereact/dialog";
import { Checkbox, CheckboxChangeEvent } from "primereact/checkbox";
import { OSIcon } from "../icon";
import { SelectButton } from "primereact/selectbutton";
import { OSDivider } from "../divider";
import { OSButton } from "../button/OSButton";

/**
 * Interface used for configuring each input on the form.
 */
export interface Yii2InputTemplate {
  /**
   * Refers to the models attribute. e.g. guid or name
   */
  field: string;

  /**
   * Refers to the model name. e.g AccessPhoneEntity.
   * If set at this field level, the field will be posted under this model.
   */
  model?: string;

  /**
   * Friendly label for the input, will also be used for 'required' error.
   * e.g. ${label} is required!
   */
  label?: string;

  /**
   * Custom ReactNode in place of label
   */
  labelTemplate?: ReactNode;

  /**
   * Hide's the input's label
   */
  hideLabel?: boolean;

  /**
   * Input's placeholder text
   */
  placeholder?: string;

  /**
   * This hint will show on an info icon hover.
   */
  hint?: string;

  /**
   * Allows configuration for full, half and third input sizes.
   * All sizes should be responsive.
   */
  size?: OSInputSize;

  /**
   * Icon that will display next to the field label.
   */
  icon?: FAIdentifier;

  /**
   * Default value for this field.
   * Set this when dealing with updates.
   * @todo Change list to handle any value without using any.
   */
  value?:
    | string
    | number
    | Date
    | (Date | null)[]
    | Date[]
    | null
    | string[]
    | number[]
    | any;

  /**
   * Refers to the input component's classname.
   * e.g. <TextInputArea classname={field.className}/>
   */
  className?: string;

  /**
   * Whether or not to show the field. Can be dynamically set with a state for conditional rendering
   *
   * Defaults to `undefined` which behaves in the same way as `true`
   */
  render?: boolean;

  /**
   * Whether or not this field will be editable if update scenario.
   */
  disabledOnUpdate?: boolean;

  /**
   * Manually disable field.
   */
  disabled?: boolean;

  /**
   * Whether or not to run local validation on the field to ensure a value is set.
   */
  required?: boolean;

  /**
   * See @enum OSInputType below.
   */
  type: OSInputType;

  /**
   * Whether or not the field should be read only
   */
  readonly?: boolean | undefined;

  /**
   * Date format if input type is date.
   */
  dateFormat?: string;

  /**
   * The minimum date if the input type is date.
   */
  dateMin?: Date;

  /**
   * The maximum date if the input type is date.
   */
  dateMax?: Date;

  /**
   * If an input type is date, whether or not to show the button bar.
   */
  dateButtonBar?: boolean;

  /**
   * Number type of either integer or double if input type is number.
   */
  numberType?: string;

  /**
   * Min number if input type is number.
   */
  minNumber?: number;

  /**
   * Max number if input type is number.
   */
  maxNumber?: number;

  /**
   * Min chars if input type is text.
   */
  stringMin?: number;

  /**
   * Max chars if input type is text.
   */
  stringMax?: number;

  /**
   * Custom ReactNode dropdown if input type is dropdown.
   * @ignore Not implemented
   */
  dropDown?: ReactNode;

  /**
   * Dropdown options if input type is dropdown.
   * Label used for display in input, with value being the underlying field value, usually primary key.
   * e.g. [{label: 'Label 1', value: 'value'}]
   */
  dropDownOptions?: SelectItemOptionsType;

  /**
   * Allow the dropdown to be an editible type. Allowing the user to type a value.
   */
  dropDownEditible?: boolean | undefined;

  /**
   * Where relevant, allow the selection of multiple options
   */
  selectMultiple?: boolean;

  /**
   * Any actions to take before saving the input value to the form object.
   */
  onChangeBeforeActions?: (
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    e: any,
    fields: Yii2InputTemplate[],
    formData: GenericField[],
    setFormData: React.Dispatch<React.SetStateAction<GenericField[]>>,
  ) => void;

  /**
   * Any actions to take after saving the input value to the form object.
   */
  onChangeAfterActions?: (
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    e: any,
    fields: Yii2InputTemplate[],
    formData: GenericField[],
    setFormData: React.Dispatch<React.SetStateAction<GenericField[]>>,
  ) => void;

  /**
   * Apply a text filter to a text field.
   *
   * @see https://primereact.org/keyfilter/
   */
  textKeyFilterType?: KeyFilterType | undefined;

  /**
   * Apply a mask to a text field
   *
   * @see https://primereact.org/inputmask/
   */
  mask?: string | undefined;

  /**
   * Item template for Multiselect Dropdown
   */
  itemTemplate?: ReactNode | ((option: any) => React.ReactNode);

  /**
   * Value template for Dropdown Input
   */
  valueTemplate?:
    | ReactNode
    | ((option: any, props: DropdownProps) => React.ReactNode);

  /**
   * Empty input message.
   * Currently only utilized in MultiSelect
   */
  emptyMessage?: string;

  /**
   * Custom component as part of a form
   */
  component?: ReactNode;

  /**
   * Used for CustomToggleButton
   */
  onLabel?: string;

  /**
   * Used for CustomToggleButton
   */
  offLabel?: string;

  /**
   * Callback for onFilter event
   */
  onFilter?: (event: DropdownFilterEvent) => void;

  /**
   * Whether filter is enabled for dropdown. Default: false
   */
  filter?: boolean;

  /**
   * Whether to filter by the value or the label
   */
  filterBy?: "label" | "value";

  /**
   * Placeholder text for dropdown filter
   */
  filterPlaceholder?: string;

  /**
   * Custom ReactNode for dropdown footer
   */
  dropdownFooter?: ReactNode;

  /**
   * Used for setting state of dropdown esque inputs when doing an ajax request to populate options
   */
  loading?: boolean;

  /**
   * Custom message for required field.
   * Defaults to '{field.label} is required!'
   */
  customRequiredMessage?: string;

  /**
   * Show clear button on dropdown that has filtering enabled
   */
  showClear?: boolean | undefined;

  /**
   * Icon for dropdown filter clear button
   */
  clearIcon?: IconType<DropdownProps>;

  /**
   * Key name for dropdown component values.
   * Defaults to "value"
   */
  optionValue?: string;

  /**
   * Key name for dropdown component labels.
   * Defaults to "label"
   */
  optionLabel?: string;
}

/**
 * Structure of Yii'2 model errors.
 */
export interface Yii2ValidationError {
  field: string;
  errors: string[];
}

export interface Yii2ResponseObject {
  success: boolean;
  errors: Yii2ValidationError[];
  code?: number;
  message?: string;
}

export interface OSFormProps {
  /**
   * This is used for the default model that field values will be posted under if the field.model is not set.
   * e.g. model: AccessMethod => Post: ['AccessMethod' => ['attribute1' => 'value1', 'attribute2' => 'value2']]
   */
  model: string;

  /**
   * An array of input configurations
   */
  fields: Yii2InputTemplate[];

  /**
   * Controller you want the form to point to.
   * If this is set and custom endpoint is not, it will look at ${controller}/create for create scenarios and ${controller}/update for update scenarios.
   */
  controller?: string;

  /**
   * You can specify a custom controller endpoint to post the form to.
   */
  customEndpoint?: string;

  /**
   * Allows for passing any additional data in the post.
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  additionalParams?: { key: string; value: any }[];

  /**
   * Whether or not the scenario is update. This will:
   *  1. Disable fields that have disableOnUpdate = true.
   *  2. Sets the button caption to 'Update' if using built in buttons or submitLabel prop. (@see useCustomButtons prop)
   *  3. If controller is set and customEndpoint is not, will post to ${controller}/update
   */
  isUpdate?: boolean;

  /**
   * Custom label for submit button
   */
  submitLabel?: string;

  /**
   * Custom label for cancel button
   */
  cancelLabel?: string;

  /**
   * Allows for parent to hook into the beforeSubmit event.
   * @returns void
   */
  beforeSubmit?: (props: OSFormProps, formData: GenericField[]) => void;

  /**
   * Allows for parent to hook into the afterSubmit event.
   * @returns void
   */
  afterSubmit?: (response: object) => void;

  /**
   * Allows for parent to hook into the onCancel event.
   * @returns void
   */
  onCancel?: () => void;

  /**
   * If true, built in buttons will not render.
   * To hook into the handleSubmit, handleCancel, networkProcessing and resetForm functionality, useRef<OSFormRef> is required.
   * @see OSFormRef
   */
  useCustomButtons?: boolean;

  /**
   * Allows parent to hook into network processing state
   * @param networkProcessing
   * @returns void
   */
  onNetworkProcessingChange?: (networkProcessing: boolean) => void;

  /**
   * If you want a confirmation before allowing submission, set the details here
   *
   * @see https://primereact.org/confirmdialog/
   * @see `node_modules/primereact/confirmdialog/confirmdialog.d.ts`
   */
  confirmBeforeSubmit?: {
    /**
     * Message of the confirmation.
     */
    message:
      | React.ReactNode
      | ((options: ConfirmDialogOptions) => React.ReactNode);
    /**
     * Title content of the dialog.
     */
    header?: React.ReactNode | ((props: DialogProps) => React.ReactNode);
    /**
     * Icon to display next to the message.
     */
    icon?: IconType<ConfirmDialogProps> | undefined;
    /**
     * Element to receive the focus when the dialog gets visible, valid values are "accept" and "reject".
     * @defaultValue accept
     */
    defaultFocus?: "accept" | "reject";
  };

  /**
   * Hook into any errors from form submission.
   * These are different to model errors, used for 500 type exceptions
   * @param error Error message
   * @returns void
   */
  onSubmitError?: (error: Yii2ResponseObject) => void;

  /**
   * Whether to throw the full body response on post error
   */
  throwFullResponse?: boolean;

  /**
   * Deliberately disable the submit button
   */
  disableSubmit?: boolean;
}

/**
 * Used for formdata management
 */
export interface GenericField {
  field: string;
  required: boolean;
  value?:
    | string
    | number
    | Date
    | (Date | null)[]
    | Date[]
    | null
    | boolean
    | string[]
    | number[];
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  errors?: any;

  /** If set, no trimming of values will take place */
  readOnly?: boolean;
}

/**
 * Used for building the post object
 */
type Yii2PostObject = {
  [key: string]:
    | {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        [field: string]: any;
      }
    | Array<{
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        [field: string]: any;
      }>;
};

/**
 * Converts TS Date into y-m-d string
 * @param date Date object
 * @returns string e.g. 2025-02-10
 * @deprecated use helpers/DateHelper/normalizeDate
 */
export const normalizeDate = (date: Date): string => {
  const localDate = new Date(date);
  localDate.setHours(0, 0, 0, 0);
  const year = localDate.getFullYear();
  const month = String(localDate.getMonth() + 1).padStart(2, "0");
  const day = String(localDate.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
};

/**
 * Ref to use for implementing custom buttons and hooking in to reset form.
 * @example
 * const osFormRef = useRef<OSFormRef>(null); //initialize to null
 * <OSForm ref={osFormRef} />
 * const parentHandleSubmit = () => {
 *  if(osFormRef.current) {
 *      osFormRef.current.handleSubmit();
 *  }
 * }
 *
 */
export interface OSFormRef {
  handleSubmit: () => Promise<void>;
  handleCancel: () => void;
  resetForm: () => void;
  networkProcessing: boolean;
}

const OSForm = forwardRef<OSFormRef, OSFormProps>(
  ({ onNetworkProcessingChange, ...props }, ref) => {
    const [networkProcessing, setNetworkProcessing] = useState<boolean>(false);
    const [formReset, setFormReset] = useState<boolean>(true);
    const toast = useRef<Toast>(null);

    useEffect(() => {
      if (onNetworkProcessingChange) {
        onNetworkProcessingChange(networkProcessing);
      }
    }, [networkProcessing, onNetworkProcessingChange]);

    /**
     * Resets form to field configuration passed.
     * @returns Defaulted form data
     */
    const defaultFormData = (): GenericField[] => {
      return props.fields
        .map((field) => {
          if (field.type === OSInputType.Divider) {
            return undefined;
          }

          if (field.type === OSInputType.Date && field.value !== undefined) {
            if (
              typeof field.value === "string" ||
              typeof field.value === "number" ||
              field.value instanceof Date
            )
              field.value = normalizeDateHelper(new Date(field.value));
          }
          return {
            field: field.field,
            value: field.value,
            required: field.required ?? false,
            readOnly:
              (field.disabledOnUpdate && props.isUpdate) ||
              field.readonly ||
              field.disabled,
          };
        })
        .filter((field) => field !== undefined);
    };

    const [formData, setFormData] = useState<GenericField[]>(defaultFormData);

    /**
     * Resets form to default configuration passed.
     * @ignore Currently scuffed and broken.
     */
    const resetForm = () => {
      setFormReset(false);
      setFormData(defaultFormData);
      setFormReset(true);
    };

    /**
     * Runs local check on whether any fields set to required don't have a value.
     * @returns Whether there are any fields that are required that don't have a value.
     */
    const localValidation = (): boolean => {
      const updatedFormData = formData.map((form) => {
        const errors: string[] = [];
        if (
          form.required &&
          (form.value === undefined || form.value === null || form.value === "")
        ) {
          const field = props.fields.find(
            (field) => field.field === form.field,
          )!;
          if (field.customRequiredMessage) {
            errors.push(`${field.customRequiredMessage}`);
          } else {
            errors.push(`${field.label} is required!`);
          }
        }

        if (typeof form.value === "string" && !form.readOnly) {
          // remove any leading, trailing or double whitespace
          form.value = form.value.replace(/ +(?= )/g, "").trim();
        }

        return {
          ...form,
          errors,
        };
      });
      setFormData(updatedFormData);
      return !updatedFormData.some((form) => form.errors.length > 0);
    };

    /**
     * Populates error fields in the form data from errors returned from server side model.
     * Todo: Handle non field errors
     * @param serverErrors Errors returned from server side model
     */
    const handleServerValidation = (serverErrors: Yii2ValidationError[]) => {
      //   console.table(serverErrors);
      const errors = Object.entries(serverErrors).map(([field, errors]) => ({
        field,
        errors,
      }));

      const updatedFormData = formData.map((form) => {
        const fieldErrors = errors.find(
          (error) => error.field === form.field,
        )?.errors;
        return {
          ...form,
          errors: fieldErrors,
        };
      });
      setFormData(updatedFormData);
    };

    const getInvalid = (fieldName: string) => {
      const errors = formData.find((form) => form.field == fieldName)?.errors;
      if (errors !== undefined && errors.length > 0) {
        return true;
      }
      return false;
    };

    const renderFieldErrors = (fieldName: string) => {
      const fieldData = formData.find((f) => f.field === fieldName);
      return fieldData?.errors ? errorTemplate(fieldData.errors) : null;
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const errorTemplate = (error: any) => (
      <small className="text-sm" style={{ color: "var(--red-500)" }}>
        {error[0]}
      </small>
    );

    /**
     * Sets the classname for the input based on the inputSize configuration.
     * @param field Form field
     * @returns Responsive class name
     */
    const getFieldClassName = (field: Yii2InputTemplate) => {
      if (field.size === undefined) {
        field.size = OSInputSize.Full;
      }
      const sizeClass =
        field.size === OSInputSize.Full
          ? "col-span-12"
          : field.size === OSInputSize.Half
            ? "md:col-span-6 xs:col-span-12"
            : "lg:col-span-4 md:col-span-6 xs:col-span-12";

      return `${sizeClass} flex flex-col gap-1 mb-2`;
    };

    const getFieldLabel = (field: Yii2InputTemplate) => {
      return field.label ? (
        <OSLabel
          className="os-form-field-label"
          label={field.label}
          tag={"span"}
          leadingIcon={field.icon}
          trailingIcon={
            field.hint
              ? {
                  identifier: "circle-info",
                  tooltip: field.hint,
                  weight: "solid",
                  colour: "var(--os-accent)",
                }
              : undefined
          }
        />
      ) : (
        field.labelTemplate
      );
    };

    /**
     * Updates the formData on every change.
     * Should probably add some delay but shouldn't reduce performance.
     * @param fieldName
     * @param value
     */
    const handleInputChange = (
      fieldName: string,
      value: string | number | null | Nullable<Date> | boolean,
    ) => {
      setFormData((prevFormData) =>
        prevFormData.map((f) =>
          f.field === fieldName ? { ...f, value: value, errors: [] } : f,
        ),
      );
    };

    /**
     * Converts string dates that are usual in our models to a Date object that the Calendar inputs can use.
     * @param fieldName
     * @returns
     */
    const getDateValue = (fieldName: string) => {
      const value = formData.find((f) => f.field === fieldName)?.value;
      if (value) {
        if (value instanceof Date) {
          return value;
        }
        if (typeof value === "string" || typeof value === "number") {
          return new Date(value);
        }
      }
      return null;
    };

    /**
     * Converts number into boolean for switch inputs.
     * @param fieldName
     * @returns
     */
    const getCheckedValue = (fieldName: string) => {
      const value = formData.find((f) => f.field === fieldName)?.value;
      if (value) {
        if (typeof value === "boolean") {
          return value;
        }
        if (typeof value === "number") {
          return value === 1 ? true : false;
        }
      }
      return false;
    };

    const getTextValue = (fieldName: string) => {
      const value = formData.find((f) => f.field === fieldName)?.value;
      if (value && typeof value == "string") {
        return value;
      }
      return undefined;
    };

    const getUrlString = () => {
      return props.customEndpoint
        ? props.customEndpoint
        : props.isUpdate
          ? `${props.controller}/update`
          : `${props.controller}/create`;
    };

    /**
     * Handles building of the post body.
     * If there is an additional param with the same model as the main model
     * @param formData
     * @returns
     */
    const transformData = (formData: GenericField[]): Yii2PostObject => {
      const result: Yii2PostObject = {};
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const formDataObject: { [field: string]: any } = {};
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const specificModels: { key: string; value: any }[] = [];

      formData.forEach((field) => {
        const value =
          typeof field.value === "boolean"
            ? field.value === true
              ? 1
              : 0
            : field.value;
        //handle specific model
        const fieldModel = props.fields.find(
          (f) => f.field === field.field,
        )?.model;
        if (fieldModel) {
          specificModels.push({
            key: fieldModel,
            value: { [field.field]: value },
          });
        } else {
          formDataObject[field.field] = value;
        }
      });

      result[props.model] = formDataObject;

      const mergedAdditionalParams = [
        ...(props.additionalParams || []),
        ...specificModels,
      ];
      if (mergedAdditionalParams) {
        mergedAdditionalParams.forEach((param) => {
          if (result[param.key]) {
            // handle adding additional params that are the same as the model
            result[param.key] = { ...result[param.key], ...param.value };
          } else {
            result[param.key] = param.value;
          }
        });
      }

      //   console.table(result);
      return result;
    };

    /**
     * Handles submission to server.
     * Todo: Handle errors more elegantly
     */
    const handleSubmit = async () => {
      const post = () =>
        goPost(
          getUrlString(),
          transformData(formData),
          null,
          props.throwFullResponse,
        )
          .then((response) => {
            const responseData = response as Yii2ResponseObject;
            // console.log("Form response: ");
            // console.table(response);
            // console.table(responseData);
            if (!responseData.success) {
              handleServerValidation(responseData.errors);
              setNetworkProcessing(false);
              return;
            } else {
              setNetworkProcessing(false);
              if (props.afterSubmit) {
                props.afterSubmit(responseData);
              }
            }
          })
          .catch((error) => {
            console.error(error);
            setNetworkProcessing(false);
            handleSubmitError(error as Yii2ResponseObject);
          });

      if (!localValidation()) {
        return;
      }

      setNetworkProcessing(true);
      if (props.beforeSubmit) {
        props.beforeSubmit(props, formData);
      }
      if (props.confirmBeforeSubmit !== undefined) {
        const accept = async () => await post();
        const reject = async () => setNetworkProcessing(false);
        // console.log(props.confirmBeforeSubmit);
        confirmDialog({
          message: props.confirmBeforeSubmit.message,
          header: props.confirmBeforeSubmit.header,
          icon: props.confirmBeforeSubmit.icon,
          defaultFocus: props.confirmBeforeSubmit.defaultFocus,
          accept,
          reject,
        });
      } else {
        await post();
      }
    };

    const handleCancel = () => {
      if (props.onCancel) {
        props.onCancel();
      }
    };

    const handleSubmitError = (error: Yii2ResponseObject) => {
      if (props.onSubmitError) {
        props.onSubmitError(error);
      }
    };

    useImperativeHandle(ref, () => ({
      handleSubmit,
      handleCancel,
      resetForm,
      networkProcessing,
    }));

    return (
      <>
        <Toast ref={toast} />
        {/* YOU MUST SET THIS IN YOUR PAGE */}
        {/* <ConfirmDialog /> */}
        <div className="flex flex-col">
          <div className="grid grid-cols-12 w-full gap-2">
            {formReset &&
              props.fields.map((field, i) => {
                if (field.render === false) {
                  return <React.Fragment key={i}></React.Fragment>;
                }
                switch (field.type) {
                  case OSInputType.Text:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        {field.mask !== undefined ? (
                          <InputMask
                            value={getTextValue(field.field) || ""} // || to maintain controlled field
                            onChange={(e: InputMaskChangeEvent) => {
                              if (field.onChangeBeforeActions !== undefined) {
                                field.onChangeBeforeActions(
                                  e,
                                  props.fields,
                                  formData,
                                  setFormData,
                                );
                              }
                              handleInputChange(field.field, e.target.value);
                              if (field.onChangeAfterActions !== undefined) {
                                field.onChangeAfterActions(
                                  e,
                                  props.fields,
                                  formData,
                                  setFormData,
                                );
                              }
                            }}
                            className={`w-auto`}
                            disabled={
                              field.disabled ||
                              (props.isUpdate && field.disabledOnUpdate)
                            }
                            invalid={getInvalid(field.field)}
                            placeholder={field.placeholder}
                            keyfilter={field.textKeyFilterType}
                            mask={field.mask}
                            readOnly={field.readonly}
                          />
                        ) : (
                          <InputText
                            value={getTextValue(field.field) || ""} // || to maintain controlled field
                            onChange={(
                              e: React.ChangeEvent<HTMLInputElement>,
                            ) => {
                              if (field.onChangeBeforeActions !== undefined) {
                                field.onChangeBeforeActions(
                                  e,
                                  props.fields,
                                  formData,
                                  setFormData,
                                );
                              }
                              handleInputChange(field.field, e.target.value);
                              if (field.onChangeAfterActions !== undefined) {
                                field.onChangeAfterActions(
                                  e,
                                  props.fields,
                                  formData,
                                  setFormData,
                                );
                              }
                            }}
                            className={`w-auto ${field.className}`}
                            disabled={
                              field.disabled ||
                              (props.isUpdate && field.disabledOnUpdate)
                            }
                            invalid={getInvalid(field.field)}
                            placeholder={field.placeholder}
                            keyfilter={field.textKeyFilterType}
                            readOnly={field.readonly}
                          />
                        )}

                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.Checkbox:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <Checkbox
                          checked={getCheckedValue(field.field)}
                          onChange={(e: CheckboxChangeEvent) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.checked);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          className="m-2"
                          invalid={getInvalid(field.field)}
                          readOnly={field.readonly}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.TextArea:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <InputTextarea
                          value={getTextValue(field.field) || ""} // || to maintain controlled field
                          onChange={(
                            e: React.ChangeEvent<HTMLTextAreaElement>,
                          ) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.target.value);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          className={`w-auto ${field.className}`}
                          disabled={
                            field.disabled ||
                            (props.isUpdate && field.disabledOnUpdate)
                          }
                          invalid={getInvalid(field.field)}
                          placeholder={field.placeholder}
                          readOnly={field.readonly}
                          maxLength={field.stringMax}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.Number:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <InputNumber
                          value={
                            typeof field.value == "number"
                              ? field.value
                              : undefined
                          }
                          onChange={(e: InputNumberChangeEvent) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.value);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          className={`w-auto ${field.className}`}
                          disabled={
                            field.disabled ||
                            (props.isUpdate && field.disabledOnUpdate)
                          }
                          invalid={getInvalid(field.field)}
                          showButtons
                          placeholder={field.placeholder}
                          readOnly={field.readonly}
                          max={field.maxNumber}
                          useGrouping={false}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.Date:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <Calendar
                          id="joinStartDate"
                          dateFormat={field.dateFormat || "dd MM yy"}
                          value={getDateValue(field.field)}
                          minDate={field.dateMin}
                          maxDate={field.dateMax}
                          showButtonBar={field.dateButtonBar}
                          onChange={(e) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            const selectedDate = e.value
                              ? normalizeDateHelper(e.value)
                              : null;
                            handleInputChange(field.field, selectedDate);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          className={`w-auto ${field.className}`}
                          showIcon
                          iconPos="right"
                          disabled={
                            field.disabled ||
                            (props.isUpdate && field.disabledOnUpdate)
                          }
                          invalid={getInvalid(field.field)}
                          placeholder={field.placeholder}
                          readOnlyInput={field.readonly}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.Dropdown:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        {field.dropDown ? (
                          field.dropDown
                        ) : (
                          <Dropdown
                            value={
                              formData.find((f) => f.field === field.field)
                                ?.value
                            }
                            optionLabel={field.optionLabel ?? "label"}
                            optionValue={field.optionValue ?? "value"}
                            options={field.dropDownOptions}
                            placeholder={field.placeholder}
                            editable={field.dropDownEditible}
                            onChange={(e: DropdownChangeEvent) => {
                              if (field.onChangeBeforeActions !== undefined) {
                                field.onChangeBeforeActions(
                                  e,
                                  props.fields,
                                  formData,
                                  setFormData,
                                );
                              }
                              handleInputChange(field.field, e.value);
                              if (field.onChangeAfterActions !== undefined) {
                                field.onChangeAfterActions(
                                  e,
                                  props.fields,
                                  formData,
                                  setFormData,
                                );
                              }
                            }}
                            className={`w-auto ${field.className}`}
                            disabled={
                              field.disabled ||
                              (props.isUpdate && field.disabledOnUpdate)
                            }
                            invalid={getInvalid(field.field)}
                            readOnly={field.readonly}
                            itemTemplate={field.itemTemplate}
                            valueTemplate={field.valueTemplate}
                            onFilter={field.onFilter}
                            filter={field.filter}
                            filterPlaceholder={field.filterPlaceholder}
                            panelFooterTemplate={field.dropdownFooter}
                            loading={field.loading}
                            showClear={field.showClear}
                            clearIcon={field.clearIcon}
                          />
                        )}
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.Switch:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <InputSwitch
                          checked={getCheckedValue(field.field)}
                          onChange={(e: InputSwitchChangeEvent) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.value);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          className="my-1"
                          invalid={getInvalid(field.field)}
                          readOnly={field.readonly}
                          disabled={
                            field.disabled ||
                            (props.isUpdate && field.disabledOnUpdate)
                          }
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.Toggle:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <CustomToggleButton
                          checked={getCheckedValue(field.field)}
                          onChange={(e) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.value);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          className="m-2"
                          disabled={field.disabled || field.readonly}
                          onLabel={field.onLabel}
                          offLabel={field.offLabel}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.ListBox:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <ListBox
                          value={
                            formData.find((f) => f.field === field.field)?.value
                          }
                          onChange={(e: ListBoxChangeEvent) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.value);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          options={field.dropDownOptions}
                          className="m-2"
                          invalid={getInvalid(field.field)}
                          readOnly={field.readonly}
                          multiple={field.selectMultiple || false}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.MultiSelect:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <MultiSelect
                          value={
                            formData.find((f) => f.field === field.field)?.value
                          }
                          onChange={(e: MultiSelectChangeEvent) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.value);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          emptyMessage={field.emptyMessage}
                          selectAllLabel="Select All"
                          optionLabel={field.optionLabel ?? "label"}
                          optionValue={field.optionValue ?? "value"}
                          options={field.dropDownOptions}
                          className="m-2"
                          invalid={getInvalid(field.field)}
                          readOnly={field.readonly}
                          maxSelectedLabels={2}
                          itemTemplate={field.itemTemplate}
                          placeholder={field.placeholder}
                          loading={field.loading}
                          loadingIcon={() => {
                            return (
                              <OSIcon
                                identifier="spinner"
                                animation={"spin-pulse"}
                              />
                            );
                          }}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.SelectButton:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.hideLabel ? null : getFieldLabel(field)}
                        <SelectButton
                          value={
                            formData.find((f) => f.field === field.field)?.value
                          }
                          options={field.dropDownOptions}
                          onChange={(e: DropdownChangeEvent) => {
                            if (field.onChangeBeforeActions !== undefined) {
                              field.onChangeBeforeActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                            handleInputChange(field.field, e.value);
                            if (field.onChangeAfterActions !== undefined) {
                              field.onChangeAfterActions(
                                e,
                                props.fields,
                                formData,
                                setFormData,
                              );
                            }
                          }}
                          className={`w-auto ${field.className}`}
                          disabled={
                            field.disabled ||
                            (props.isUpdate && field.disabledOnUpdate)
                          }
                          invalid={getInvalid(field.field)}
                          readOnly={field.readonly}
                          multiple={field.selectMultiple}
                        />
                        {renderFieldErrors(field.field)}
                      </div>
                    );

                  case OSInputType.Hidden:
                    return <React.Fragment key={i}></React.Fragment>;

                  case OSInputType.Divider:
                    return (
                      <OSDivider
                        key={i}
                        className="col-span-12"
                        label={{
                          label: field.label ?? "Divider",
                          tag: "h3",
                          leadingIcon: field.icon,
                          trailingIcon: {
                            identifier: "circle-info",
                            weight: "solid",
                            colour: "var(--os-accent)",
                            tooltip: field.hint,
                          },
                        }}
                      />
                    );

                  case OSInputType.Custom:
                    return (
                      <div className={getFieldClassName(field)} key={i}>
                        {field.component}
                      </div>
                    );
                }
              })}
          </div>
          {!props.useCustomButtons && (
            <div className="flex flex-row justify-end gap-1 py-2">
              <OSButton
                label={props.cancelLabel ?? "Cancel"}
                severity="contrast"
                onClick={handleCancel}
              />
              <OSButton
                label={
                  props.submitLabel
                    ? props.submitLabel
                    : props.isUpdate
                      ? "Update"
                      : "Create"
                }
                severity="success"
                loading={networkProcessing}
                onClick={handleSubmit}
                icon={"cloud-check"}
                disabled={props.disableSubmit === true}
              />
            </div>
          )}
        </div>
      </>
    );
  },
);

export default OSForm;
