import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useState, useEffect } from "react";
import { Controller, useFieldArray, useFormContext, useForm } from "react-hook-form";
import type { UseFormReturn, SubmitHandler } from "react-hook-form";
import type { z } from "zod";

import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import {
  Label,
  Badge,
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogHeader,
  DialogFooter,
  Form,
  BooleanToggleGroupField,
  SelectField,
  InputField,
  Input,
  Switch,
  showToast,
} from "@calcom/ui";
import { ArrowDown, ArrowUp, X, Plus, Trash2 } from "@calcom/ui/components/icon";

import { fieldTypesConfigMap } from "./fieldTypes";
import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml";
import type { fieldsSchema } from "./schema";
import { getVariantsConfig } from "./utils";
import { getFieldIdentifier } from "./utils/getFieldIdentifier";

type RhfForm = {
  fields: z.infer<typeof fieldsSchema>;
};

type RhfFormFields = RhfForm["fields"];

type RhfFormField = RhfFormFields[number];

/**
 * It works with a react-hook-form only.
 * `formProp` specifies the name of the property in the react-hook-form that has the fields. This is where fields would be updated.
 */
export const FormBuilder = function FormBuilder({
  title,
  description,
  addFieldLabel,
  formProp,
  disabled,
  LockedIcon,
  dataStore,
}: {
  formProp: string;
  title: string;
  description: string;
  addFieldLabel: string;
  disabled: boolean;
  LockedIcon: false | JSX.Element;
  /**
   * A readonly dataStore that is used to lookup the options for the fields. It works in conjunction with the field.getOptionAt property which acts as the key in options
   */
  dataStore: {
    options: Record<string, { label: string; value: string; inputPlaceholder?: string }[]>;
  };
}) {
  // I would have liked to give Form Builder it's own Form but nested Forms aren't something that browsers support.
  // So, this would reuse the same Form as the parent form.
  const fieldsForm = useFormContext<RhfForm>();

  const { t } = useLocale();

  const { fields, swap, remove, update, append } = useFieldArray({
    control: fieldsForm.control,
    // HACK: It allows any property name to be used for instead of `fields` property name
    name: formProp as unknown as "fields",
  });

  const [fieldDialog, setFieldDialog] = useState({
    isOpen: false,
    fieldIndex: -1,
    data: {} as RhfFormField | null,
  });

  const addField = () => {
    setFieldDialog({
      isOpen: true,
      fieldIndex: -1,
      data: null,
    });
  };

  const editField = (index: number, data: RhfFormField) => {
    setFieldDialog({
      isOpen: true,
      fieldIndex: index,
      data,
    });
  };

  const removeField = (index: number) => {
    remove(index);
  };

  return (
    <div>
      <div>
        <div className="text-default text-sm font-semibold ltr:mr-1 rtl:ml-1">
          {title}
          {LockedIcon}
        </div>
        <p className="text-subtle max-w-[280px] break-words py-1 text-sm sm:max-w-[500px]">{description}</p>
        <ul className="border-default divide-subtle mt-2 divide-y rounded-md border">
          {fields.map((field, index) => {
            const options = field.options
              ? field.options
              : field.getOptionsAt
              ? dataStore.options[field.getOptionsAt as keyof typeof dataStore]
              : [];
            const numOptions = options?.length ?? 0;
            if (field.hideWhenJustOneOption && numOptions <= 1) {
              return null;
            }
            const fieldType = fieldTypesConfigMap[field.type];
            const isRequired = field.required;
            const isFieldEditableSystemButOptional = field.editable === "system-but-optional";
            const isFieldEditableSystemButHidden = field.editable === "system-but-hidden";
            const isFieldEditableSystem = field.editable === "system";
            const isUserField =
              !isFieldEditableSystem && !isFieldEditableSystemButOptional && !isFieldEditableSystemButHidden;

            if (!fieldType) {
              throw new Error(`Invalid field type - ${field.type}`);
            }
            const sources = field.sources || [];
            const groupedBySourceLabel = sources.reduce((groupBy, source) => {
              const item = groupBy[source.label] || [];
              if (source.type === "user" || source.type === "default") {
                return groupBy;
              }
              item.push(source);
              groupBy[source.label] = item;
              return groupBy;
            }, {} as Record<string, NonNullable<(typeof field)["sources"]>>);

            return (
              <li
                key={field.name}
                data-testid={`field-${field.name}`}
                className="hover:bg-muted group relative flex items-center  justify-between p-4 ">
                {!disabled && (
                  <>
                    {index >= 1 && (
                      <button
                        type="button"
                        className="bg-default text-muted hover:text-emphasis disabled:hover:text-muted border-default hover:border-emphasis invisible absolute -left-[12px] -ml-4 -mt-4 mb-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all hover:shadow disabled:hover:border-inherit disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
                        onClick={() => swap(index, index - 1)}>
                        <ArrowUp className="h-5 w-5" />
                      </button>
                    )}
                    {index < fields.length - 1 && (
                      <button
                        type="button"
                        className="bg-default text-muted hover:border-emphasis border-default hover:text-emphasis disabled:hover:text-muted invisible absolute -left-[12px] -ml-4 mt-8 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all hover:shadow disabled:hover:border-inherit disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
                        onClick={() => swap(index, index + 1)}>
                        <ArrowDown className="h-5 w-5" />
                      </button>
                    )}
                  </>
                )}

                <div>
                  <div className="flex flex-col lg:flex-row lg:items-center">
                    <div className="text-default text-sm font-semibold ltr:mr-2 rtl:ml-2">
                      <FieldLabel field={field} />
                    </div>
                    <div className="flex items-center space-x-2">
                      {field.hidden ? (
                        // Hidden field can't be required, so we don't need to show the Optional badge
                        <Badge variant="grayWithoutHover">{t("hidden")}</Badge>
                      ) : (
                        <Badge variant="grayWithoutHover">{isRequired ? t("required") : t("optional")}</Badge>
                      )}
                      {Object.entries(groupedBySourceLabel).map(([sourceLabel, sources], key) => (
                        // We don't know how to pluralize `sourceLabel` because it can be anything
                        <Badge key={key} variant="blue">
                          {sources.length} {sources.length === 1 ? sourceLabel : `${sourceLabel}s`}
                        </Badge>
                      ))}
                    </div>
                  </div>
                  <p className="text-subtle max-w-[280px] break-words pt-1 text-sm sm:max-w-[500px]">
                    {fieldType.label}
                  </p>
                </div>
                {field.editable !== "user-readonly" && !disabled && (
                  <div className="flex items-center space-x-2">
                    {!isFieldEditableSystem && !isFieldEditableSystemButHidden && !disabled && (
                      <Switch
                        data-testid="toggle-field"
                        disabled={isFieldEditableSystem}
                        checked={!field.hidden}
                        onCheckedChange={(checked) => {
                          update(index, { ...field, hidden: !checked });
                        }}
                        classNames={{ container: "p-2 hover:bg-subtle rounded" }}
                        tooltip={t("show_on_booking_page")}
                      />
                    )}
                    {isUserField && (
                      <Button
                        color="destructive"
                        disabled={!isUserField}
                        variant="icon"
                        onClick={() => {
                          removeField(index);
                        }}
                        StartIcon={Trash2}
                      />
                    )}
                    <Button
                      data-testid="edit-field-action"
                      color="secondary"
                      onClick={() => {
                        editField(index, field);
                      }}>
                      {t("edit")}
                    </Button>
                  </div>
                )}
              </li>
            );
          })}
        </ul>
        {!disabled && (
          <Button
            color="minimal"
            data-testid="add-field"
            onClick={addField}
            className="mt-4"
            StartIcon={Plus}>
            {addFieldLabel}
          </Button>
        )}
      </div>
      {/* Move this Dialog in another component and it would take with it fieldForm */}
      {fieldDialog.isOpen && (
        <FieldEditDialog
          dialog={fieldDialog}
          onOpenChange={(isOpen) =>
            setFieldDialog({
              isOpen,
              fieldIndex: -1,
              data: null,
            })
          }
          handleSubmit={(data: Parameters<SubmitHandler<RhfFormField>>[0]) => {
            const type = data.type || "text";
            const isNewField = !fieldDialog.data;
            if (isNewField && fields.some((f) => f.name === data.name)) {
              showToast(t("form_builder_field_already_exists"), "error");
              return;
            }
            if (fieldDialog.data) {
              update(fieldDialog.fieldIndex, data);
            } else {
              const field: RhfFormField = {
                ...data,
                type,
                sources: [
                  {
                    label: "User",
                    type: "user",
                    id: "user",
                    fieldRequired: data.required,
                  },
                ],
              };
              field.editable = field.editable || "user";
              append(field);
            }
            setFieldDialog({
              isOpen: false,
              fieldIndex: -1,
              data: null,
            });
          }}
        />
      )}
    </div>
  );
};

