import {
  forwardRef,
  Fragment,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from "react";
import { ListBox } from "primereact/listbox";
import { Calendar } from "primereact/calendar";
import { OverlayPanel } from "primereact/overlaypanel";
import { InputText } from "primereact/inputtext";
import { Nullable } from "primereact/ts-helpers";
import { Dropdown } from "primereact/dropdown";
import { Tag } from "primereact/tag";
import { SelectItem, SelectItemOptionsType } from "primereact/selectitem";
import { OSIcon } from "../icon";
import { OSButton } from "../button/OSButton";

interface TimeRange {
  [label: string]: Date[];
}

interface Props {
  id?: string;
  value?: [Date, Date];
  inputName?: string;

  startDate?: Date;
  endDate?: Date;
  minDate?: Date;
  maxDate?: Date;

  timePicker?: boolean;
  timePickerIncrement?: number;
  timePickerSeconds?: boolean;

  ranges?: TimeRange;
  showCustomRangeLabel?: boolean;
  alwaysShowCalendars?: boolean;
  opens?: "left" | "right" | "center";
  drops?: "down" | "up" | "auto";

  buttonClasses?: string;
  applyButtonClasses?: string;
  cancelButtonClasses?: string;

  singleDatePicker?: boolean;
  autoApply?: boolean;

  autoUpdateInput?: boolean;
  parentEl?: string;

  disabled?: boolean;

  onChange?(start: Date, end: Date, predefinedLabel?: string | null): void;
}

export interface DateTimePickerRef {
  /** The label of the currently selected preset range, or "Custom" if a bespoke range is selected, or null if nothing has been chosen yet. */
  getSelectedRange: () => string | null;
  getSelectedDates: () => { start: Date | undefined; end: Date | undefined };
}

const CUSTOM_VALUE = "custom";
const CUSTOM_LABEL = "Custom";

const buildRangeString = (startDate: Date, endDate: Date): string =>
  startDate.toISOString() + " - " + endDate.toISOString();

const toDateArray = (value: Date | (Date | null)[] | null | undefined): (Date | null)[] | null => {
  if (value == null) return null;
  return Array.isArray(value) ? value : [value];
};

const DateTimePicker = forwardRef<DateTimePickerRef, Props>(
  (
    {
      id,
      value,
      inputName,
      minDate,
      maxDate,
      timePicker,
      timePickerIncrement,
      timePickerSeconds,
      ranges,
      showCustomRangeLabel,
      alwaysShowCalendars,
      buttonClasses,
      applyButtonClasses,
      cancelButtonClasses,
      singleDatePicker,
      autoApply: autoApplyProp,
      disabled,
      onChange,
    },
    ref,
  ) => {
    const calendarRef = useRef<Calendar>(null);
    const overlayRef = useRef<OverlayPanel>(null);

    const [predefined, setPredefined] = useState<boolean>(false);
    const [rawDates, setRawDates] = useState<Nullable<(Date | null)[]>>();
    const [rawPreset, setRawPreset] = useState<string>();

    const [startDate, setStartDate] = useState<Date>();
    const [endDate, setEndDate] = useState<Date>();

    const [tempStartDate, setTempStartDate] = useState<Date>();
    const [tempEndDate, setTempEndDate] = useState<Date>();

    // if the time picker is selected, we can't auto apply
    const autoApply = autoApplyProp && !timePicker;

    // define ranges according to definitions provided
    const rangeList: SelectItemOptionsType = useMemo(() => {
      let calculatedList: SelectItemOptionsType = [];
      if (ranges != null) {
        calculatedList = Object.keys(ranges)
          .filter((key) => ranges[key].length === 2)
          .map((key) => ({
            value: buildRangeString(ranges[key][0], ranges[key][1]),
            label: key,
          }));
        // add the custom range
        if (showCustomRangeLabel) {
          calculatedList.push({
            value: CUSTOM_VALUE,
            label: CUSTOM_LABEL,
          });
        }
      }
      return calculatedList;
    }, [ranges, showCustomRangeLabel]);

    // resolve the human-readable label for whatever is currently selected
    const selectedRangeLabel: string | null = useMemo(() => {
      if (rawPreset === undefined) return null;
      if (rawPreset === CUSTOM_VALUE) return CUSTOM_LABEL;
      const match = rangeList.find((item) => item?.value === rawPreset);
      return match ? String(match.label) : CUSTOM_LABEL;
    }, [rawPreset, rangeList]);

    // establish the initial selection: from a controlled `value`, matched against
    // the available ranges, falling back to "custom" if nothing matches (or no
    // value was given at all)
    useEffect(() => {
      if (rawPreset !== undefined) return;

      if (value !== undefined && value.length === 2) {
        const [valueStartDate, valueEndDate] = value;
        setStartDate(valueStartDate);
        setTempStartDate(valueStartDate);
        setEndDate(new Date(valueEndDate));
        setTempEndDate(new Date(valueEndDate));

        const standardisedString = buildRangeString(valueStartDate, valueEndDate);
        const foundPreset = rangeList.some((item) => item?.value === standardisedString);

        if (foundPreset) {
          setRawPreset(standardisedString);
          setPredefined(true);
        } else {
          setRawPreset(CUSTOM_VALUE);
          setPredefined(false);
        }
      } else {
        setRawPreset(CUSTOM_VALUE);
        setPredefined(false);
      }
    }, [value, rangeList, rawPreset]);

    // if the ranges on offer change shape and our current preset no longer
    // corresponds to any of them, fall back to custom rather than pointing at
    // a preset that no longer exists
    useEffect(() => {
      if (rawPreset === undefined || rawPreset === CUSTOM_VALUE) return;
      const stillValid = rangeList.some((item) => item?.value === rawPreset);
      if (!stillValid) {
        setRawPreset(CUSTOM_VALUE);
        setPredefined(false);
      }
    }, [rangeList, rawPreset]);

    const dateRangeString =
      (startDate?.toLocaleString() ?? "") + " - " + (endDate?.toLocaleString() ?? "");
    const tempDateRangeString =
      (tempStartDate?.toLocaleString() ?? "") + " - " + (tempEndDate?.toLocaleString() ?? "");

    // attributes for time picker (always computed, regardless of `timePicker`,
    // to satisfy the rules of hooks)
    const hoursList: SelectItem[] = useMemo(() => {
      const calculatedList: SelectItem[] = [];
      for (let x = 0; x < 24; x++) {
        calculatedList.push({ value: x, label: x.toString().padStart(2, "0") });
      }
      return calculatedList;
    }, []);
    const minutesList: SelectItem[] = useMemo(() => {
      const calculatedList: SelectItem[] = [];
      for (let x = 0; x < 60; x += timePickerIncrement ?? 30) {
        calculatedList.push({ value: x, label: x.toString().padStart(2, "0") });
      }
      return calculatedList;
    }, [timePickerIncrement]);
    const secondsList: SelectItem[] = useMemo(() => {
      const calculatedList: SelectItem[] = [];
      for (let x = 0; x < 60; x++) {
        calculatedList.push({ value: x, label: x.toString().padStart(2, "0") });
      }
      return calculatedList;
    }, []);

    // split the date range array into start and end dates
    const handleDateRangeSet = (dates: Nullable<(Date | null)[]>) => {
      if (dates == null) return;

      // any manual calendar interaction means we're no longer on a preset —
      // flip to custom immediately, before we even know the full range yet
      setRawPreset(CUSTOM_VALUE);
      setPredefined(false);

      let newStartDate = startDate ?? new Date();
      let newEndDate = endDate ?? new Date();

      if (autoApply) {
        if (dates[0] != null) {
          newStartDate = new Date(changeDate(startDate, dates[0]));
          if (dates.length == 1) {
            newEndDate = new Date(changeDate(endDate, dates[0]));
          }
        }
        if (dates[1] != null) {
          newEndDate = new Date(changeDate(endDate, dates[1]));
        }

        // for a manually-picked custom range without a time picker, default to
        // the full day rather than leaving whatever time-of-day was on the
        // previous selection
        if (!timePicker) {
          newStartDate = startOfDay(newStartDate);
          newEndDate = endOfDay(newEndDate);
        }

        setStartDate(newStartDate);
        setEndDate(newEndDate);
        if (onChange !== undefined) {
          onChange(newStartDate, newEndDate, CUSTOM_VALUE);
        }
      } else {
        let newTempStartDate = tempStartDate ?? new Date();
        let newTempEndDate = tempEndDate ?? new Date();

        if (dates[0] != null) {
          newTempStartDate = new Date(changeDate(tempStartDate, dates[0]));
          if (dates.length == 1) {
            newTempEndDate = new Date(changeDate(tempEndDate, dates[0]));
          }
        }
        if (dates[1] != null) {
          newTempEndDate = new Date(changeDate(tempEndDate, dates[1]));
        }

        if (!timePicker) {
          newTempStartDate = startOfDay(newTempStartDate);
          newTempEndDate = endOfDay(newTempEndDate);
        }

        setTempStartDate(newTempStartDate);
        setTempEndDate(newTempEndDate);
      }
    };

    // handle selecting a preset
    const handlePresetSelected = (presetString: string) => {
      setRawPreset(presetString);
      if (presetString == CUSTOM_VALUE) {
        setPredefined(false);
        if (autoApply) {
          setEndDate(new Date(changeSeconds(endDate, 0)));
          const foundOption = minutesList.some((element) => element.value == endDate?.getMinutes());
          if (!foundOption) {
            setEndDate(
              new Date(changeMinutes(endDate, minutesList.at(minutesList.length - 1)?.value)),
            );
          }
        } else {
          setTempEndDate(new Date(changeSeconds(tempEndDate, 0)));
          const foundOption = minutesList.some(
            (element) => element.value == tempEndDate?.getMinutes(),
          );
          if (!foundOption) {
            setTempEndDate(
              new Date(changeMinutes(tempEndDate, minutesList.at(minutesList.length - 1)?.value)),
            );
          }
        }
      } else if (presetString != null) {
        setPredefined(true);
        // a named preset was chosen — clear any leftover manual calendar
        // selection so old highlighted dates don't linger visually
        setRawDates(null);

        const dates = presetString.split(" - ", 2);
        if (dates.length == 2) {
          const newStartDate = new Date(dates[0]);
          const newEndDate = new Date(dates[1]);
          setStartDate(newStartDate);
          setEndDate(newEndDate);
          setTempStartDate(newStartDate);
          setTempEndDate(newEndDate);

          if (onChange !== undefined) {
            onChange(newStartDate, newEndDate, presetString);
          }
        }
        overlayRef.current?.hide();
      }
    };

    // buttons to cancel or apply date changes
    const handleDateApply = () => {
      const newStartDate = new Date(tempStartDate ?? new Date());
      setStartDate(newStartDate);
      const newEndDate = new Date(tempEndDate ?? new Date());
      setEndDate(newEndDate);
      if (onChange !== undefined) {
        onChange(newStartDate, newEndDate, rawPreset);
      }
      overlayRef.current?.hide();
    };
    const handleDateCancel = () => {
      setTempStartDate(new Date(startDate ?? new Date()));
      setTempEndDate(new Date(endDate ?? new Date()));
    };

    // utility methods to transform date objects as required
    const changeDate = (alteringDate: Date | undefined, newDate: Date): Date => {
      const target = alteringDate == undefined ? new Date() : new Date(alteringDate);
      target.setDate(newDate.getDate());
      target.setMonth(newDate.getMonth());
      target.setFullYear(newDate.getFullYear());
      return target;
    };
    const startOfDay = (date: Date): Date => {
      const target = new Date(date);
      target.setHours(0, 0, 0, 0);
      return target;
    };
    const endOfDay = (date: Date): Date => {
      const target = new Date(date);
      target.setHours(23, 59, 59, 999);
      return target;
    };

    const changeHours = (alteringDate: Date | undefined, newHour: string | number): Date => {
      const target = alteringDate == undefined ? new Date() : new Date(alteringDate);
      target.setHours(typeof newHour == "string" ? parseInt(newHour) : newHour);
      return target;
    };
    const changeMinutes = (
      alteringDate: Date | undefined,
      newMinute: string | number | undefined,
    ): Date => {
      const target = alteringDate == undefined ? new Date() : new Date(alteringDate);
      if (newMinute !== undefined) {
        target.setMinutes(typeof newMinute == "string" ? parseInt(newMinute) : newMinute);
      }
      return target;
    };
    const changeSeconds = (alteringDate: Date | undefined, newSecond: string | number): Date => {
      const target = alteringDate == undefined ? new Date() : new Date(alteringDate);
      target.setSeconds(typeof newSecond == "string" ? parseInt(newSecond) : newSecond);
      return target;
    };

    // expose the current selection to parents via ref
    useImperativeHandle(
      ref,
      () => ({
        getSelectedRange: () => selectedRangeLabel,
        getSelectedDates: () => ({ start: startDate, end: endDate }),
      }),
      [selectedRangeLabel, startDate, endDate],
    );

    const isPresetSelected = selectedRangeLabel !== null;

    const calendarVisible = !predefined || alwaysShowCalendars || rangeList.length == 0;

    // only let the list's column span react to preset selection when the calendar
    // isn't being force-shown — with alwaysShowCalendars on, the list stays a fixed
    // width regardless of what's selected
    const listColSpanClass =
      alwaysShowCalendars || calendarVisible ? "md:col-span-4 col-span-12" : "col-span-12";

    // actually render the component
    return (
      <Fragment>
        <div className="p-inputgroup flex-auto">
          <span className="p-inputgroup-addon">
            <OSIcon identifier="calendar" weight="solid" />
          </span>

          <div
            className="p-inputtext w-auto flex items-center gap-2 cursor-pointer rounded-l-none"
            onClick={(e) => {
              e.preventDefault();
              overlayRef.current?.toggle(e);
            }}
          >
            {isPresetSelected && <Tag value={selectedRangeLabel} severity="info" />}
            <span>{dateRangeString}</span>
          </div>
          <input type="hidden" name={inputName} value={dateRangeString} readOnly />
          {/* kept for markup/id parity with the original InputText-based field */}
          <InputText id={id} value={dateRangeString} readOnly hidden disabled={disabled} />
        </div>
        <OverlayPanel ref={overlayRef}>
          <div className="grid grid-cols-12 w-full p-0 gap-0">
            <div className={listColSpanClass}>
              <ListBox
                value={rawPreset}
                options={rangeList}
                onChange={(e) => {
                  handlePresetSelected(e.value);
                }}
              />
            </div>
            {(!predefined || alwaysShowCalendars || rangeList.length == 0) && (
              <Fragment>
                <div className="md:col-span-8 col-span-12">
                  <Calendar
                    ref={calendarRef}
                    numberOfMonths={singleDatePicker ? 1 : 2}
                    inline
                    selectionMode="range"
                    maxDate={maxDate}
                    minDate={minDate}
                    value={rawDates}
                    onChange={(e) => {
                      const dates = toDateArray(e.value);
                      setRawDates(dates);
                      handleDateRangeSet(dates);
                    }}
                    disabled={disabled}
                  />
                  {timePicker && (
                    <div className="grid grid-cols-12">
                      <div className="col-span-6 grid grid-cols-12">
                        <Dropdown
                          className={timePickerSeconds ? "col-span-4" : "col-span-6"}
                          options={hoursList}
                          value={autoApply ? startDate?.getHours() : tempStartDate?.getHours()}
                          onChange={(e) => {
                            if (autoApply) {
                              setStartDate(new Date(changeHours(startDate, e.value)));
                            } else {
                              setTempStartDate(new Date(changeHours(tempStartDate, e.value)));
                            }
                          }}
                          disabled={disabled}
                        />
                        <Dropdown
                          className={timePickerSeconds ? "col-span-4" : "col-span-6"}
                          options={minutesList}
                          value={autoApply ? startDate?.getMinutes() : tempStartDate?.getMinutes()}
                          onChange={(e) => {
                            if (autoApply) {
                              setStartDate(new Date(changeMinutes(startDate, e.value)));
                            } else {
                              setTempStartDate(new Date(changeMinutes(tempStartDate, e.value)));
                            }
                          }}
                          disabled={disabled}
                        />
                        {timePickerSeconds && (
                          <Dropdown
                            className="col-span-4"
                            options={secondsList}
                            value={
                              autoApply ? startDate?.getSeconds() : tempStartDate?.getSeconds()
                            }
                            onChange={(e) => {
                              if (autoApply) {
                                setStartDate(new Date(changeSeconds(startDate, e.value)));
                              } else {
                                setTempStartDate(new Date(changeSeconds(tempStartDate, e.value)));
                              }
                            }}
                            disabled={disabled}
                          />
                        )}
                      </div>
                      <div className="col-span-6 grid grid-cols-12">
                        <Dropdown
                          className={timePickerSeconds ? "col-span-4" : "col-span-6"}
                          options={hoursList}
                          value={autoApply ? endDate?.getHours() : tempEndDate?.getHours()}
                          onChange={(e) => {
                            if (autoApply) {
                              setEndDate(new Date(changeHours(endDate, e.value)));
                            } else {
                              setTempEndDate(new Date(changeHours(tempEndDate, e.value)));
                            }
                          }}
                          disabled={disabled}
                        />
                        <Dropdown
                          className={timePickerSeconds ? "col-span-4" : "col-span-6"}
                          options={minutesList}
                          value={autoApply ? endDate?.getMinutes() : tempEndDate?.getMinutes()}
                          onChange={(e) => {
                            if (autoApply) {
                              setEndDate(new Date(changeMinutes(endDate, e.value)));
                            } else {
                              setTempEndDate(new Date(changeMinutes(tempEndDate, e.value)));
                            }
                          }}
                          disabled={disabled}
                        />
                        {timePickerSeconds && (
                          <Dropdown
                            className="col-span-4"
                            options={secondsList}
                            value={autoApply ? endDate?.getSeconds() : tempEndDate?.getSeconds()}
                            onChange={(e) => {
                              if (autoApply) {
                                setEndDate(new Date(changeSeconds(endDate, e.value)));
                              } else {
                                setTempEndDate(new Date(changeSeconds(tempEndDate, e.value)));
                              }
                            }}
                            disabled={disabled}
                          />
                        )}
                      </div>
                    </div>
                  )}
                </div>
                {!autoApply && (
                  <div className="col-span-12">
                    <div className="float-right gap-internal items-center flex">
                      <span className="inline-block align-middle mr-3">{tempDateRangeString}</span>
                      <OSButton
                        label="Cancel"
                        severity="contrast"
                        onClick={handleDateCancel}
                        className={(buttonClasses ?? "") + (cancelButtonClasses ?? "")}
                        disabled={disabled}
                      />
                      <OSButton
                        label="Apply"
                        severity="success"
                        onClick={handleDateApply}
                        className={"" + (buttonClasses ?? "") + (applyButtonClasses ?? "")}
                        disabled={disabled}
                      />
                    </div>
                  </div>
                )}
              </Fragment>
            )}
          </div>
        </OverlayPanel>
      </Fragment>
    );
  },
);

DateTimePicker.displayName = "DateTimePicker";

export default DateTimePicker;
