import clsx from "clsx";
import * as React from "react";

import { useGetSet } from "../../hooks";
import { useGetKey } from "../../hooks/useGetKey";
import { useDebugEvents } from "../../utils";
import { composeEventHandlers } from "../../utils/composeEvents";
import { useComposedRefs } from "../../utils/mergeRefs";
import * as styles from "./styles.module.css";

export interface InputProps
  extends Omit<React.ComponentPropsWithoutRef<"input">, "size"> {
  "data-id": string;
  type: MODES;
  "data-label"?: string;
  hideControls?: boolean;
  size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  variant?: "subtle" | "outline" | "flushed";
}

export enum MODES {
  text = "text",
  number = "number",
  email = "email",
  password = "password",
  phone = "tel",
  date = "date",
  "datetime-local" = "datetime-local",
  time = "time",
  search = "search",
  hidden = "hidden",
  url = "url",
  otp = "one-time-code",
}

function getTypeProps(
  type: MODES = MODES.text,
): Partial<React.ComponentProps<"input">> {
  switch (type) {
    case MODES.number:
      return {
        type: "number",
        pattern: "[0-9]*",
        inputMode: "numeric",
      };
    case MODES.otp:
      return {
        type: "text",
        pattern: "[0-9]*",
        inputMode: "numeric",
        autoComplete: "one-time-code",
      };
    case MODES.email:
      return {
        type: "email",
        inputMode: "email",
      };
    case MODES.password:
      return {
        type: "password",
        autoComplete: "current-password",
      };
    default:
      return {
        type,
      };
  }
}

export const Input = React.forwardRef(function Input(
  {
    defaultValue,
    onChange,
    onFocus,
    onBlur,
    size = "md",
    variant = "outline",
    ...rest
  }: InputProps,
  ref: React.ForwardedRef<HTMLInputElement>,
) {
  const inputRef = React.useRef<HTMLInputElement>(null);
  const key = useGetKey(rest);
  const initialValue = React.useMemo(
    () => ({ value: defaultValue as string }),
    [defaultValue],
  );
  const [{ value }, setState] = useGetSet<{ value: string; focused?: boolean }>(
    key,
    initialValue,
  );
  const {
    className,
    onChange: handleChange,
    type,
    ...props
  } = useDebugEvents<Omit<InputProps, "size">>(
    Object.assign(rest, {
      onFocus: composeEventHandlers(() => {
        setState(
          { focused: true },
          process.env.PREVIEW ? `onFocus` : undefined,
        );
      }, onFocus),
      onChange: composeEventHandlers(
        (event: React.ChangeEvent<HTMLInputElement>) => {
          const value = event.target.value;
          setState({ value }, process.env.PREVIEW ? `onChange` : undefined);
        },
        onChange,
      ),
      onBlur: composeEventHandlers(() => {
        setState(
          { focused: false },
          process.env.PREVIEW ? `onBlur` : undefined,
        );
      }, onBlur),
    }),
  );

  React.useEffect(() => {
    const parentForm = inputRef.current?.form;
    const handler = () => {
      setState({ value: defaultValue as string });
    };
    parentForm?.addEventListener("reset", handler);
    return () => {
      parentForm?.removeEventListener("reset", handler);
    };
  }, []);

  const finalRef = useComposedRefs(inputRef, ref);

  return (
    <input
      data-component="Input$Brevity"
      {...props}
      {...getTypeProps(type)}
      data-size={size}
      data-variant={variant}
      ref={finalRef}
      data-id={key}
      className={clsx("input input-text", styles.input, className)}
      value={(value as string) ?? ""}
      onChange={handleChange}
    />
  );
});

