import {
  type ShiftCopy,
  type ShiftCopyPasteParams,
  type ShiftPaste,
} from 'src/services/shifts';
import {
  type CopiedShiftCell,
  type ShiftAssignCellVC,
  type ShiftCalendarUser,
} from 'src/types/shifts';

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

import { mapFilters, type ShiftFilters } from './assignment';
import {
  buildCopiedCellsWithShifts,
  getUserDayData,
  validateSingleCollaboratorSelection,
} from './selection';

const DAYS_IN_MS = 1000 * 60 * 60 * 24;

/**
 * Groups selected cells by user and returns the earliest date for each.
 * Used to determine paste start date (each user pastes from their first cell).
 *
 * @param selectedCells - Selected assignment cells (userId + date)
 * @returns Map userId -> earliest date (ISO string) for that user
 */
export const groupCellsByUserAndFirstDate = (
  selectedCells: ShiftAssignCellVC[],
) => {
  const userFirstDates = new Map<number, string>();
  selectedCells.forEach(cell => {
    const existingDate = userFirstDates.get(cell.userId);
    if (!existingDate || cell.date < existingDate) {
      userFirstDates.set(cell.userId, cell.date);
    }
  });
  return userFirstDates;
};

/**
 * Computes the cells that will be affected by the paste: for each (userId, startDate) in pasteTargets,
 * generates the sequence of cells (userId, date) respecting the day offset of the copied cells.
 * Only considers copied cells with shifts (ignores empty dates at the source).
 *
 * @param copiedCells - Copied cells (userId, date, shiftIds)
 * @param pasteTargets - Map userId -> paste start date
 * @returns List of cells (userId, date) that will receive the paste
 */
export const calculateAffectedPasteCells = (
  copiedCells: CopiedShiftCell[],
  pasteTargets: Map<number, string>,
) => {
  const affectedCells: ShiftAssignCellVC[] = [];
  if (!copiedCells?.length) return affectedCells;

  const copiedDates = [...new Set(copiedCells.map(c => c.date))].sort();
  const copiedDatesEmptyShifts = [
    ...new Set(
      copiedCells.filter(c => c.shiftIds.length === 0).map(c => c.date),
    ),
  ];
  const firstCopiedDate = new Date(copiedDates[0]);

  pasteTargets.forEach((date, userId) => {
    copiedDates
      .filter(d => !copiedDatesEmptyShifts.includes(d))
      .forEach(filledCopiedDate => {
        const daysDiff = Math.round(
          (new Date(filledCopiedDate).getTime() - firstCopiedDate.getTime()) /
            DAYS_IN_MS,
        );
        const targetDate = new Date(date);
        targetDate.setDate(targetDate.getDate() + daysDiff);
        affectedCells.push({
          userId,
          date: targetDate.toISOString().split('T')[0],
        });
      });
  });
  return affectedCells;
};

type ValidateCopiedCellsParams = {
  registers: ShiftCalendarUser[];
  selectedCells: ShiftAssignCellVC[];
  selectedShifts: Map<number, ShiftSelection>;
  areDatesSelected: boolean;
};

type ValidateCopiedCellsResult = {
  success: boolean;
  copiedData: CopiedShiftCell[] | undefined;
  errorMessage?: string;
  errorDescription?: string;
};

/**
 * Validates that the current selection is valid for copy: single collaborator,
 * no date selection, and has content (cells or shifts). Builds the copied data.
 *
 * @param params - Calendar registers, selected cells/shifts, whether dates are selected
 * @returns success, copiedData (if ok) or errorMessage/errorDescription for i18n
 */
export const validateCopiedCells = ({
  registers,
  selectedCells,
  selectedShifts,
  areDatesSelected,
}: ValidateCopiedCellsParams): ValidateCopiedCellsResult => {
  const result = {
    success: false,
    copiedData: undefined as CopiedShiftCell[] | undefined,
  };
  const selectedUsersCount = validateSingleCollaboratorSelection(
    selectedCells,
    selectedShifts,
  );

  if (!selectedUsersCount) {
    return { success: false, copiedData: undefined };
  }
  if (selectedUsersCount !== 1 || areDatesSelected) {
    return {
      ...result,
      errorMessage: 'shifts:actions.copy.error_multiple_collaborators',
      errorDescription:
        'shifts:actions.copy.error_multiple_collaborators_description',
    };
  }
  if (selectedCells.length === 0 && selectedShifts.size === 0) {
    return { ...result, errorMessage: 'shifts:actions.copy.error_no_content' };
  }

  const copiedData = buildCopiedCellsWithShifts(
    selectedCells,
    selectedShifts,
    registers,
  );
  if (copiedData.length === 0) {
    return { ...result, errorMessage: 'shifts:actions.copy.error_no_content' };
  }
  return { success: true, copiedData };
};

