import { addDays, isSameDay } from 'date-fns';
import { SxProps, Theme } from '@material-hu/mui/styles';

import {
  ShiftAssignCellVC,
  ShiftCalendarUser,
  ShiftColor,
} from 'src/types/shifts';

import { TimeOffRequest } from 'src/types/timeTracking';
import { LokaliseTFunction } from 'src/utils/i18n';
import {
  processHoursForDisplay,
  sliceTimeToHoursAndMinutes,
} from 'src/utils/timeTracking';

import { ShiftSelection } from '../../contexts/ShiftsSelectionContext';

import {
  CARD_CONTENT_PADDING_SX,
  DRAFT_STYLES,
  SHIFT_CARD_COLORS,
} from './constants';

const isValidShiftColor = (color: ShiftColor | string) =>
  Object.values(ShiftColor).includes(color as ShiftColor);

export const getValidShiftColor = (color: string | null): ShiftColor =>
  !color || !isValidShiftColor(color) ? ShiftColor.BLUE : (color as ShiftColor);

type ShiftColorParseParams = {
  palette: Theme['palette'];
  color: ShiftColor | 'dayOff';
  isDraft: boolean;
  isSelected: boolean;
  isCopiedOrDragged: boolean;
};

export const parseShiftCardSx = ({
  palette,
  color,
  isDraft,
  isSelected,
  isCopiedOrDragged,
}: ShiftColorParseParams): SxProps<Theme> => ({
  backgroundColor: isDraft
    ? DRAFT_STYLES(palette).backgroundColor
    : SHIFT_CARD_COLORS(palette)[color].backgroundColor,
  borderColor: isSelected
    ? SHIFT_CARD_COLORS(palette)[color].accentBorderColor
    : SHIFT_CARD_COLORS(palette)[color].baseBorderColor,
  pointerEvents: 'auto',
  position: 'relative',
  width: '100%',
  ...(isCopiedOrDragged && {
    borderColor: 'transparent',
    // outline used to create a dashed border effect without affecting the width of the card and shifting the layout
    outline: `2px dashed ${isSelected ? SHIFT_CARD_COLORS(palette)[color].accentBorderColor : SHIFT_CARD_COLORS(palette)[color].baseBorderColor}`,
  }),
  '.MuiCardContent-root': {
    opacity: 1,
    span: {
      fontSize: 12,
      color: isDraft
        ? DRAFT_STYLES(palette).textColor
        : SHIFT_CARD_COLORS(palette)[color].textColor,
    },
  },
});

const ICON_BUTTON_COMMON_SX = {
  border: 'unset',
  opacity: 0,
  position: 'absolute',
  right: 12,
  top: '50%',
  transform: 'translateY(-50%)',
  transition: 'opacity 0.2s ease',
  pointerEvents: 'auto',
  '&:hover': {
    opacity: 1,
  },
};

export const parseShiftMenuButtonSx = ({
  palette,
  color,
  isDraft,
}: Omit<
  ShiftColorParseParams,
  'isSelected' | 'isCopiedOrDragged'
>): SxProps<Theme> => ({
  svg: {
    stroke: isDraft
      ? DRAFT_STYLES(palette).textColor
      : SHIFT_CARD_COLORS(palette)[color].textColor,
  },
  '&.MuiIconButton-root': {
    backgroundColor: isDraft
      ? DRAFT_STYLES(palette).backgroundColor
      : SHIFT_CARD_COLORS(palette)[color].backgroundColor,
  },
  '&:focus-visible': {
    opacity: 1,
    border: `1px solid ${SHIFT_CARD_COLORS(palette)[color].accentBorderColor}`,
  },
  ...ICON_BUTTON_COMMON_SX,
});

// Helper function to create the shift card sx because TS can't infer the type of the spread sx props
export const createShiftCardSx = (
  shiftCardSxParams: ShiftColorParseParams,
) => ({
  ...parseShiftCardSx(shiftCardSxParams),
  ...CARD_CONTENT_PADDING_SX,
});