function Options({
  label = "Options",
  value,
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  onChange = () => {},
  className = "",
  readOnly = false,
}: {
  label?: string;
  value: { label: string; value: string }[];
  onChange?: (value: { label: string; value: string }[]) => void;
  className?: string;
  readOnly?: boolean;
}) {
  const [animationRef] = useAutoAnimate<HTMLUListElement>();
  if (!value) {
    onChange([
      {
        label: "Option 1",
        value: "Option 1",
      },
      {
        label: "Option 2",
        value: "Option 2",
      },
    ]);
  }
  return (
    <div className={className}>
      <Label>{label}</Label>
      <div className="bg-muted rounded-md p-4">
        <ul ref={animationRef}>
          {value?.map((option, index) => (
            <li key={index}>
              <div className="flex items-center">
                <Input
                  required
                  value={option.label}
                  onChange={(e) => {
                    // Right now we use label of the option as the value of the option. It allows us to not separately lookup the optionId to know the optionValue
                    // It has the same drawback that if the label is changed, the value of the option will change. It is not a big deal for now.
                    value.splice(index, 1, {
                      label: e.target.value,
                      value: e.target.value.trim(),
                    });
                    onChange(value);
                  }}
                  readOnly={readOnly}
                  placeholder={`Enter Option ${index + 1}`}
                />
                {value.length > 2 && !readOnly && (
                  <Button
                    type="button"
                    className="-ml-8 mb-2 hover:!bg-transparent focus:!bg-transparent focus:!outline-none focus:!ring-0"
                    size="sm"
                    color="minimal"
                    StartIcon={X}
                    onClick={() => {
                      if (!value) {
                        return;
                      }
                      const newOptions = [...value];
                      newOptions.splice(index, 1);
                      onChange(newOptions);
                    }}
                  />
                )}
              </div>
            </li>
          ))}
        </ul>
        {!readOnly && (
          <Button
            color="minimal"
            onClick={() => {
              value.push({ label: "", value: "" });
              onChange(value);
            }}
            StartIcon={Plus}>
            Add an Option
          </Button>
        )}
      </div>
    </div>
  );
}