type ValidatePasteOperationParams = {
  copiedCells: CopiedShiftCell[];
  selectedCells: ShiftAssignCellVC[];
  selectedDates: Set<string>;
  allRowsSelected: boolean;
  registers: ShiftCalendarUser[];
};

type ValidatePasteOperationResult = {
  success: boolean;
  targetCells?: ShiftAssignCellVC[];
  pasteTargets?: Map<number, string>;
  errorMessage?: string;
};

/**
 * Validates that paste is allowed: copied data exists and there is a target (cells, dates, or full row).
 * Builds targetCells and pasteTargets (per user, first date) for the paste.
 *
 * @param params - Copied cells, cells/dates selected as destination, registers, allRowsSelected
 * @returns success, targetCells, pasteTargets or errorMessage for i18n
 */
export const validatePasteOperation = ({
  copiedCells,
  selectedCells,
  selectedDates,
  allRowsSelected,
  registers,
}: ValidatePasteOperationParams): ValidatePasteOperationResult => {
  const result = { success: false };

  if (!copiedCells?.length) {
    return {
      ...result,
      errorMessage: 'shifts:actions.paste.error_without_copy',
    };
  }
  if (
    selectedCells.length === 0 &&
    selectedDates.size === 0 &&
    !allRowsSelected
  ) {
    return {
      ...result,
      errorMessage: 'shifts:actions.paste.error_target_empty',
    };
  }

  const targetCells: ShiftAssignCellVC[] = [...selectedCells];

  if (selectedDates.size > 0) {
    registers.forEach(user => {
      selectedDates.forEach(date => {
        const exists = targetCells.some(
          c => c.userId === user.userId && c.date === date,
        );
        if (!exists) targetCells.push({ userId: user.userId, date });
      });
    });
  }

  if (allRowsSelected && targetCells.length === 0 && registers.length > 0) {
    const firstUser = registers[0];
    const firstDate = Object.keys(firstUser.days)[0];
    if (firstDate) {
      targetCells.push({ userId: firstUser.userId, date: firstDate });
    }
  }

  const hasDateSelections = selectedDates.size > 0 || allRowsSelected;
  if (!hasDateSelections && targetCells.length > 0) {
    targetCells.sort(
      (a, b) => a.userId - b.userId || a.date.localeCompare(b.date),
    );
  }

  const pasteTargets = groupCellsByUserAndFirstDate(targetCells);
  if (!pasteTargets.size) {
    return {
      ...result,
      errorMessage: 'shifts:actions.paste.error_without_copy',
    };
  }

  return { success: true, targetCells, pasteTargets };
};

/**
 * Adjusts the paste start date by skipping leading copied cells that were empty,
 * so the first copied shift aligns with the target date.
 *
 * @param targetDate - Paste start date (ISO date string)
 * @param copiedCells - Copied cells (ordered); counts how many empty ones are at the start
 * @returns Shifted date (ISO date string)
 */
export const parsePasteTargetDate = (
  targetDate: string,
  copiedCells: CopiedShiftCell[],
) => {
  let previousEmptyCellsCount = 0;
  for (const cell of copiedCells) {
    if (cell.shiftIds.length === 0) previousEmptyCellsCount++;
    else break;
  }
  const date = new Date(`${targetDate}T00:00`);
  const shiftedDate = new Date(
    date.setDate(date.getDate() + previousEmptyCellsCount),
  );
  return shiftedDate.toISOString().split('T')[0];
};