const isConsecutiveDates = (dates: Set<string>) => {
  const sortedDates = Array.from(dates)
    .map(dateStr => {
      const [year, month, day] = dateStr.split('-').map(Number);
      return new Date(year, month - 1, day);
    })
    .sort((a, b) => a.getTime() - b.getTime());

  const isConsecutive = sortedDates.every((date, index) => {
    if (index === 0) return true;
    const previousDate = sortedDates[index - 1];
    return isSameDay(date, addDays(previousDate, 1));
  });

  return {
    isConsecutive,
    startDate: sortedDates[0],
    endDate: sortedDates[sortedDates.length - 1],
  };
};

export const getDateRangeText = (
  selectedDates: Set<string>,
  selectedCells: ShiftAssignCellVC[],
  t: LokaliseTFunction,
  formatDate: (date: Date, format?: string) => string,
) => {
  // If there are selected dates but no selected cells
  if (selectedDates.size > 0 && selectedCells.length === 0) {
    const { isConsecutive, startDate, endDate } =
      isConsecutiveDates(selectedDates);

    if (isConsecutive) {
      return t('date_range', {
        startDate: formatDate(startDate, 'dd/MM/yyyy'),
        endDate: formatDate(endDate, 'dd/MM/yyyy'),
      });
    } else {
      return t('shifts:multiple_dates');
    }
    // If there are selected cells but no selected dates
  } else if (selectedCells.length > 0 && selectedDates.size === 0) {
    const cellDates = new Set(selectedCells.map(cell => cell.date));
    const datesByUser = selectedCells.reduce(
      (acc, cell) => {
        if (!acc[cell.userId]) {
          acc[cell.userId] = new Set();
        }
        acc[cell.userId].add(cell.date);
        return acc;
      },
      {} as Record<number, Set<string>>,
    );

    // Check if all users have exactly the same dates
    const userDateSets = Object.values(datesByUser);
    const firstUserDates = userDateSets[0];
    const allUsersHaveSameDates = userDateSets.every(
      userDates =>
        userDates.size === firstUserDates.size &&
        [...userDates].every(date => firstUserDates.has(date)),
    );

    if (!allUsersHaveSameDates) {
      return t('shifts:multiple_dates');
    }

    const { isConsecutive, startDate, endDate } = isConsecutiveDates(cellDates);

    if (isConsecutive) {
      return t('date_range', {
        startDate: formatDate(startDate, 'dd/MM/yyyy'),
        endDate: formatDate(endDate, 'dd/MM/yyyy'),
      });
    } else {
      return t('shifts:multiple_dates');
    }
    // If there are selected dates and selected cells. Always show the multiple dates text
  } else {
    return t('shifts:multiple_dates');
  }
};

type IsShiftSelectedParams = {
  shiftId: number;
  cellData: ShiftAssignCellVC;
  excludeShifts: Map<number, ShiftSelection>;
  selectedShifts: Map<number, ShiftSelection>;
  selectedDates: Set<string>;
  cellSelectionMap: Map<string, boolean>;
};

/**
 * Determines if a shift is selected based on following criteria.
 * 1. It's explicitly in selectedShifts, OR
 * 2. Its cell is explicitly selected (via cellSelectionMap), OR
 * 3. Its date is selected AND the cell is not explicitly excluded
 *
 * A shift is NOT selected if it's in excludeShifts.
 */
export const checkIfShiftIsSelected = ({
  shiftId,
  cellData,
  excludeShifts,
  selectedShifts,
  selectedDates,
  cellSelectionMap,
}: IsShiftSelectedParams): boolean => {
  if (excludeShifts.has(shiftId)) return false;
  if (selectedShifts.has(shiftId)) return true;

  // Check cell-level selection
  const cellKey = `${cellData.userId}-${cellData.date}`;
  const explicitCellSelection = cellSelectionMap.get(cellKey);

  if (explicitCellSelection) return true;

  // Cell is NOT explicitly excluded and is implicitly selected via date
  return (
    explicitCellSelection === undefined && selectedDates.has(cellData.date)
  );
};

