import ObjectID from "../../../Types/ObjectID";
import React, {
  FunctionComponent,
  ReactElement,
  useLayoutEffect,
  useRef,
  useState,
} from "react";
import Color from "../../../Types/Color";
import Label from "../../../Models/DatabaseModels/Label";
import Select, {
  ControlProps,
  CSSObjectWithLabel,
  FormatOptionLabelMeta,
  GroupBase,
  OptionProps,
} from "react-select";

export type DropdownValue = string | number | boolean;

export type DropdownOptionLabel =
  | Label
  | {
      id?: string;
      name: string;
      color?: Color;
    };

export interface DropdownOption {
  value: DropdownValue;
  label: string;
  description?: string;
  labels?: Array<DropdownOptionLabel>;
  color?: Color;
}

export interface ComponentProps {
  options: Array<DropdownOption>;
  initialValue?: undefined | DropdownOption | Array<DropdownOption>;
  onClick?: undefined | (() => void);
  placeholder?: undefined | string;
  className?: undefined | string;
  onChange?:
    | undefined
    | ((value: DropdownValue | Array<DropdownValue> | null) => void);
  value?: DropdownOption | Array<DropdownOption> | undefined;
  onFocus?: (() => void) | undefined;
  onBlur?: (() => void) | undefined;
  isMultiSelect?: boolean;
  tabIndex?: number | undefined;
  error?: string | undefined;
  id?: string | undefined;
  dataTestId?: string | undefined;
}