type BuildPastePayloadParams = {
  copiedCells: CopiedShiftCell[];
  pasteTargets: Map<number, string>;
  registers: ShiftCalendarUser[];
  selectedDates: Set<string>;
  allRowsSelected: boolean;
  excludeCells: ShiftAssignCellVC[];
  filters: ShiftFilters;
};

/**
 * Builds the copy/paste payload for the API: defines source (copy) and destinations (paste).
 * If all copied cells have the full day, uses dates; otherwise assignedShiftIds.
 * Supports paste by dates (allUsers + startDate + excludeTargetUserIds + filters) or by specific users.
 *
 * @param params - Copied cells, pasteTargets, registers, selectedDates, allRowsSelected, excludeCells, filters
 * @returns ShiftCopyPasteParams ready to send to the service
 * @throws Error if there are no copied cells
 */
export const buildPastePayload = ({
  copiedCells,
  pasteTargets,
  registers,
  selectedDates,
  allRowsSelected,
  excludeCells,
  filters,
}: BuildPastePayloadParams): ShiftCopyPasteParams => {
  if (!copiedCells?.length) {
    throw new Error('No copied items to paste');
  }

  const cellsWithPartialShifts: string[] = [];
  copiedCells.forEach(copiedCell => {
    const userDayData = getUserDayData(
      registers,
      copiedCell.userId,
      copiedCell.date,
    );
    if (!userDayData) return;
    const copyableShifts = userDayData.dayData.shifts
      .filter(shift => shift.shiftAssignedShiftId)
      .map(shift => shift.shiftAssignedShiftId);
    const copiedAllShifts =
      copyableShifts.length > 0 &&
      copyableShifts.length === copiedCell.shiftIds.length &&
      copyableShifts.every(id => copiedCell.shiftIds.includes(id));
    if (!copiedAllShifts) cellsWithPartialShifts.push(copiedCell.date);
  });

  const sendDates = cellsWithPartialShifts.length === 0;
  const copy: ShiftCopy = {
    sourceUserId: copiedCells[0].userId,
    dates: sendDates ? [...new Set(copiedCells.map(c => c.date))] : undefined,
    assignedShiftIds: !sendDates
      ? copiedCells.flatMap(c => c.shiftIds)
      : undefined,
  };

  const hasDateSelections = selectedDates.size > 0 || allRowsSelected;
  let paste: ShiftPaste;

  if (hasDateSelections) {
    const excludeUserIds = excludeCells
      .filter(cell => selectedDates.has(cell.date) || allRowsSelected)
      .map(cell => cell.userId);
    const firstTargetDate = pasteTargets.values().next().value;
    paste = {
      allUsers: true,
      startDate: parsePasteTargetDate(firstTargetDate, copiedCells),
      excludeTargetUserIds:
        excludeUserIds.length > 0 ? excludeUserIds : undefined,
      filters: mapFilters(filters),
    };
  } else {
    paste = {
      users: Array.from(pasteTargets.entries()).map(([userId, startDate]) => ({
        targetUserId: userId,
        startDate: parsePasteTargetDate(startDate, copiedCells),
      })),
    };
  }

  return { copy, paste };
};

/**
 * Builds the ShiftCopy object for a single copied cell: if the day is full (all shifts),
 * uses dates; otherwise assignedShiftIds.
 *
 * @param copiedCell - Copied cell (userId, date, shiftIds)
 * @param registers - Calendar registers to verify the day's shifts
 * @returns ShiftCopy (sourceUserId + dates or assignedShiftIds)
 */
const getCopyForCell = (
  copiedCell: CopiedShiftCell,
  registers: ShiftCalendarUser[],
): ShiftCopy => {
  if (copiedCell.shiftIds.length === 0) {
    return {
      sourceUserId: copiedCell.userId,
      dates: [copiedCell.date],
    };
  }
  const userDayData = getUserDayData(
    registers,
    copiedCell.userId,
    copiedCell.date,
  );
  const sendDates = (() => {
    if (!userDayData) return false;
    const copyableShifts = userDayData.dayData.shifts
      .filter(shift => shift.shiftAssignedShiftId)
      .map(shift => shift.shiftAssignedShiftId);
    return (
      copyableShifts.length > 0 &&
      copyableShifts.length === copiedCell.shiftIds.length &&
      copyableShifts.every(id => copiedCell.shiftIds.includes(id))
    );
  })();
  return {
    sourceUserId: copiedCell.userId,
    dates: sendDates ? [copiedCell.date] : undefined,
    assignedShiftIds: !sendDates ? copiedCell.shiftIds : undefined,
  };
};

