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

import { Body, Input, SelectInput, SelectInputOptionContent, SelectInputProps } from '..';
import {
  DateMode,
  MonthFormat,
  Size,
  SizeLarge,
  SizeMedium,
  SizeSmall,
  Typography,
} from '../common';
import { MDY, YMD, getMonthNames, isDateValid, isMonthAndYearFormat } from '../common/dateUtils';
import { 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;
  disabled?: boolean;
  size?: SizeSmall | SizeMedium | SizeLarge;
  value?: Date | string;
  onChange: (value: string | null) => void;
  onFocus?: React.FocusEventHandler<HTMLDivElement>;
  onBlur?: React.FocusEventHandler<HTMLDivElement>;
  dayLabel?: string;
  dayAutoComplete?: string;
  monthLabel?: string;
  yearLabel?: string;
  yearAutoComplete?: string;
  monthFormat?: `${MonthFormat}`;
  mode?: `${DateMode}`;
  placeholders?: {
    day?: string;
    month?: string;
    year?: string;
  };
  id?: string;
  selectProps?: Partial<SelectInputProps<number | null>>;
}

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 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);

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

  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}>{monthLabel}</Body>
        <SelectInput
          id={`${id}:month`}
          name="month"
          disabled={disabled}
          placeholder={placeholders?.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 monthYearOnly = mode === DateMode.MONTH_YEAR;

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

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

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

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

  return (
    <div
      className="tw-date"
      {...inputAttributes}
      id={id}
      aria-labelledby={ariaLabelledBy}
      aria-label={ariaLabel}
      role="group" // Add role attribute to indicate container for interactive elements
      onFocus={(event) =>
        shouldPropagateOnFocus(event) ? onFocus && onFocus(event) : event.stopPropagation()
      }
      onBlur={(event) =>
        shouldPropagateOnBlur(event) ? onBlur && 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>
    </div>
  );
};

// Should only propagate if the relatedTarget is not part of this DateInput component.
function shouldPropagateOnFocus({
  target,
  relatedTarget,
}: Pick<React.FocusEvent, 'target' | 'relatedTarget'>) {
  const targetParent = target.closest('.tw-date');
  const relatedParent = relatedTarget && relatedTarget.closest('.tw-date');
  return targetParent !== relatedParent;
}

// Should only propagate if the relatedTarget or the activeElement is not part of this DateInput component.
function shouldPropagateOnBlur({
  target,
  relatedTarget,
}: Pick<React.FocusEvent, 'target' | 'relatedTarget'>) {
  const blurElementParent = target.closest('.tw-date');
  // Even though FocusEvent.relatedTarget is supported by IE
  // (https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/relatedTarget)
  // "IE11 sets document.activeElement to the next focused element before the blur event is called."
  // (https://stackoverflow.com/a/49325196/986241)
  // Therefore if the relatedTarget is null, we try the document.activeElement,
  // which may contain the HTML element that is gaining focus
  const focusElement =
    relatedTarget || (document.activeElement !== target ? document.activeElement : null);
  const focusElementParent = focusElement && focusElement.closest('.tw-date');
  return blurElementParent !== focusElementParent;
}

export default DateInput;