function FieldEditDialog({
  dialog,
  onOpenChange,
  handleSubmit,
}: {
  dialog: { isOpen: boolean; fieldIndex: number; data: RhfFormField | null };
  onOpenChange: (isOpen: boolean) => void;
  handleSubmit: SubmitHandler<RhfFormField>;
}) {
  const { t } = useLocale();
  const fieldForm = useForm<RhfFormField>({
    defaultValues: dialog.data || {},
    // resolver: zodResolver(fieldSchema),
  });
  useEffect(() => {
    if (!fieldForm.getValues("type")) {
      return;
    }

    const variantsConfig = getVariantsConfig({
      type: fieldForm.getValues("type"),
      variantsConfig: fieldForm.getValues("variantsConfig"),
    });

    // We need to set the variantsConfig in the RHF instead of using a derived value because RHF won't have the variantConfig for the variant that's not rendered yet.
    fieldForm.setValue("variantsConfig", variantsConfig);
  }, [fieldForm]);
  const isFieldEditMode = !!dialog.data;
  const fieldType = fieldTypesConfigMap[fieldForm.watch("type") || "text"];

  const variantsConfig = fieldForm.watch("variantsConfig");

  const fieldTypes = Object.values(fieldTypesConfigMap);

  return (
    <Dialog open={dialog.isOpen} onOpenChange={onOpenChange}>
      <DialogContent className="max-h-none p-0" data-testid="edit-field-dialog">
        <Form id="form-builder" form={fieldForm} handleSubmit={handleSubmit}>
          <div className="h-auto max-h-[85vh] overflow-auto px-8 pb-7 pt-8">
            <DialogHeader title={t("add_a_booking_question")} subtitle={t("booking_questions_description")} />
            <SelectField
              defaultValue={fieldTypesConfigMap.text}
              id="test-field-type"
              isDisabled={
                fieldForm.getValues("editable") === "system" ||
                fieldForm.getValues("editable") === "system-but-optional"
              }
              onChange={(e) => {
                const value = e?.value;
                if (!value) {
                  return;
                }
                fieldForm.setValue("type", value);
              }}
              value={fieldTypesConfigMap[fieldForm.getValues("type")]}
              options={fieldTypes.filter((f) => !f.systemOnly)}
              label={t("input_type")}
              classNames={{
                menuList: () => "min-h-[27.25rem]",
              }}
            />
            {(() => {
              if (!variantsConfig) {
                return (
                  <>
                    <InputField
                      required
                      {...fieldForm.register("name")}
                      containerClassName="mt-6"
                      onChange={(e) => {
                        fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""));
                      }}
                      disabled={
                        fieldForm.getValues("editable") === "system" ||
                        fieldForm.getValues("editable") === "system-but-optional"
                      }
                      label={t("identifier")}
                    />
                    <InputField
                      {...fieldForm.register("label")}
                      // System fields have a defaultLabel, so there a label is not required
                      required={
                        !["system", "system-but-optional"].includes(fieldForm.getValues("editable") || "")
                      }
                      placeholder={t(fieldForm.getValues("defaultLabel") || "")}
                      containerClassName="mt-6"
                      label={t("label")}
                    />
                    {fieldType?.isTextType ? (
                      <InputField
                        {...fieldForm.register("placeholder")}
                        containerClassName="mt-6"
                        label={t("placeholder")}
                        placeholder={t(fieldForm.getValues("defaultPlaceholder") || "")}
                      />
                    ) : null}
                    {fieldType?.needsOptions && !fieldForm.getValues("getOptionsAt") ? (
                      <Controller
                        name="options"
                        render={({ field: { value, onChange } }) => {
                          return <Options onChange={onChange} value={value} className="mt-6" />;
                        }}
                      />
                    ) : null}
                    <Controller
                      name="required"
                      control={fieldForm.control}
                      render={({ field: { value, onChange } }) => {
                        return (
                          <BooleanToggleGroupField
                            data-testid="field-required"
                            disabled={fieldForm.getValues("editable") === "system"}
                            value={value}
                            onValueChange={(val) => {
                              onChange(val);
                            }}
                            label={t("required")}
                          />
                        );
                      }}
                    />
                  </>
                );
              }

              if (!fieldType.isTextType) {
                throw new Error("Variants are currently supported only with text type");
              }

              return <VariantFields variantsConfig={variantsConfig} fieldForm={fieldForm} />;
            })()}
          </div>

          <DialogFooter className="relative rounded px-8" showDivider>
            <DialogClose color="secondary">{t("cancel")}</DialogClose>
            <Button data-testid="field-add-save" type="submit">
              {isFieldEditMode ? t("save") : t("add")}
            </Button>
          </DialogFooter>
        </Form>
      </DialogContent>
    </Dialog>
  );
}

