"use client";

import type { ChangeEvent, FocusEvent, Ref, RefAttributes } from "react";
import { useCallback, useEffect } from "react";
import { useIMask } from "react-imask";
import type { FactoryOpts, InputMask, InputMaskElement } from "imask";
import type { TextFieldProps } from "../TextField";
import { TextField } from "../TextField";

export type MaskedFieldElementType = HTMLInputElement;

export interface MaskedFieldProps
  extends
    Omit<TextFieldProps, "type" | "ref">,
    RefAttributes<MaskedFieldElementType> {
  mask: FactoryOpts;
  "data-testid"?: string;
  /**
   * If true, onChange and onBlur events will emit the masked (formatted) value.
   * If false (default), events will emit the unmasked (raw) value.
   *
   * @example
   * // TelephoneQuestion uses useMaskedValue={true}
   * // User types: 5551234567
   * // onChange receives: "(555) 123-4567" (formatted for display/storage)
   *
   * @example
   * // CurrencyQuestion uses useMaskedValue={false}
   * // User types: 1234
   * // User sees: "1,234" (with thousands separator)
   * // onChange receives: "1234" (raw number for calculations)
   */
  useMaskedValue?: boolean;
}

export type MaskedFieldRef = Ref<MaskedFieldElementType>;

/**
 * Creates an onChange handler for IMask's onAccept callback.
 *
 * IMask calls onAccept every time the user types and the mask accepts the input.
 * This handler converts IMask's callback into a standard React onChange event.
 *
 * @param onChange - The onChange handler from parent component
 * @param useMaskedValue - Whether to emit the formatted value or raw value
 * @param name - The input's name attribute (for the synthetic event)
 *
 * @example
 * // TelephoneQuestion (US) - useMaskedValue=true
 * // User types: "5551234567"
 * // Emits: "(555) 123-4567" (formatted for display)
 * // Use case: Want to store/display the formatted phone number
 *
 * @example
 * // CurrencyQuestion - useMaskedValue=false
 * // User types: "1234.56"
 * // User sees: "1,234.56" (with formatting)
 * // Emits: "1234.56" (raw value for calculations)
 * // Use case: Need numeric value for backend/calculations
 */
const useAcceptHandler = (
  onChange: MaskedFieldProps["onChange"],
  useMaskedValue: boolean,
  name?: string,
) => {
  return useCallback(
    (maskedValue: string, maskInstance: InputMask<FactoryOpts>) => {
      // Exit early if no onChange handler provided
      if (!onChange) {
        return;
      }

      // IMask provides two values:
      // 1. maskedValue: The formatted value user sees (e.g., "(555) 123-4567")
      // 2. maskInstance.unmaskedValue: The raw value without formatting (e.g., "5551234567")
      const formattedValue = maskedValue;
      const rawValue = maskInstance.unmaskedValue;

      // Choose which value to emit based on useMaskedValue prop
      const valueToEmit = useMaskedValue ? formattedValue : rawValue;

      // Create a synthetic React event that matches the standard onChange signature
      // This allows parent components to handle it like any other input change
      onChange({
        target: { name, value: valueToEmit },
      } as ChangeEvent<HTMLInputElement>);
    },
    [onChange, useMaskedValue, name],
  );
};

/**
 * Creates a ref callback that forwards the input element to both IMask and the parent component.
 *
 * React allows two ways to pass refs:
 * 1. Ref callback function: (element) => { ... }
 * 2. Ref object: { current: element }
 *
 * This hook handles both cases and also ensures IMask gets the element reference it needs.
 *
 * @param imaskRef - IMask's ref (needs the element to apply masking)
 * @param forwardedRef - Parent component's ref (could be function or object)
 *
 * @example
 * // Parent uses callback ref
 * <MaskedField ref={(el) => console.log(el)} />
 *
 * @example
 * // Parent uses useRef
 * const inputRef = useRef(null);
 * <MaskedField ref={inputRef} />
 */
const useCombinedRef = (
  imaskRef: React.MutableRefObject<InputMaskElement | null>,
  forwardedRef?: MaskedFieldRef,
) => {
  return useCallback(
    (element: HTMLInputElement | null) => {
      // First, give the element to IMask so it can apply masking
      imaskRef.current = element;

      // Then forward the element to the parent component's ref
      // Handle both callback refs and ref objects

      // Case 1: Parent passed a callback ref function
      if (typeof forwardedRef === "function") {
        forwardedRef(element);
      }
      // Case 2: Parent passed a ref object (from useRef)
      else if (forwardedRef) {
        forwardedRef.current = element;
      }
      // Case 3: No ref was forwarded (forwardedRef is null/undefined)
      // Do nothing - this is fine
    },
    [imaskRef, forwardedRef],
  );
};