/**
 * Expands a target cell into a sequence of consecutive cells for the same user,
 * based on available dates in registers and the length of the copied content.
 *
 * @param targetCell - Initial cell (userId, date)
 * @param copiedLength - Number of days to expand
 * @param registers - Calendar registers (for date order)
 * @returns List of consecutive cells (userId, date); [targetCell] if the date does not exist
 */
const expandTargetToSequence = (
  targetCell: ShiftAssignCellVC,
  copiedLength: number,
  registers: ShiftCalendarUser[],
): ShiftAssignCellVC[] => {
  const user = registers.find(r => r.userId === targetCell.userId);
  const sortedDates = Object.keys(user?.days ?? {}).sort((a, b) =>
    a.localeCompare(b),
  );
  const startIndex = sortedDates.indexOf(targetCell.date);
  if (startIndex === -1) return [targetCell];
  const datesToUse = sortedDates.slice(startIndex, startIndex + copiedLength);
  return datesToUse.map(date => ({ userId: targetCell.userId, date }));
};

/**
 * Checks whether dates form consecutive blocks that should collapse into a single payload per user.
 * Only collapses when the number of target days exactly matches the copied count (same week/pattern).
 * If there are more target cells than copied (e.g. 2→4 or 2→3), does not collapse so the fallback
 * repeats the pattern per cell (index % copiedLength).
 *
 * @param effectiveTargetCells - Already expanded target cells
 * @param copiedLength - Number of copied days
 * @returns List of { userId, startDate } per user that qualifies, or null if none
 */
const groupTargetCellsByUserAndConsecutiveBlock = (
  effectiveTargetCells: ShiftAssignCellVC[],
  copiedLength: number,
): { userId: number; startDate: string }[] | null => {
  if (!effectiveTargetCells.length || copiedLength <= 0) return null;
  const byUser = new Map<number, string[]>();
  for (const cell of effectiveTargetCells) {
    const list = byUser.get(cell.userId) ?? [];
    if (!list.includes(cell.date)) list.push(cell.date);
    byUser.set(cell.userId, list);
  }
  const result: { userId: number; startDate: string }[] = [];
  for (const [userId, dates] of byUser) {
    if (dates.length !== copiedLength) continue;
    const sorted = [...dates].sort((a, b) => a.localeCompare(b));
    let consecutive = true;
    for (let i = 1; i < sorted.length; i++) {
      const prev = new Date(`${sorted[i - 1]}T00:00`);
      const curr = new Date(`${sorted[i]}T00:00`);
      const diffDays =
        (curr.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24);
      if (diffDays !== 1) {
        consecutive = false;
        break;
      }
    }
    if (consecutive) result.push({ userId, startDate: sorted[0] });
  }
  return result.length > 0 ? result : null;
};

/**
 * Builds ShiftCopy for a block of cells (pattern/week): if all days are full uses dates;
 * if there are partial days, uses assignedShiftIds.
 *
 * @param copiedCells - Copied cells from the pattern
 * @param registers - Calendar registers
 * @returns ShiftCopy (sourceUserId + dates or assignedShiftIds)
 */
const getCopyForWeek = (
  copiedCells: CopiedShiftCell[],
  registers: ShiftCalendarUser[],
): ShiftCopy => {
  const cellsWithPartialShifts: string[] = [];
  copiedCells.forEach(copiedCell => {
    const userDayData = getUserDayData(
      registers,
      copiedCell.userId,
      copiedCell.date,
    );
    if (!userDayData) return;
    const copyableShifts = userDayData.dayData.shifts
      .filter(shift => shift.shiftAssignedShiftId)
      .map(shift => shift.shiftAssignedShiftId);
    const copiedAllShifts =
      copyableShifts.length > 0 &&
      copyableShifts.length === copiedCell.shiftIds.length &&
      copyableShifts.every(id => copiedCell.shiftIds.includes(id));
    if (!copiedAllShifts) cellsWithPartialShifts.push(copiedCell.date);
  });
  const sendDates = cellsWithPartialShifts.length === 0;
  return {
    sourceUserId: copiedCells[0].userId,
    dates: sendDates ? [...new Set(copiedCells.map(c => c.date))] : undefined,
    assignedShiftIds: !sendDates
      ? copiedCells.flatMap(c => c.shiftIds)
      : undefined,
  };
};