/**
 * Shows the label of the field, taking into account the current variant selected
 */
function FieldLabel({ field }: { field: RhfFormField }) {
  const { t } = useLocale();
  const fieldTypeConfig = fieldTypesConfigMap[field.type];
  const fieldTypeConfigVariantsConfig = fieldTypeConfig?.variantsConfig;
  const fieldTypeConfigVariants = fieldTypeConfigVariantsConfig?.variants;
  const variantsConfig = field.variantsConfig;
  const variantsConfigVariants = variantsConfig?.variants;
  const defaultVariant = fieldTypeConfigVariantsConfig?.defaultVariant;
  if (!fieldTypeConfigVariants || !variantsConfig) {
    if (fieldsThatSupportLabelAsSafeHtml.includes(field.type)) {
      return (
        <span
          dangerouslySetInnerHTML={{
            // Derive from field.label because label might change in b/w and field.labelAsSafeHtml will not be updated.
            __html: markdownToSafeHTML(field.label || "") || t(field.defaultLabel || ""),
          }}
        />
      );
    } else {
      return <span>{field.label || t(field.defaultLabel || "")}</span>;
    }
  }
  const variant = field.variant || defaultVariant;
  if (!variant) {
    throw new Error(
      "Field has `variantsConfig` but no `defaultVariant`" + JSON.stringify(fieldTypeConfigVariantsConfig)
    );
  }
  const label =
    variantsConfigVariants?.[variant as keyof typeof fieldTypeConfigVariants]?.fields?.[0]?.label || "";
  return <span>{t(label)}</span>;
}

function VariantSelector() {
  // Implement a Variant selector for cases when there are more than 2 variants
  return null;
}