export const NumberInput = React.forwardRef(function NumberInput(
  {
    defaultValue,
    hideControls,
    onChange,
    onFocus,
    onBlur,
    size = "md",
    variant = "outline",
    ...rest
  }: InputProps,
  ref: React.ForwardedRef<HTMLInputElement>,
) {
  const inputRef = React.useRef<HTMLInputElement>(null);
  const key = useGetKey(rest);
  const initialValue = React.useMemo(
    () => ({ value: defaultValue as number }),
    [defaultValue],
  );
  const [{ value }, setState] = useGetSet<{
    value: number | "";
    focused?: boolean;
  }>(key, initialValue);
  const {
    className,
    onChange: handleChange,
    ...props
  } = useDebugEvents<Omit<InputProps, "size">>(
    Object.assign(rest, {
      onFocus: composeEventHandlers(() => {
        setState(
          { focused: true },
          process.env.PREVIEW ? `onFocus` : undefined,
        );
      }, onFocus),
      onChange: composeEventHandlers(
        (event: React.ChangeEvent<HTMLInputElement>) => {
          const value = event.target.valueAsNumber;
          setState(
            { value: isNaN(value) ? "" : value },
            process.env.PREVIEW ? `onChange` : undefined,
          );
        },
        onChange,
      ),
      onBlur: composeEventHandlers(() => {
        setState(
          { focused: false },
          process.env.PREVIEW ? `onBlur` : undefined,
        );
      }, onBlur),
    }),
  );

  React.useEffect(() => {
    const parentForm = inputRef.current?.form;
    const handler = () => {
      setState({ value: defaultValue as number });
    };
    parentForm?.addEventListener("reset", handler);
    return () => {
      parentForm?.removeEventListener("reset", handler);
    };
  }, []);

  const finalRef = useComposedRefs(inputRef, ref);

  return (
    <input
      data-component="NumberInput$Brevity"
      {...props}
      {...getTypeProps(MODES.number)}
      ref={finalRef}
      data-id={key}
      data-size={size}
      data-variant={variant}
      className={clsx(
        "input input-number",
        styles.input,
        className,
        hideControls && styles.hideControls,
      )}
      value={(value as number) ?? ""}
      onChange={handleChange}
    />
  );
});

function isValidDate(date: any) {
  return date instanceof Date && !isNaN(date.getTime());
}

const formatDate = (value?: Date | null, type: MODES = MODES.date): string => {
  if (!value) return "";
  switch (type) {
    case MODES.time:
      return value && isValidDate(value)
        ? new Date(
            Date.UTC(
              value.getFullYear(),
              value.getMonth(),
              value.getDate(),
              value.getHours(),
              value.getMinutes(),
              0,
              0,
            ),
          )
            .toISOString()
            .slice(11, 16)
        : "";
    case MODES["datetime-local"]:
      return value && isValidDate(value)
        ? new Date(
            Date.UTC(
              value.getFullYear(),
              value.getMonth(),
              value.getDate(),
              value.getHours(),
              value.getMinutes(),
              0,
              0,
            ),
          )
            .toISOString()
            .slice(0, 16)
        : "";
    case MODES.date:
    default:
      return value && isValidDate(value)
        ? new Date(
            Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()),
          )
            .toISOString()
            .slice(0, 10)
        : "";
  }
};
const dateRE = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
const datetimeRE =
  /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9])$/;
const timeRE = /^([01][0-9]|2[0-3]):([0-5][0-9])$/;

