import { clsx } from 'clsx';
import { useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';

import Body from '../body';
import { Input } from '../inputs/Input';
import {
  SelectInput,
  SelectInputOptionContent,
  type SelectInputProps,
} from '../inputs/SelectInput';
import {
  DateMode,
  MonthFormat,
  Size,
  SizeLarge,
  SizeMedium,
  SizeSmall,
  Typography,
} from '../common';
import { MDY, YMD, getMonthNames, isDateValid, isMonthAndYearFormat } from '../common/dateUtils';
import { useFieldLabelRef, useInputAttributes } from '../inputs/contexts';
import messages from './DateInput.messages';
import { convertToLocalMidnight } from './utils';

export interface DateInputProps {
  /** @deprecated Use `Field` wrapper or the `aria-labelledby` attribute instead. */
  'aria-label'?: string;
  'aria-labelledby'?: string;
  /** @default false */
  disabled?: boolean;
  /** @default 'md' */
  size?: SizeSmall | SizeMedium | SizeLarge;
  value?: Date | string;
  onChange: (value: string | null) => void;
  onFocus?: React.FocusEventHandler<HTMLDivElement | HTMLFieldSetElement>;
  onBlur?: React.FocusEventHandler<HTMLDivElement | HTMLFieldSetElement>;
  dayLabel?: string;
  dayAutoComplete?: string;
  monthLabel?: string;
  yearLabel?: string;
  yearAutoComplete?: string;
  /** @default 'long' */
  monthFormat?: `${MonthFormat}`;
  /** @default 'day-month-year' */
  mode?: `${DateMode}`;
  placeholders?: {
    day?: string;
    month?: string;
    year?: string;
  };
  id?: string;
  /** @default {} */
  selectProps?: Partial<SelectInputProps<number | null>>;
}

/**
 * To be passed to SelectInput's parentId prop for correct blur handling.
 */
const DATE_INPUT_PARENT_ID = 'dateInput';

const DateInput = ({
  'aria-labelledby': ariaLabelledByProp,
  'aria-label': ariaLabel,
  disabled = false,
  size = Size.MEDIUM,
  value,
  dayLabel,
  dayAutoComplete,
  monthLabel,
  yearLabel,
  yearAutoComplete,
  monthFormat = MonthFormat.LONG,
  mode = DateMode.DAY_MONTH_YEAR,
  onChange,
  onFocus,
  onBlur,
  placeholders,
  id: idProp,
  selectProps = {},
}: DateInputProps) => {
  const inputAttributes = useInputAttributes({ nonLabelable: true });
  const fieldLabelRef = useFieldLabelRef();
  const dayRef = useRef<HTMLInputElement>(null);
  const monthRef = useRef<HTMLButtonElement>(null);
  const yearRef = useRef<HTMLInputElement>(null);
  const id = idProp ?? inputAttributes.id;
  const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes['aria-labelledby'];

  const { locale, formatMessage } = useIntl();

  const getDateObject = (): Date | undefined => {
    if (value && isDateValid(value)) {
      return typeof value === 'string' ? convertToLocalMidnight(value) : value;
    }
    return undefined;
  };

  const getInitialDate = (unit: 'year' | 'month' | 'day'): number | null => {
    if (value && isDateValid(value)) {
      const dateObject = getDateObject();

      if (typeof value === 'string' && isMonthAndYearFormat(value) && unit === 'day') {
        return null;
      }

      if (dateObject !== undefined) {
        switch (unit) {
          case 'year':
            return dateObject.getFullYear();
          case 'month':
            return dateObject.getMonth();
          case 'day':
            return dateObject.getDate();
          default:
            return null;
        }
      }
    }
    return null;
  };

  const [day, setDay] = useState(() => getInitialDate('day'));
  const [displayDay, setDisplayDay] = useState(day?.toString());
  const [month, setMonth] = useState(() => getInitialDate('month'));
  const [year, setYear] = useState(() => getInitialDate('year'));
  const [displayYear, setDisplayYear] = useState(year?.toString());
  const [lastBroadcastedValue, setLastBroadcastedValue] = useState<Date | null | undefined>(
    getDateObject,
  );
  const monthNames = getMonthNames(locale, monthFormat);

  const monthYearOnly = mode === DateMode.MONTH_YEAR;
  const monthBeforeDay = MDY.has(locale);
  const yearFirst = YMD.has(locale);

  const resolvedDayLabel = dayLabel || formatMessage(messages.dayLabel);
  const resolvedMonthLabel = monthLabel || formatMessage(messages.monthLabel);
  const resolvedYearLabel = yearLabel || formatMessage(messages.yearLabel);
  const resolvedPlaceholders = {
    day: placeholders?.day || formatMessage(messages.dayPlaceholder),
    month: placeholders?.month || formatMessage(messages.monthLabel),
    year: placeholders?.year || formatMessage(messages.yearPlaceholder),
  };

  useEffect(() => {
    const labelRef = fieldLabelRef?.current;

    if (labelRef) {
      const handleLabelClick = () => {
        // Not the best way to do this, but we're forced to recreate the native Label-click behavior
        if (monthYearOnly || monthBeforeDay) {
          monthRef.current?.click();
        } else if (yearFirst) {
          yearRef.current?.focus();
        } else {
          dayRef.current?.focus();
        }
      };

      labelRef.addEventListener('click', handleLabelClick);

      return () => {
        labelRef?.removeEventListener('click', handleLabelClick);
      };
    }
  }, [fieldLabelRef, id, monthBeforeDay, monthYearOnly, yearFirst]);

  const getDateAsString = (date: Date) => {
    if (!isDateValid(date)) {
      return '';
    }
    switch (mode) {
      case DateMode.MONTH_YEAR:
        return [date.getFullYear(), `0${date.getMonth() + 1}`.slice(-2)].join('-');
      case DateMode.DAY_MONTH_YEAR:
      default:
        return [
          date.getFullYear(),
          `0${date.getMonth() + 1}`.slice(-2),
          `0${date.getDate()}`.slice(-2),
        ].join('-');
    }
  };

  const getSelectElement = () => {
    return (
      <label className="d-flex flex-column">
        <Body type={Typography.BODY_DEFAULT}>{resolvedMonthLabel}</Body>
        <SelectInput
          triggerRef={monthRef}
          id={`${id}:month`}
          parentId={DATE_INPUT_PARENT_ID}
          name="month"
          disabled={disabled}
          placeholder={resolvedPlaceholders.month}
          items={Array.from({ length: 12 }, (_, index) => ({ type: 'option', value: index }))}
          size={size}
          value={month}
          renderValue={(selectedValue) => (
            <SelectInputOptionContent title={monthNames[selectedValue]} />
          )}
          onChange={(selectedValue) => handleMonthChange(selectedValue)}
          {...selectProps}
        />
      </label>
    );
  };

  const isDayValid = (newDay: number, newMonth: number, newYear: number) => {
    const maxDay = new Date(newYear, newMonth + 1, 0).getDate();
    return newDay <= maxDay;
  };

  const handleInternalValue = (newDay = day, newMonth = month, newYear = year) => {
    if (newDay == null || newDay === 0 || newMonth == null || newYear == null || newYear === 0) {
      broadcastNewValue(null);
      return;
    }
    if (!isDayValid(newDay, newMonth, newYear)) {
      broadcastNewValue(null);
      return;
    }

    const dateValue = new Date(newYear, newMonth, newDay);
    if (newYear < 100) {
      dateValue.setFullYear(newYear);
    }

    if (!isDateValid(dateValue)) {
      broadcastNewValue(null);
      return;
    }

    if (mode === DateMode.MONTH_YEAR) {
      if (newMonth !== month || newYear !== year) {
        broadcastNewValue(dateValue);
      }
    } else if (newDay !== day || newMonth !== month || newYear !== year) {
      broadcastNewValue(dateValue);
    }
  };

  const handleDayChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newDayString = event.target.value.replace(/\D/g, '');
    const newDayNumber = Number.parseInt(newDayString, 10);

    setDay(newDayNumber);
    setDisplayDay(newDayString);
    handleInternalValue(newDayNumber, month, year);
  };

  const handleMonthChange = (selectedMonth: number | null) => {
    if (selectedMonth === null) {
      setMonth(null);
      handleInternalValue(day, null, year);
      return;
    }
    setMonth(selectedMonth);
    handleInternalValue(day, selectedMonth, year);
  };

  const handleYearChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newYearString = event.target.value.replace(/\D/g, '');
    const newYearNumber = Number.parseInt(newYearString, 10);

    if (newYearString.length >= 4 && newYearString.length <= 6) {
      setYear(newYearNumber);
      setDisplayYear(newYearString);
      handleInternalValue(day, month, newYearNumber);
    } else {
      setYear(null);
      setDisplayYear(newYearString);
      handleInternalValue(day, month, null);
    }
  };

  const broadcastNewValue = (newValue: Date | null) => {
    if (newValue !== lastBroadcastedValue) {
      setLastBroadcastedValue(newValue);
      onChange(newValue != null ? getDateAsString(newValue) : null);
    }
  };

  const monthWidth = clsx({
    'col-sm-8 tw-date--month': monthYearOnly,
    'col-sm-5 tw-date--month': !monthYearOnly,
  });

  const getMonth = () => {
    return <div className={monthWidth}>{getSelectElement()}</div>;
  };

  const getDay = () => {
    return (
      <div className="col-sm-3 tw-date--day">
        <label>
          <Body type={Typography.BODY_DEFAULT}>{resolvedDayLabel}</Body>
          <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
            <Input
              ref={dayRef}
              id={`${id}:day`}
              type="text"
              inputMode="numeric"
              pattern="[0-9]*"
              name="day"
              autoComplete={dayAutoComplete}
              value={displayDay || ''}
              placeholder={resolvedPlaceholders.day}
              disabled={disabled}
              min={1}
              max={31}
              maxLength={2}
              onChange={(event) => handleDayChange(event)}
            />
          </div>
        </label>
      </div>
    );
  };

  const getYear = () => {
    return (
      <div className="col-sm-4 tw-date--year">
        <label>
          <Body type={Typography.BODY_DEFAULT}>{resolvedYearLabel}</Body>
          <div className={`input-group input-group-${size} ${disabled ? 'disabled' : ''}`}>
            <Input
              ref={yearRef}
              id={`${id}:year`}
              type="text"
              inputMode="numeric"
              pattern="[0-9]*"
              name="year"
              autoComplete={yearAutoComplete}
              placeholder={resolvedPlaceholders.year}
              value={displayYear || ''}
              disabled={disabled}
              min={0}
              max={9999}
              maxLength={6}
              onChange={(event) => handleYearChange(event)}
            />
          </div>
        </label>
      </div>
    );
  };

  return (
    <fieldset
      id={id}
      className="tw-date"
      aria-describedby={inputAttributes['aria-describedby']}
      aria-invalid={inputAttributes['aria-invalid']}
      aria-labelledby={ariaLabelledBy}
      aria-label={ariaLabel}
      data-wds-dateinput=""
      onFocus={(event) =>
        shouldPropagateOnFocus(event) ? onFocus?.(event) : event.stopPropagation()
      }
      onBlur={(event) => (shouldPropagateOnBlur(event) ? onBlur?.(event) : event.stopPropagation())}
    >
      <div className="row">
        {(() => {
          if (monthYearOnly) {
            return (
              <>
                {!yearFirst && getMonth()}
                {getYear()}
                {yearFirst && getMonth()}
              </>
            );
          }
          if (monthBeforeDay) {
            return (
              <>
                {getMonth()}
                {getDay()}
                {getYear()}
              </>
            );
          }
          if (yearFirst) {
            return (
              <>
                {getYear()}
                {getMonth()}
                {getDay()}
              </>
            );
          }
          return (
            <>
              {getDay()}
              {getMonth()}
              {getYear()}
            </>
          );
        })()}
      </div>
    </fieldset>
  );
};

// Should only propagate if the relatedTarget is not part of this DateInput component.
function shouldPropagateOnFocus({
  target,
  relatedTarget,
}: Pick<React.FocusEvent, 'target' | 'relatedTarget'>) {
  const blurredElementParent = target.closest('[data-wds-dateinput]');
  const focusedElementParent = relatedTarget?.closest('[data-wds-dateinput]');
  return blurredElementParent !== focusedElementParent;
}

// Should only propagate if the focus-gaining element is not part
// of this DateInput component or the (dropdown) of the month select.
function shouldPropagateOnBlur({
  target,
  relatedTarget,
}: Pick<React.FocusEvent, 'target' | 'relatedTarget'>) {
  const blurredElementParent = target.closest('[data-wds-dateinput]');
  const focusedElementParent = relatedTarget?.closest('[data-wds-dateinput]');

  return (
    blurredElementParent !== focusedElementParent &&
    !target?.closest(`[data-wds-parent="${DATE_INPUT_PARENT_ID}"]`) &&
    !relatedTarget?.closest(`[data-wds-parent="${DATE_INPUT_PARENT_ID}"]`)
  );
}

export default DateInput;