/**
 * Gets all shifts for a specific date across all collaborators
 * @param date - Date to get shifts for
 * @param registers - Registers to search
 * @returns Array of ShiftSelection objects for the date
 */
export const getShiftsForDate = (
  date: string,
  registers: ShiftCalendarUser[] | undefined,
): ShiftSelection[] =>
  registers?.flatMap(
    collaborator =>
      collaborator.days[date]?.shifts?.map(shift => ({
        shiftId: shift.shiftAssignedShiftId,
        userId: collaborator.userId,
        date,
      })) ?? [],
  ) ?? [];

type DayItem = {
  key: string;
  label: string;
};

/**
 * Gets all shift for a specific collaborator across all days
 * @param userId - Collaborator id
 * @param days - Days to check
 * @param registers - Registers to search
 * @returns Array of ShiftSelection objects for the collaborator
 */
export const getShiftsForCollaborator = (
  userId: number,
  days: DayItem[],
  registers: ShiftCalendarUser[] | undefined,
): ShiftSelection[] => {
  const collaborator = registers?.find(r => r.userId === userId);
  if (!collaborator) return [];

  return days.flatMap(
    day =>
      collaborator.days[day.key]?.shifts?.map(shift => ({
        shiftId: shift.shiftAssignedShiftId,
        userId,
        date: day.key,
      })) ?? [],
  );
};

/**
 * Checks if any shifts are excluded from a pre-computed list
 * @param excludeShifts - Map of excluded shifts
 * @param shifts - shifts to check
 * @returns True if any shifts in the scope are excluded
 */
export const checkIfShiftsAreExcluded = (
  excludeShifts: Map<number, ShiftSelection>,
  shifts: ShiftSelection[],
) => shifts.some(shift => excludeShifts.has(shift.shiftId));

type ColumnCheckboxStateParams = {
  date: string;
  selectedDates: Set<string>;
  selectedCells: ShiftAssignCellVC[];
  excludeCells: ShiftAssignCellVC[];
  excludeShifts: Map<number, ShiftSelection>;
  dateShifts: ShiftSelection[];
};

type CheckboxState = {
  checked: boolean;
  indeterminate: boolean;
};

/**
 * Calculates the checked and indeterminate state for a column (date) checkbox
 * @param date - Date (column) to check
 * @param selectedDates - Selected dates (columns)
 * @param selectedCells - Selected cells
 * @param excludeCells - Excluded cells
 * @param excludeShifts - Excluded shifts
 * @param dateShifts - shifts for this date
 * @returns The checked and indeterminate state for the column checkbox
 */
export const getColumnCheckboxState = ({
  date,
  selectedDates,
  selectedCells,
  excludeCells,
  excludeShifts,
  dateShifts,
}: ColumnCheckboxStateParams): CheckboxState => {
  // Use pre-computed shifts if available, otherwise compute on the fly
  const hasExcludedShifts = checkIfShiftsAreExcluded(excludeShifts, dateShifts);

  const checked =
    selectedDates.has(date) &&
    !excludeCells.some(cell => cell.date === date) &&
    !hasExcludedShifts;

  const indeterminate =
    !checked &&
    (selectedCells.some(cell => cell.date === date) ||
      excludeCells.some(cell => cell.date === date) ||
      (selectedDates.has(date) && hasExcludedShifts));

  return { checked, indeterminate };
};

/**
 * Consolidates the selected cells for a collaborator
 * @param selectedDates - Selected dates (columns)
 * @param selectedCellsDates - Selected cells dates
 * @param excludedCellsDates - Collaborator's excluded cells dates
 * @param excludedShiftsDates - Collaborator's excluded shifts dates
 * @returns Dates selected for a collaborator across dates, cells and shifts
 */