/**
 * Gets the effective target cells for pasting a pattern: if there is a single target cell,
 * expands it into a sequence; if one per user, expands each; otherwise returns the same cells.
 *
 * @param copiedCells - Copied cells (defines pattern length)
 * @param targetCells - Selected target cells
 * @param registers - Calendar registers
 * @returns List of cells (userId, date) where the paste will be applied
 */
const getEffectiveTargetCellsForPattern = (
  copiedCells: CopiedShiftCell[],
  targetCells: ShiftAssignCellVC[],
  registers: ShiftCalendarUser[],
): ShiftAssignCellVC[] => {
  if (!targetCells?.length) return [];

  if (targetCells.length === 1) {
    return expandTargetToSequence(
      targetCells[0],
      copiedCells.length,
      registers,
    );
  }

  const byUser = new Map<number, ShiftAssignCellVC[]>();
  for (const cell of targetCells) {
    const list = byUser.get(cell.userId) ?? [];
    list.push(cell);
    byUser.set(cell.userId, list);
  }
  const oneCellPerUser = [...byUser.values()].every(list => list.length === 1);
  if (!oneCellPerUser) return targetCells;

  const effective: ShiftAssignCellVC[] = [];
  for (const cell of targetCells) {
    effective.push(
      ...expandTargetToSequence(cell, copiedCells.length, registers),
    );
  }
  return effective;
};

export type BuildPastePayloadsForPatternResult = {
  payloads: ShiftCopyPasteParams[];
  targetCells: ShiftAssignCellVC[];
};

type BuildPastePayloadsForPatternParams = {
  copiedCells: CopiedShiftCell[];
  targetCells: ShiftAssignCellVC[];
  registers: ShiftCalendarUser[];
};

/**
 * Builds one or more copy/paste payloads to paste a pattern onto the target cells.
 * If targets form consecutive blocks per user (week), a single payload; otherwise
 * one payload per target cell (mapping each to the corresponding copied cell).
 *
 * @param params - copiedCells, targetCells, registers
 * @returns payloads (ShiftCopyPasteParams[]) and effective targetCells used
 */
export const buildPastePayloadsForPattern = ({
  copiedCells,
  targetCells,
  registers,
}: BuildPastePayloadsForPatternParams): BuildPastePayloadsForPatternResult => {
  const effectiveTargetCells = getEffectiveTargetCellsForPattern(
    copiedCells,
    targetCells,
    registers,
  );
  if (!copiedCells?.length || !effectiveTargetCells?.length) {
    return { payloads: [], targetCells: [] };
  }

  const userWeekBlocks = groupTargetCellsByUserAndConsecutiveBlock(
    effectiveTargetCells,
    copiedCells.length,
  );
  const singleUser =
    userWeekBlocks?.length === 1 &&
    new Set(effectiveTargetCells.map(c => c.userId)).size === 1;
  if (userWeekBlocks?.length && singleUser) {
    const copy = getCopyForWeek(copiedCells, registers);
    const paste: ShiftPaste = {
      users: userWeekBlocks.map(({ userId, startDate }) => ({
        targetUserId: userId,
        startDate: parsePasteTargetDate(startDate, copiedCells),
      })),
    };
    return { payloads: [{ copy, paste }], targetCells: effectiveTargetCells };
  }

  const isSingleTargetExpanded = targetCells.length === 1;
  const payloads = effectiveTargetCells.map((targetCell, index) => {
    const copyIndex = isSingleTargetExpanded
      ? index
      : index % copiedCells.length;
    const copiedCell = copiedCells[copyIndex];
    const copy = getCopyForCell(copiedCell, registers);
    const paste: ShiftPaste = {
      users: [{ targetUserId: targetCell.userId, startDate: targetCell.date }],
    };
    return { copy, paste };
  });

  return { payloads, targetCells: effectiveTargetCells };
};