/**
 * Creates an onBlur handler that extracts the correct value from IMask.
 *
 * When the user tabs away or clicks outside the input, we need to emit
 * the current value (either formatted or raw) via the onBlur event.
 *
 * @param onBlur - The onBlur handler from parent component
 * @param maskRef - Reference to the IMask instance (provides current values)
 * @param useMaskedValue - Whether to emit the formatted value or raw value
 * @param name - The input's name attribute (for the synthetic event)
 *
 * @example
 * // TelephoneQuestion - useMaskedValue=true
 * // Input displays: "(555) 123-4567"
 * // onBlur emits: "(555) 123-4567" (formatted)
 *
 * @example
 * // CurrencyQuestion - useMaskedValue=false
 * // Input displays: "$1,234.56"
 * // onBlur emits: "1234.56" (raw for validation/submission)
 */
const useBlurHandler = (
  onBlur: MaskedFieldProps["onBlur"],
  maskRef: React.MutableRefObject<InputMask<FactoryOpts> | null>,
  useMaskedValue: boolean,
  name?: string,
) => {
  return useCallback(
    (event: FocusEvent<Element>) => {
      // Exit early if no onBlur handler provided or mask isn't initialized
      if (!onBlur || !maskRef.current) {
        return;
      }

      // Extract current values from the IMask instance
      // maskRef.current.value: The formatted value (e.g., "(555) 123-4567")
      // maskRef.current.unmaskedValue: The raw value (e.g., "5551234567")
      const formattedValue = maskRef.current.value;
      const rawValue = maskRef.current.unmaskedValue;

      // Choose which value to emit based on useMaskedValue prop
      const valueToEmit = useMaskedValue ? formattedValue : rawValue;

      // Create an enhanced event that preserves the original event properties
      // but overrides the target to include our extracted value
      onBlur({
        ...event,
        target: { ...event.target, name, value: valueToEmit },
      } as FocusEvent<Element>);
    },
    [onBlur, maskRef, useMaskedValue, name],
  );
};

/**
 * Controlled MaskedField - for use with value prop.
 * Syncs external value changes to IMask via useEffect.
 *
 * Use this when:
 * - The parent component manages the input state
 * - You need to programmatically update the value
 * - You need validation or transformation on every change
 */
const ControlledMaskedField = ({
  mask,
  useMaskedValue = false,
  value,
  onChange,
  onBlur,
  name,
  forwardedRef,
  ...textFieldProps
}: Omit<MaskedFieldProps, "defaultValue"> & {
  value: string | number;
  forwardedRef?: MaskedFieldRef;
}) => {
  const onAccept = useAcceptHandler(onChange, useMaskedValue, name);
  const { ref: imaskRef, maskRef, setValue } = useIMask(mask, { onAccept });

  const combinedRef = useCombinedRef(imaskRef, forwardedRef);
  const handleBlur = useBlurHandler(onBlur, maskRef, useMaskedValue, name);

  // Sync external value changes to IMask
  useEffect(() => {
    if (!maskRef.current) {
      return;
    }

    const stringValue = value.toString();
    const currentMasked = maskRef.current.value;
    const currentUnmasked = maskRef.current.unmaskedValue;

    // Only update if value actually changed (prevents infinite loops)
    if (currentMasked !== stringValue && currentUnmasked !== stringValue) {
      setValue(stringValue);
    }
  }, [value, maskRef, setValue, imaskRef]);

  return (
    <TextField
      {...textFieldProps}
      ref={combinedRef}
      name={name}
      onBlur={handleBlur}
    />
  );
};

/**
 * Uncontrolled MaskedField - for use with defaultValue prop.
 * No value synchronization needed - IMask manages the value internally.
 *
 * Use this when:
 * - You only need the final value on submit
 * - You don't need to react to intermediate changes
 * - You want simpler code without state management
 */
const UncontrolledMaskedField = ({
  mask,
  useMaskedValue = false,
  defaultValue,
  onChange,
  onBlur,
  name,
  forwardedRef,
  ...textFieldProps
}: Omit<MaskedFieldProps, "value"> & {
  defaultValue?: string | number;
  forwardedRef?: MaskedFieldRef;
}) => {
  const onAccept = useAcceptHandler(onChange, useMaskedValue, name);
  const { ref: imaskRef, maskRef } = useIMask(mask, { onAccept });

  const combinedRef = useCombinedRef(imaskRef, forwardedRef);
  const handleBlur = useBlurHandler(onBlur, maskRef, useMaskedValue, name);

  return (
    <TextField
      {...textFieldProps}
      ref={combinedRef}
      name={name}
      onBlur={handleBlur}
      defaultValue={defaultValue?.toString()}
    />
  );
};

const MaskedField = ({ ref: forwardedRef, ...props }: MaskedFieldProps) => {
  const { value, defaultValue, ...rest } = props;

  // Check for "value" in props (not `value !== undefined`) to maintain consistent
  // controlled/uncontrolled state throughout component lifetime and prevent focus loss.
  // Example: CurrencyQuestion passes value={undefined} initially, then value={5} after typing.
  if ("value" in props) {
    return (
      <ControlledMaskedField
        {...rest}
        value={value ?? ""}
        forwardedRef={forwardedRef}
      />
    );
  } else {
    return (
      <UncontrolledMaskedField
        {...rest}
        defaultValue={defaultValue}
        forwardedRef={forwardedRef}
      />
    );
  }
};

MaskedField.displayName = "MaskedField";

export { MaskedField };