const Dropdown: FunctionComponent<ComponentProps> = (
  props: ComponentProps,
): ReactElement => {
  type GetDropdownOptionFromValueFunctionProps =
    | undefined
    | DropdownValue
    | DropdownOption
    | Array<DropdownOption>
    | Array<DropdownValue>;

  type GetDropdownOptionFromValueFunction = (
    value: GetDropdownOptionFromValueFunctionProps,
  ) => DropdownOption | Array<DropdownOption> | undefined;

  const getDropdownOptionFromValue: GetDropdownOptionFromValueFunction = (
    value: GetDropdownOptionFromValueFunctionProps,
  ): DropdownOption | Array<DropdownOption> | undefined => {
    if (value === undefined) {
      return undefined;
    }

    if (value instanceof ObjectID) {
      value = value.toString();
    }

    if (
      Array.isArray(value) &&
      value.length > 0 &&
      Object.keys(value[0]!).includes("value")
    ) {
      return value as Array<DropdownOption>;
    }

    if (
      Array.isArray(value) &&
      value.length > 0 &&
      (typeof value[0] === "string" || typeof value[0] === "number")
    ) {
      const options: Array<DropdownOption> = [];

      for (const item of value as Array<DropdownValue>) {
        if (
          !Array.isArray(item) &&
          (typeof item === "string" || typeof item === "number")
        ) {
          const option: DropdownOption | undefined | Array<DropdownOption> =
            props.options.find((option: DropdownOption) => {
              return option.value === item;
            }) as DropdownOption | Array<DropdownOption>;

          if (option) {
            options.push(option as DropdownOption);
          }
        }
      }

      return options;
    }

    if (
      !Array.isArray(value) &&
      (typeof value === "string" || typeof value === "number")
    ) {
      return props.options.find((option: DropdownOption) => {
        return option.value === value;
      }) as DropdownOption | Array<DropdownOption>;
    }

    return value as DropdownOption | Array<DropdownOption>;
  };

  const [value, setValue] = useState<
    DropdownOption | Array<DropdownOption> | undefined
  >(getDropdownOptionFromValue(props.initialValue));

  const firstUpdate: React.MutableRefObject<boolean> = useRef(true);

  interface NormalizedDropdownLabel {
    id?: string;
    name: string;
    color?: string;
  }

  const normalizeLabelColor: (
    color?: Color | string | null,
  ) => string | undefined = (
    color?: Color | string | null,
  ): string | undefined => {
    if (!color) {
      return undefined;
    }

    if (color instanceof Color) {
      return color.toString();
    }

    if (typeof color === "string" && color.trim().length > 0) {
      return color;
    }

    return undefined;
  };

  const normalizeDropdownLabel: (
    label: DropdownOptionLabel,
  ) => NormalizedDropdownLabel | null = (
    label: DropdownOptionLabel,
  ): NormalizedDropdownLabel | null => {
    if (!label) {
      return null;
    }

    const getValueFromModel: (
      columnName: string,
    ) => string | Color | null | undefined = (
      columnName: string,
    ): string | Color | null | undefined => {
      if (
        typeof (label as Label).getColumnValue === "function" &&
        typeof (label as Label).getTableColumnMetadata === "function"
      ) {
        return (label as Label).getColumnValue(columnName) as
          | string
          | Color
          | null
          | undefined;
      }

      return (label as any)?.[columnName] as string | Color | null | undefined;
    };

    const labelName: string | undefined = (() => {
      const valueFromGetter: string | null | undefined = getValueFromModel(
        "name",
      ) as string | undefined | null;

      if (valueFromGetter && valueFromGetter.trim().length > 0) {
        return valueFromGetter;
      }

      const fallbackName: string | undefined = (label as any)?.name;
      if (fallbackName && fallbackName.trim().length > 0) {
        return fallbackName;
      }

      return undefined;
    })();

    if (!labelName) {
      return null;
    }

    const rawColor: Color | string | null | undefined = getValueFromModel(
      "color",
    ) as Color | string | null | undefined;
    const color: string | undefined =
      normalizeLabelColor(rawColor) ||
      normalizeLabelColor((label as any)?.color);

    const idValue: string | undefined = (() => {
      if (typeof (label as Label).id !== "undefined") {
        const idFromGetter: ObjectID | null | undefined = (label as Label).id;
        if (idFromGetter) {
          return idFromGetter.toString();
        }
      }

      const fallbackId: string | undefined =
        (label as any)?._id || (label as any)?.id;

      if (fallbackId) {
        return fallbackId;
      }

      return undefined;
    })();

    const normalized: NormalizedDropdownLabel = {
      name: labelName,
    };

    if (idValue) {
      normalized.id = idValue;
    }

    if (color) {
      normalized.color = color;
    }

    return normalized;
  };

  const normalizeLabelCollection: (
    labels?: Array<DropdownOptionLabel>,
  ) => Array<NormalizedDropdownLabel> = (
    labels?: Array<DropdownOptionLabel>,
  ): Array<NormalizedDropdownLabel> => {
    if (!labels || labels.length === 0) {
      return [];
    }

    return labels
      .map((label: DropdownOptionLabel) => {
        return normalizeDropdownLabel(label);
      })
      .filter(
        (
          label: NormalizedDropdownLabel | null,
        ): label is NormalizedDropdownLabel => {
          return label !== null;
        },
      );
  };

  const renderOptionColorIndicator: (
    color?: Color | string,
  ) => ReactElement | null = (color?: Color | string): ReactElement | null => {
    const normalizedColor: string | undefined = color
      ? new Color(color).toString()
      : undefined;

    if (!normalizedColor) {
      return null;
    }

    return (
      <span
        aria-hidden="true"
        className="h-2.5 w-2.5 flex-none rounded-full border border-gray-200"
        style={{
          backgroundColor: normalizedColor,
        }}
        title={normalizedColor}
      ></span>
    );
  };

  const getLabelStyle: (color?: string) => {
    backgroundColor: string;
    color: string;
  } = (color?: string): { backgroundColor: string; color: string } => {
    if (!color) {
      return {
        backgroundColor: "#e5e7eb", // gray-200
        color: "#374151", // gray-700
      };
    }

    try {
      const parsedColor: Color = Color.fromString(color);
      return {
        backgroundColor: parsedColor.toString(),
        color: Color.shouldUseDarkText(parsedColor)
          ? "#111827" // gray-900
          : "#f9fafb", // gray-50
      };
    } catch {
      return {
        backgroundColor: color,
        color: "#111827",
      };
    }
  };

  const defaultSelectedLabelAccentColor: string = "#6366f1"; // indigo-500

  const resolveSelectedLabelColor: (color?: string) => string = (
    color?: string,
  ): string => {
    if (!color) {
      return defaultSelectedLabelAccentColor;
    }

    try {
      return Color.fromString(color).toString();
    } catch {
      return defaultSelectedLabelAccentColor;
    }
  };

  const renderAssociatedLabels: (
    labels: Array<NormalizedDropdownLabel>,
    context: FormatOptionLabelMeta<DropdownOption>["context"],
    hiddenLabelCount: number,
  ) => ReactElement | null = (
    labels: Array<NormalizedDropdownLabel>,
    context: FormatOptionLabelMeta<DropdownOption>["context"],
    hiddenLabelCount: number,
  ): ReactElement | null => {
    if (labels.length === 0 && hiddenLabelCount === 0) {
      return null;
    }

    if (context === "value") {
      return (
        <div className="flex flex-wrap items-center gap-1">
          {labels.map((label: NormalizedDropdownLabel, index: number) => {
            const accentColor: string = resolveSelectedLabelColor(label.color);

            return (
              <span
                key={`${label.id || label.name}-selected-${index}`}
                className="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-600"
                style={{
                  borderColor: accentColor,
                }}
                title={label.name}
              >
                <span
                  aria-hidden="true"
                  className="h-1.5 w-1.5 rounded-full"
                  style={{
                    backgroundColor: accentColor,
                  }}
                ></span>
                {label.name}
              </span>
            );
          })}
          {hiddenLabelCount > 0 ? (
            <span className="inline-flex items-center rounded-full border border-gray-200 bg-white px-2 py-0.5 text-xs font-medium text-gray-500">
              +{hiddenLabelCount}
            </span>
          ) : null}
        </div>
      );
    }

    return (
      <div className="flex flex-wrap gap-1">
        {labels.map((label: NormalizedDropdownLabel, index: number) => {
          const { backgroundColor, color } = getLabelStyle(label.color);

          return (
            <span
              key={`${label.id || label.name}-menu-${index}`}
              className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium shadow-sm"
              style={{ backgroundColor, color }}
            >
              {label.name}
            </span>
          );
        })}
        {hiddenLabelCount > 0 ? (
          <span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
            +{hiddenLabelCount}
          </span>
        ) : null}
      </div>
    );
  };

  const formatDropdownOptionLabel: (
    option: DropdownOption,
    meta: FormatOptionLabelMeta<DropdownOption>,
  ) => ReactElement = (
    option: DropdownOption,
    meta: FormatOptionLabelMeta<DropdownOption>,
  ): ReactElement => {
    const normalizedLabels: Array<NormalizedDropdownLabel> =
      normalizeLabelCollection(option.labels);

    const maxVisibleLabels: number = meta.context === "menu" ? 4 : 2;
    const visibleLabels: Array<NormalizedDropdownLabel> =
      normalizedLabels.slice(0, maxVisibleLabels);
    const hiddenLabelCount: number = Math.max(
      normalizedLabels.length - visibleLabels.length,
      0,
    );

    if (meta.context === "value") {
      return (
        <div className="flex flex-wrap items-center gap-2">
          <div className="flex items-center gap-2">
            {renderOptionColorIndicator(option.color)}
            <span className="text-sm font-medium text-gray-900">
              {option.label}
            </span>
          </div>
          {renderAssociatedLabels(
            visibleLabels,
            meta.context,
            hiddenLabelCount,
          )}
        </div>
      );
    }

    return (
      <div className="flex flex-col gap-1">
        <div className="flex items-center gap-2">
          {renderOptionColorIndicator(option.color)}
          <span className="text-sm font-medium text-gray-900">
            {option.label}
          </span>
        </div>
        {option.description ? (
          <span className="text-xs text-gray-500">{option.description}</span>
        ) : null}
        {renderAssociatedLabels(visibleLabels, meta.context, hiddenLabelCount)}
      </div>
    );
  };

  useLayoutEffect(() => {
    if (firstUpdate.current && props.initialValue) {
      firstUpdate.current = false;
      return;
    }

    const value: DropdownOption | Array<DropdownOption> | undefined =
      getDropdownOptionFromValue(
        props.value === null ? undefined : props.value,
      );

    setValue(value);
  }, [props.value]);

  return (
    <div
      id={props.id}
      className={`${
        props.className ||
        "relative mt-2 mb-1 rounded-md w-full overflow-visible"
      }`}
      onClick={() => {
        props.onClick?.();
        props.onFocus?.();
      }}
    >
      <Select
        classNamePrefix="ou-select"
        unstyled={false}
        formatOptionLabel={formatDropdownOptionLabel}
        onBlur={() => {
          props.onBlur?.();
        }}
        data-testid={props.dataTestId}
        tabIndex={props.tabIndex}
        isMulti={props.isMultiSelect}
        value={value || null}
        onFocus={() => {
          props.onFocus?.();
        }}
        classNames={{
          control: (
            state: ControlProps<DropdownOption, boolean, GroupBase<any>>,
          ): string => {
            const classes: Array<string> = [
              "!min-h-[40px] !rounded-lg !border !bg-white !shadow-sm !transition-all !duration-150",
              state.isFocused
                ? "!border-indigo-400 !ring-2 !ring-indigo-100"
                : "!border-gray-300 hover:!border-indigo-300",
              state.isDisabled
                ? "!bg-gray-100 !text-gray-400"
                : "!cursor-pointer",
            ];

            if (props.error) {
              classes.push("!border-red-400 !ring-2 !ring-red-100");
            }

            return classes.join(" ");
          },
          valueContainer: () => {
            return "!gap-2 !px-2";
          },
          placeholder: () => {
            return "text-sm text-gray-400";
          },
          input: () => {
            return "text-sm text-gray-900";
          },
          singleValue: () => {
            return "text-sm text-gray-900 font-medium";
          },
          indicatorsContainer: () => {
            return "!gap-1 !px-1";
          },
          dropdownIndicator: () => {
            return "text-gray-500 transition-colors duration-150 hover:text-indigo-400";
          },
          clearIndicator: () => {
            return "text-gray-400 transition-colors duration-150 hover:text-red-500";
          },
          menu: () => {
            return "!mt-2 !rounded-xl !border !border-gray-100 !bg-white !shadow-xl";
          },
          menuList: () => {
            return "!py-2";
          },
          option: (
            state: OptionProps<DropdownOption, boolean, GroupBase<any>>,
          ): string => {
            if (state.isDisabled) {
              return "px-3 py-2 text-sm text-gray-300 cursor-not-allowed";
            }

            if (state.isSelected) {
              return "px-3 py-2 text-sm bg-indigo-200 text-indigo-900";
            }

            if (state.isFocused) {
              return "px-3 py-2 text-sm bg-indigo-100 text-indigo-700";
            }

            return "px-3 py-2 text-sm text-gray-700";
          },
          noOptionsMessage: () => {
            return "px-3 py-2 text-sm text-gray-500";
          },
          multiValue: () => {
            return "flex items-center gap-2 rounded-lg border border-indigo-100 bg-indigo-50 px-2 py-1";
          },
          multiValueLabel: () => {
            return "flex flex-wrap items-center gap-2 text-sm font-medium text-indigo-900";
          },
          multiValueRemove: () => {
            return "text-indigo-400 hover:text-indigo-600 transition-colors duration-150";
          },
        }}
        styles={{
          dropdownIndicator: (
            provided: CSSObjectWithLabel,
          ): CSSObjectWithLabel => {
            return {
              ...provided,
              padding: 8,
            };
          },
          clearIndicator: (
            provided: CSSObjectWithLabel,
          ): CSSObjectWithLabel => {
            return {
              ...provided,
              padding: 8,
            };
          },
          indicatorSeparator: (): CSSObjectWithLabel => {
            return {
              display: "none",
            } as CSSObjectWithLabel;
          },
          option: (
            provided: CSSObjectWithLabel,
            state: OptionProps<
              DropdownOption,
              boolean,
              GroupBase<DropdownOption>
            >,
          ): CSSObjectWithLabel => {
            if (state.isSelected) {
              return {
                ...provided,
                backgroundColor: "#c7d2fe", // indigo-200
                color: "#1e1b4b", // indigo-900
              };
            }

            if (state.isFocused) {
              return {
                ...provided,
                backgroundColor: "#e0e7ff", // indigo-100
                color: "#312e81", // indigo-800
              };
            }

            return {
              ...provided,
              color: "#374151", // gray-700
            };
          },
          multiValue: (provided: CSSObjectWithLabel): CSSObjectWithLabel => {
            return {
              ...provided,
              backgroundColor: "#eef2ff", // indigo-50
              borderRadius: 8,
              border: "1px solid #c7d2fe", // indigo-200
              paddingLeft: 4,
              paddingRight: 4,
            };
          },
          multiValueLabel: (
            provided: CSSObjectWithLabel,
          ): CSSObjectWithLabel => {
            return {
              ...provided,
              color: "#312e81", // indigo-800
              fontSize: "0.875rem",
              fontWeight: 500,
            };
          },
          multiValueRemove: (
            provided: CSSObjectWithLabel,
          ): CSSObjectWithLabel => {
            return {
              ...provided,
              color: "#6366f1", // indigo-500
              ":hover": {
                color: "#4f46e5", // indigo-600
                backgroundColor: "transparent",
              },
            };
          },
          menuPortal: (base: CSSObjectWithLabel): CSSObjectWithLabel => {
            return {
              ...base,
              zIndex: 50,
            };
          },
        }}
        isClearable={true}
        isSearchable={true}
        placeholder={props.placeholder}
        options={props.options as any}
        onChange={(option: any | null) => {
          if (option) {
            if (props.isMultiSelect) {
              const value: Array<DropdownOption> =
                option as Array<DropdownOption>;
              setValue(value);

              props.onChange?.(
                value.map((i: DropdownOption) => {
                  return i.value;
                }),
              );
            } else {
              const value: DropdownOption = option as DropdownOption;
              setValue(value);
              props.onChange?.(value.value);
            }
          }

          if (option === null && props.isMultiSelect) {
            setValue([]);
            props.onChange?.([]);
          }

          if (option === null && !props.isMultiSelect) {
            setValue(undefined);
            props.onChange?.(null);
          }
        }}
      />
      {props.error && (
        <p data-testid="error-message" className="mt-1 text-sm text-red-400">
          {props.error}
        </p>
      )}
    </div>
  );
};

export default Dropdown;