function VariantFields({
  fieldForm,
  variantsConfig,
}: {
  fieldForm: UseFormReturn<RhfFormField>;
  variantsConfig: RhfFormField["variantsConfig"];
}) {
  const { t } = useLocale();
  if (!variantsConfig) {
    throw new Error("VariantFields component needs variantsConfig");
  }
  const fieldTypeConfigVariantsConfig = fieldTypesConfigMap[fieldForm.getValues("type")]?.variantsConfig;

  if (!fieldTypeConfigVariantsConfig) {
    throw new Error("Coniguration Issue: FieldType doesn't have `variantsConfig`");
  }

  const variantToggleLabel = t(fieldTypeConfigVariantsConfig.toggleLabel || "");

  const defaultVariant = fieldTypeConfigVariantsConfig.defaultVariant;

  const variantNames = Object.keys(variantsConfig.variants);
  const otherVariants = variantNames.filter((v) => v !== defaultVariant);
  if (otherVariants.length > 1 && variantToggleLabel) {
    throw new Error("More than one other variant. Remove toggleLabel ");
  }
  const otherVariant = otherVariants[0];
  const variantName = fieldForm.watch("variant") || defaultVariant;
  const variantFields = variantsConfig.variants[variantName as keyof typeof variantsConfig].fields;
  /**
   * A variant that has just one field can be shown in a simpler way in UI.
   */
  const isSimpleVariant = variantFields.length === 1;
  const isDefaultVariant = variantName === defaultVariant;
  const supportsVariantToggle = variantNames.length === 2;
  return (
    <>
      {supportsVariantToggle ? (
        <Switch
          checked={!isDefaultVariant}
          label={variantToggleLabel}
          data-testid="variant-toggle"
          onCheckedChange={(checked) => {
            fieldForm.setValue("variant", checked ? otherVariant : defaultVariant);
          }}
          classNames={{ container: "p-2 mt-2 sm:hover:bg-muted rounded" }}
          tooltip={t("Toggle Variant")}
        />
      ) : (
        <VariantSelector />
      )}

      <InputField
        required
        {...fieldForm.register("name")}
        containerClassName="mt-6"
        disabled={
          fieldForm.getValues("editable") === "system" ||
          fieldForm.getValues("editable") === "system-but-optional"
        }
        label={t("identifier")}
      />

      <ul
        className={classNames(
          !isSimpleVariant ? "border-default divide-subtle mt-2 divide-y rounded-md border" : ""
        )}>
        {variantFields.map((f, index) => {
          const rhfVariantFieldPrefix = `variantsConfig.variants.${variantName}.fields.${index}` as const;
          const fieldTypeConfigVariants =
            fieldTypeConfigVariantsConfig.variants[
              variantName as keyof typeof fieldTypeConfigVariantsConfig.variants
            ];
          const appUiFieldConfig =
            fieldTypeConfigVariants.fieldsMap[f.name as keyof typeof fieldTypeConfigVariants.fieldsMap];
          return (
            <li className={classNames(!isSimpleVariant ? "p-4" : "")} key={f.name}>
              {!isSimpleVariant && (
                <Label className="flex justify-between">
                  <span>{`Field ${index + 1}`}</span>
                  <span className="text-muted">{`${fieldForm.getValues("name")}.${f.name}`}</span>
                </Label>
              )}
              <InputField
                {...fieldForm.register(`${rhfVariantFieldPrefix}.label`)}
                value={f.label || ""}
                placeholder={t(appUiFieldConfig?.defaultLabel || "")}
                containerClassName="mt-6"
                label={t("label")}
              />
              <InputField
                {...fieldForm.register(`${rhfVariantFieldPrefix}.placeholder`)}
                key={f.name}
                value={f.placeholder || ""}
                containerClassName="mt-6"
                label={t("placeholder")}
                placeholder={t(appUiFieldConfig?.defaultPlaceholder || "")}
              />

              <Controller
                name={`${rhfVariantFieldPrefix}.required`}
                control={fieldForm.control}
                render={({ field: { onChange } }) => {
                  return (
                    <BooleanToggleGroupField
                      data-testid="field-required"
                      disabled={!appUiFieldConfig?.canChangeRequirability}
                      value={f.required}
                      onValueChange={(val) => {
                        onChange(val);
                      }}
                      label={t("required")}
                    />
                  );
                }}
              />
            </li>
          );
        })}
      </ul>
    </>
  );
}