const consolidateCollaboratorSelectedCells = (
  selectedDates: Set<string>,
  selectedCellsDates: Set<string>,
  excludedCellsDates: Set<string>,
  excludedShiftsDates: Set<string>,
) => {
  if (!selectedCellsDates.size && !selectedDates.size) {
    return new Set();
  }
  const consolidatedCells = new Set(
    [...selectedDates, ...selectedCellsDates].filter(
      date => !excludedCellsDates.has(date) && !excludedShiftsDates.has(date),
    ),
  );

  return consolidatedCells;
};

type RowCheckboxStateParams = {
  collaborator: ShiftCalendarUser;
  days: DayItem[];
  selectedDates: Set<string>;
  selectedCells: ShiftAssignCellVC[];
  excludeCells: ShiftAssignCellVC[];
  excludeShifts: Map<number, ShiftSelection>;
  rowShifts: ShiftSelection[];
};

const reduceCellsToDates = (cells: ShiftAssignCellVC[], userId: number) => {
  return cells.reduce<Set<string>>((acc, cell) => {
    if (cell.userId === userId) {
      acc.add(cell.date);
    }
    return acc;
  }, new Set());
};

export const getRowCheckboxState = ({
  collaborator,
  days,
  selectedDates,
  selectedCells,
  excludeCells,
  excludeShifts,
  rowShifts,
}: RowCheckboxStateParams): CheckboxState => {
  const selectedCellsDates = reduceCellsToDates(
    selectedCells,
    collaborator.userId,
  );
  const excludedCellsDates = reduceCellsToDates(
    excludeCells,
    collaborator.userId,
  );
  const excludedShiftsDates = new Set(
    [...rowShifts].reduce<Set<string>>((acc, item) => {
      if (excludeShifts.has(item.shiftId)) {
        acc.add(item.date);
      }
      return acc;
    }, new Set()),
  );

  const consolidatedSelectedCells = consolidateCollaboratorSelectedCells(
    selectedDates,
    selectedCellsDates,
    excludedCellsDates,
    excludedShiftsDates,
  );

  const checked = consolidatedSelectedCells.size === days.length;
  const indeterminate = !checked && consolidatedSelectedCells.size > 0;

  return { checked, indeterminate };
};

/**
 * Gets the time offs cell card description
 * @param timeOffRequest - Time off request
 * @param t - Translation function
 * @param onlyShowRangedHours - If true, only show the range hours for the hours consumption type
 * @returns Returns description based on consumption type or time off policy type name
 */
export const timeOffHoursDescription = (
  timeOffRequest: TimeOffRequest,
  t: LokaliseTFunction,
  onlyShowRangedHours: boolean = false,
) => {
  const { consumptionType, fromTime, timeOffPolicyTypeName, toTime } =
    timeOffRequest;
  const { hours, minutes } = processHoursForDisplay(timeOffRequest.hours ?? 0);
  if (consumptionType === 'FULL_DAY') {
    return t('time_tracking_common:full_time');
  }
  if (consumptionType === 'HALF_DAY') {
    return t('time_tracking_common:half_day');
  }
  if (consumptionType === 'HOURS') {
    if (onlyShowRangedHours) {
      return t('time_tracking_common:hours_from_to', {
        startHour: sliceTimeToHoursAndMinutes(fromTime),
        endHour: sliceTimeToHoursAndMinutes(toTime),
      });
    }
    const timeSpan = `${sliceTimeToHoursAndMinutes(fromTime)} - ${sliceTimeToHoursAndMinutes(toTime)}`;
    const duration =
      minutes === '00'
        ? t('time_tracking_common:hours_abbreviated', { hours })
        : t('time_tracking_common:hours_and_minutes_abbreviated', {
            hours,
            minutes,
          });
    return `${timeSpan} (${duration})`;
  }
  return timeOffPolicyTypeName;
};