export const DateInput = React.forwardRef(function DateInput(
  {
    defaultValue,
    onChange,
    onFocus,
    onBlur,
    size = "md",
    variant = "outline",
    ...rest
  }: InputProps,
  ref: React.ForwardedRef<HTMLInputElement>,
) {
  const key = useGetKey(rest);
  const inputRef = React.useRef<HTMLInputElement>(null);
  const isUserInputRef = React.useRef(false); // Add this ref to track user input

  const initialValue = React.useMemo(() => {
    return {
      value:
        defaultValue && typeof defaultValue === "string"
          ? new Date(defaultValue)
          : defaultValue != null
            ? (defaultValue as unknown as Date)
            : null,
    };
  }, [defaultValue]);

  const [{ value }, setState] = useGetSet<{
    value: Date | null;
    focused?: boolean;
  }>(key, initialValue);

  const [strValue, setStrValue] = React.useState(() =>
    formatDate(value, rest.type ?? MODES.date),
  );

  const {
    className,
    onChange: handleChange,
    type = MODES.date,
    ...props
  } = useDebugEvents<Omit<InputProps, "size">>(
    Object.assign(rest, {
      onFocus: composeEventHandlers(() => {
        setState(
          { focused: true },
          process.env.PREVIEW ? `onFocus` : undefined,
        );
      }, onFocus),
      onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
        const valueStr = event.target.value;
        isUserInputRef.current = true; // Mark as user input
        setStrValue(valueStr);

        switch (type) {
          case MODES.date:
            if (dateRE.test(valueStr)) {
              const isoDate = event.target.valueAsDate;
              if (!isoDate) {
                return;
              }
              const year = isoDate.getUTCFullYear();
              if (year < 1000 || year > 9999) {
                return;
              }
              setState(
                {
                  value: new Date(
                    year,
                    isoDate.getUTCMonth(),
                    isoDate.getUTCDate(),
                  ),
                },
                process.env.PREVIEW ? `onChange` : undefined,
              );
              onChange?.(event);
            }
            break;
          case MODES["datetime-local"]:
            if (datetimeRE.test(valueStr)) {
              const isoDate = new Date(`${valueStr}:00.000Z`);
              setState(
                {
                  value: new Date(
                    isoDate.getFullYear(),
                    isoDate.getUTCMonth(),
                    isoDate.getUTCDate(),
                    isoDate.getUTCHours(),
                    isoDate.getUTCMinutes(),
                    0,
                    0,
                  ),
                },
                process.env.PREVIEW ? `onChange` : undefined,
              );
              onChange?.(event);
            }
            break;
          case MODES.time:
            if (timeRE.test(valueStr)) {
              const dateStr = (
                value && isValidDate(value) ? new Date(value) : new Date()
              ).toISOString();
              const isoStr = `${dateStr.slice(0, 10)}T${valueStr}:00.000Z`;
              const isoDate = new Date(isoStr);
              setState(
                {
                  value: new Date(
                    isoDate.getFullYear(),
                    isoDate.getUTCMonth(),
                    isoDate.getUTCDate(),
                    isoDate.getUTCHours(),
                    isoDate.getUTCMinutes(),
                    0,
                    0,
                  ),
                },
                process.env.PREVIEW ? `onChange` : undefined,
              );
              onChange?.(event);
            }
            break;
          default:
            return;
        }
      },
      onBlur: composeEventHandlers(() => {
        setState(
          { focused: false },
          process.env.PREVIEW ? `onBlur` : undefined,
        );
      }, onBlur),
    }),
  );

  // Only update strValue from external value changes, not user input
  React.useEffect(() => {
    if (isUserInputRef.current) {
      isUserInputRef.current = false; // Reset the flag
      return;
    }

    const formatted = formatDate(value, type);
    if (inputRef.current?.value !== formatted) {
      setStrValue(formatted);
    }
  }, [value, type]);

  React.useEffect(() => {
    const parentForm = inputRef.current?.form;
    const handler = () => {
      setState({
        value:
          defaultValue && typeof defaultValue === "string"
            ? new Date(defaultValue)
            : (defaultValue as unknown as Date),
      });
    };
    parentForm?.addEventListener("reset", handler);
    return () => {
      parentForm?.removeEventListener("reset", handler);
    };
  }, []);

  const finalRef = useComposedRefs(inputRef, ref);

  return (
    <input
      data-component="DateInput$Brevity"
      {...props}
      {...getTypeProps(type)}
      data-id={key}
      data-size={size}
      data-variant={variant}
      ref={finalRef}
      className={clsx("input input-date", styles.input, className)}
      value={strValue}
      onChange={handleChange}
    />
  );
});
