import {
  addDays,
  format,
  isSameDay,
  isValid,
  parseISO,
  startOfDay,
} from 'date-fns';
import { type TOptions } from 'i18next';
import { IconCalendarEvent } from '@material-hu/icons/tabler';
import { type Palette } from '@material-hu/mui/styles';

import { recognizePeriod } from '@material-hu/components/composed-components/DatePeriodSelector/utils';

import { TIME_PLACEHOLDER } from 'src/constants/date';
import {
  HIDDEN_INCIDENCES,
  NON_TAG_INCIDENCES,
  REMOTE_WORK_SITE_VALUE,
} from 'src/pages/dashboard/timeTracking/constants';
import {
  type DaySummary,
  type DirtyEntryFormFields,
  type EntryField,
  type EntryFormFields,
  EntrySource,
  EntryType,
  Incidence,
  type ModifiedEntryFields,
  type PairedEntry,
  type ReportFilterFields,
  ScheduleSource,
  type SegmentationGroup,
  type ShiftAssociations,
  type SiteCoordinates,
  type SiteLocation,
  type TimeSlot,
  type TimeTrackingEntry,
  TrackingDayType,
  type UserAccountStatus,
  type UserSite,
  UserTrackingStatus,
} from 'src/types/timeTracking';
import { type MeUser } from 'src/types/user';
import {
  formatToTime,
  formatUTCDate,
  formatUTCLocalDate,
  parseLocalDateWithTimezone,
} from 'src/utils/date';

import { ProfileField } from 'src/components/AdvancedFilter/types';
import { selectedItemsToString } from 'src/components/AdvancedFilter/utils';

import { type LokaliseTFunction } from './i18n';
import { iconsPolicyTypes } from './vacations';

export const getTodayInTimezone = (timezone: string) =>
  parseLocalDateWithTimezone(new Date(), timezone, 'to');

type ActivityTrackingStatus =
  | UserTrackingStatus.ACTIVE
  | UserTrackingStatus.NOT_CLOCKED_IN
  | UserTrackingStatus.INACTIVE
  | UserAccountStatus.DEACTIVATED;

export const statusColor = (
  palette: Palette,
): Record<ActivityTrackingStatus, string> => ({
  ACTIVE: palette.newBase?.green[500] || '',
  NOT_CLOCKED_IN: palette.newBase?.yellow[500] || '',
  INACTIVE: palette.new.text.neutral.disabled,
  DEACTIVATED: palette.new.text.neutral.disabled,
});

export const flattenSegmentationGroup = (
  segmentationGroups: SegmentationGroup[],
) => segmentationGroups.flatMap(field => field.items);

type CommonParams = {
  fromDate: Date;
  toDate: Date;
  reportFilters: ReportFilterFields;
};

export const formatCommonReportParams = (params: CommonParams) => {
  const { fromDate, reportFilters, toDate } = params;
  return {
    startDate: formatUTCDate(fromDate, 'yyyy-MM-dd'),
    endDate: formatUTCDate(toDate, 'yyyy-MM-dd'),
    userIds: reportFilters.users?.map(u => u.id).join(','),
    itemIds: selectedItemsToString(reportFilters.segmentationItems),
    status: selectedItemsToString(reportFilters.statusItems),
    incidences: selectedItemsToString(reportFilters.incidencesItems),
    siteIds: selectedItemsToString(reportFilters.siteIds),
    hasHoliday: reportFilters.hasHoliday,
    hasTimeOff: reportFilters.hasTimeOff,
    includeDeactivated: reportFilters.includeDeactivated,
  };
};

const addLeadingZero = (time: number) => time.toString().padStart(2, '0');

const processHours = (number: number) => {
  // Truncate hours to have only 2 decimal digits (in order to avoid rounding up based on more than 2 decimals)
  const truncatedHours = Math.trunc(number * 100) / 100;
  const minutesInDecimal = truncatedHours % 1;
  return {
    truncatedHours,
    minutesInDecimal,
    parsedMinutes: Math.round(Math.abs(minutesInDecimal * 60)),
  };
};

export const processHoursForDisplay = (
  number: number,
  hourLeadingZero = true,
) => {
  const { parsedMinutes, truncatedHours } = processHours(number);
  return {
    minutes: addLeadingZero(parsedMinutes),
    hours: hourLeadingZero
      ? addLeadingZero(Math.trunc(truncatedHours))
      : `${Math.trunc(truncatedHours)}`,
  };
};

export const parseHoursAndMinutesDisplay = (
  hoursDecimal: number,
  hideMinutesIfZero = false,
): [string, TOptions] => {
  const { hours, minutes } = processHoursForDisplay(hoursDecimal);
  return hideMinutesIfZero && minutes === '00'
    ? ['time_tracking_common:hours_abbreviated', { hours }]
    : [
        'time_tracking_common:hours_and_minutes_abbreviated',
        { hours, minutes },
      ];
};

const formatDecimalHours = (hours: number) => {
  const { minutesInDecimal, parsedMinutes, truncatedHours } =
    processHours(hours);
  const onlyMinuteFraction = Math.abs(truncatedHours) < 1;
  if (onlyMinuteFraction) {
    return `${Math.sign(truncatedHours) * parsedMinutes}m`;
  }
  if (minutesInDecimal === 0) {
    return `${truncatedHours}h`;
  }
  return `${Math.trunc(truncatedHours)}h ${parsedMinutes}m`;
};

export const displayHoursAndMinutes = (
  number: number,
  enforceSign = false,
  completeFormat = false,
) =>
  `${enforceSign && number > 0 ? '+' : ''}${
    completeFormat
      ? formatCompleteDecimalHours(number)
      : formatDecimalHours(number)
  }`;

export const formatCompleteDecimalHours = (
  hours: number,
  enforceSign = false,
) => {
  const { parsedMinutes, truncatedHours } = processHours(hours);
  const sign = enforceSign && hours > 0 ? '+' : '';
  const time = `${addLeadingZero(Math.trunc(truncatedHours))}h ${addLeadingZero(parsedMinutes)}m`;
  return `${sign}${time}`;
};

export const formatMetrics = (hours?: number) =>
  formatCompleteDecimalHours(hours || 0);

export const isCurrentDay = (date: string | Date, timezone?: string) =>
  isSameDay(
    new Date(date),
    timezone ? parseLocalDateWithTimezone(new Date(), timezone) : new Date(),
  );

export const TIMESHEET_HEADERS = [
  'DATE',
  'PROGRAMMED',
  'WORKED',
  'INCIDENCES',
  '',
];

export const parseDateToDisplay = (date: string, user: MeUser) => ({
  date: formatUTCLocalDate(new Date(date), user, 'MMMM, d'),
  dayOfWeek: formatUTCLocalDate(new Date(date), user, 'EEEE'),
});

const dateTypeLabel = {
  [TrackingDayType.WORK]: 'programmed_day',
  [TrackingDayType.FREE]: 'free_day',
  [TrackingDayType.VACATION]: 'vacations',
};

export const dateStatusTag = (
  isCurrentDate: boolean,
  status: UserTrackingStatus,
  dateType: TrackingDayType,
  palette: Palette,
  translate: LokaliseTFunction,
) => {
  const userActiveOrWorkday =
    status === UserTrackingStatus.ACTIVE || dateType === TrackingDayType.WORK;
  if (isCurrentDate && userActiveOrWorkday) {
    return {
      label: translate(
        status === UserTrackingStatus.ACTIVE ? 'clock_running' : 'ongoing_day',
      ),
      type: 'success' as const,
      border: true,
      ...(status !== UserTrackingStatus.INACTIVE && {
        backgroundColor: statusColor(palette)[status],
        color: statusColor(palette)[status], // using alt colors?
      }),
    };
  }
  return {
    label: translate(dateTypeLabel[dateType]),
    backgroundColor: palette.common.white,
    color: palette.grey[600],
    type: 'neutral' as const,
    border: true,
  };
};

// MIN = Minimum expected value
// MAX = Maximum expected value
// Function to normalise the values
export const normaliseBarValue = (value: number, min = 0, max = 100) =>
  ((value - min) * 100) / (max - min);

type PartialTimeTrackingEntry = Pick<TimeTrackingEntry, 'id' | 'time' | 'type'>;

export const pairEntries = (
  entries: PartialTimeTrackingEntry[],
  sort: 'ASC' | 'DESC' = 'DESC',
): PairedEntry[] => {
  if (!entries?.length) {
    return [];
  }
  const pairedResults: PairedEntry[] = [];
  const sortedEntries = [...entries].sort(
    (a, b) => new Date(b.time).getTime() - new Date(a.time).getTime(),
  );
  for (let i = 0; i < sortedEntries.length; i++) {
    const entry = sortedEntries[i];
    if (entry.type === EntryType.END) {
      const nextStart = sortedEntries.findIndex(
        e =>
          e.type === EntryType.START &&
          !pairedResults.find(pair => pair.start?.id === e.id),
      );
      pairedResults.push({
        id:
          nextStart < 0
            ? `${entry.id}`
            : `${sortedEntries[nextStart].id}-${entry.id}`,
        end: entry,
        start: sortedEntries[nextStart],
      });
    } else if (
      entry.type === EntryType.START &&
      !pairedResults.find(pair => pair.start?.id === entry.id)
    ) {
      pairedResults.push({
        id: `${entry.id}`,
        start: entry,
      });
    }
  }
  return pairedResults.sort((a, b) => {
    if (a.start === undefined) {
      return sort === 'ASC' ? 1 : -1;
    }
    const comparison =
      new Date(a.start?.time || 0).getTime() -
      new Date(b.start?.time || 0).getTime();
    return sort === 'ASC' ? comparison : -comparison;
  });
};

export const formatEntryTime = (timestamp: string) =>
  formatToTime(new Date(timestamp));

export const parseEntryName = (
  startTime: string | undefined,
  endTime: string | undefined,
  ongoingEntry: boolean,
  translate: LokaliseTFunction,
) => {
  const unsetTimeEntries = !startTime && !endTime;
  if (unsetTimeEntries) {
    return translate('new_entry');
  }
  const formattedStart = startTime
    ? formatEntryTime(startTime)
    : TIME_PLACEHOLDER;
  const formattedEnd = endTime ? formatEntryTime(endTime) : TIME_PLACEHOLDER;
  return ongoingEntry && !endTime
    ? `${formattedStart} - ${translate('on_course')}`
    : `${formattedStart} - ${formattedEnd}`;
};

export const timestampInReferenceDate = (
  timestamp: Date | string,
  reference: string,
) => {
  const referenceDate = new Date(reference);
  const parsedTimestamp = new Date(timestamp);
  parsedTimestamp.setFullYear(referenceDate.getFullYear());
  parsedTimestamp.setMonth(referenceDate.getMonth());
  parsedTimestamp.setDate(referenceDate.getDate());
  return parsedTimestamp.toISOString();
};

const replaceValue = <T>(
  base: Record<string, unknown> | undefined,
  values: Record<string, unknown> | undefined,
): T => {
  const modified = { ...base };
  Object.keys(modified).forEach(key => {
    if (modified[key] && values) {
      modified[key] = values[key];
    }
  });
  return modified as T;
};

export const buildCycleUpdateParams = (
  updatedPair: EntryFormFields,
  dirtyFields: DirtyEntryFormFields,
): EntryFormFields => {
  const params = {
    start: replaceValue<EntryField>(dirtyFields.start, updatedPair.start),
    end: replaceValue<EntryField>(dirtyFields.end, updatedPair.end),
  };
  return params;
};

export const addDayToIsoString = (date: string) =>
  `${format(addDays(parseISO(date), 1), 'yyyy-MM-dd')}T00:00:00`;

/** Resolves form siteId when `buildCycleUpdateParams` omits unchanged fields. */
const resolveEffectiveEntrySiteId = (
  values: Partial<EntryField>,
  currentEntry?: Partial<TimeTrackingEntry>,
): number => {
  if (values.siteId !== undefined) {
    return values.siteId;
  }
  if (currentEntry?.isRemoteWork) {
    return REMOTE_WORK_SITE_VALUE;
  }
  if (currentEntry?.siteId != null) {
    return currentEntry.siteId;
  }
  return -1;
};

export const updatedEntry = ({
  values,
  currentEntry,
  summaryDay,
  sites,
  timezone,
  applyNightShift = false,
}: ModifiedEntryFields) => {
  const referenceSummaryDay = applyNightShift
    ? addDayToIsoString(summaryDay)
    : summaryDay;
  const effectiveSiteId = resolveEffectiveEntrySiteId(values, currentEntry);
  const isRemoteWork = effectiveSiteId === REMOTE_WORK_SITE_VALUE;
  const siteDefined = effectiveSiteId !== -1 && !isRemoteWork;
  const resolveLocation = () => {
    if (isRemoteWork) return currentEntry?.location ?? null;
    if (
      siteDefined &&
      (!currentEntry?.location || currentEntry?.source === EntrySource.WEB)
    ) {
      return sites.find(s => s.id === effectiveSiteId)?.location;
    }
    return currentEntry?.location;
  };
  const newLocation = resolveLocation();
  const currentEntryDate = currentEntry?.time
    ? new Date(currentEntry.time)
    : new Date();
  const referencedTime = timestampInReferenceDate(
    values.time || currentEntryDate,
    referenceSummaryDay,
  );
  const parsedReference = parseLocalDateWithTimezone(
    new Date(referencedTime),
    timezone,
    'from',
  );
  return {
    time: parsedReference.toISOString(),
    siteId: isRemoteWork || effectiveSiteId === -1 ? null : effectiveSiteId,
    comment: values?.comment,
    location: newLocation,
    isRemoteWork,
  };
};

export const getUserFields = (fields: string[]) => ({
  userFields: fields
    .filter(f => ProfileField[f as keyof typeof ProfileField])
    .toString(),
  userCustomFields: fields
    .filter(f => !ProfileField[f as keyof typeof ProfileField])
    .toString(),
});

export const parseEntriesIntoTimezone = (
  entries: TimeTrackingEntry[],
  timezone: string,
) =>
  entries.map(e => ({
    ...e,
    time: parseLocalDateWithTimezone(
      new Date(e.time),
      timezone,
      'from',
    ).toISOString(),
  }));

const isNextDay = (dateISO1: string, dateISO2: string) => {
  const startOfDate1 = startOfDay(parseISO(dateISO1));
  const nextDay = addDays(startOfDate1, 1);
  return isSameDay(nextDay, startOfDay(parseISO(dateISO2)));
};

export const endIsNightShifted = (
  endTime: Date | string,
  startTime: Date | string,
) => {
  // Changed any value of the entries but times remain the same
  if (typeof startTime === 'string' && typeof endTime === 'string') {
    return isNextDay(startTime, endTime);
  }
  // Changed endTime
  if (isValid(endTime)) {
    const parsedStartTime =
      typeof startTime === 'string' ? parseISO(startTime) : startTime;
    return endTime < parsedStartTime;
  }
  // Changed startTime
  const parsedEndTime =
    typeof endTime === 'string' ? parseISO(endTime) : endTime;
  return parsedEndTime < startTime;
};

const entrySourceUpdateMapping: Partial<Record<EntrySource, EntrySource>> = {
  [EntrySource.AUTO_CLOSE]: EntrySource.MANUAL,
  [EntrySource.DAY_CHANGE]: EntrySource.MANUAL,
  [EntrySource.INTEGRATION]: EntrySource.MANUAL,
  // Other entry sources are not updated
};

export const updateEntrySourceIfTimeChanged = (
  timeChanged: boolean,
  source: EntrySource = EntrySource.MANUAL,
): EntrySource =>
  timeChanged ? entrySourceUpdateMapping[source] || source : source;

/**
 * Calculates the time span between startTime and endTime in decimal hours
 * @param startTime - Start time in "HH:MM" format
 * @param endTime - End time in "HH:MM" format
 * @param allowNightShift - Whether to allow night shifts
 * @returns Time span in decimal hours
 */
export const calculateTimeSpanInHours = (
  startTime: string,
  endTime: string,
  allowNightShift = true,
): number => {
  const [startHour, startMinute] = startTime.split(':').map(Number);
  const [endHour, endMinute] = endTime.split(':').map(Number);

  const isNightShift =
    allowNightShift &&
    (startHour > endHour || (startHour === endHour && startMinute > endMinute));

  const startTotalMinutes = startHour * 60 + startMinute;
  const endTotalMinutes = isNightShift
    ? (endHour + 24) * 60 + endMinute
    : endHour * 60 + endMinute;

  // Calculate difference in minutes and convert to decimal hours
  return (endTotalMinutes - startTotalMinutes) / 60;
};

type ProcessedEntry = {
  pairId?: string;
  start: string | null;
  end: string | null;
  isNightShift: boolean;
  hasLocationIncidence: boolean;
};

export type TimeSlotWithEntries = {
  startTime: string | null;
  endTime: string | null;
  timeSpan: number | null;
  entries: ProcessedEntry[];
  workedHours: number;
};

/**
 * Groups timeSlots with timeTrackingEntries based on if the entries are contained in a timeslot.
 * Entries not contained in any timeslot are grouped.
 * @param timeSlots - Array of time slots (startTime and endTime in "HH:MM" format)
 * @param timeTrackingEntries - Array of time tracking entries
 * @param populateTimeSlots - Whether to populate time slots with empty entries
 * @returns Array of TimeSlotWithEntries sorted by timeSlots startTime
 */

export const groupTimeSlotWithEntries = (
  timeSlots: TimeSlot[],
  timeTrackingEntries: TimeTrackingEntry[],
  populateTimeSlots: boolean,
): TimeSlotWithEntries[] => {
  // Sort time slots by startTime
  const sortedTimeSlots = [...timeSlots].sort((a, b) =>
    a.startTime.localeCompare(b.startTime),
  );

  const entryPairs = pairEntries(
    timeTrackingEntries.map(entry => ({
      ...entry,
      id: new Date(entry.time).getTime(),
      type: entry.type as EntryType,
    })),
    'ASC',
  );

  // Create a map for time slots
  const timeSlotsMap = new Map<string, TimeSlotWithEntries>();

  // Initialize time slots with empty entries only if populateTimeSlots is true
  if (populateTimeSlots) {
    sortedTimeSlots.forEach(slot => {
      timeSlotsMap.set(`${slot.startTime}-${slot.endTime}`, {
        startTime: slot.startTime,
        endTime: slot.endTime,
        timeSpan: calculateTimeSpanInHours(slot.startTime, slot.endTime),
        entries: [],
        workedHours: 0,
      });
    });
  }

  // Categorize each entry pair to corresponding time slot or create an orphan entry
  let currentOrphanGroup = 0;
  let lastEntryWasInTimeSlot = false;
  const orphanSlotMap = new Map<number, TimeSlotWithEntries>();

  entryPairs.forEach(entryPair => {
    const entryStart = entryPair.start?.time
      ? new Date(entryPair.start.time)
      : null;
    const entryEnd = entryPair.end?.time ? new Date(entryPair.end.time) : null;

    const formattedEntry = {
      pairId: entryPair.id,
      start: entryStart ? formatUTCDate(entryStart, 'HH:mm') : null,
      end: entryEnd ? formatUTCDate(entryEnd, 'HH:mm') : null,
      isNightShift:
        entryStart && entryEnd
          ? endIsNightShifted(entryEnd, entryStart)
          : false,
      hasLocationIncidence: Boolean(
        entryPair.start?.locationIncidence || entryPair.end?.locationIncidence,
      ),
    };

    // Calculate the duration of this entry in hours (if end time is null, it's an ongoing entry, won't count for worked hours)
    const entryDuration =
      entryStart && entryEnd
        ? (entryEnd.getTime() - entryStart.getTime()) / (1000 * 60 * 60)
        : 0;

    // Check if the entry fits into any time slot
    let foundSlot = false;

    // Avoid showing slots with no start time
    if (!entryStart) {
      return;
    }

    for (const [key, timeSlot] of timeSlotsMap.entries()) {
      const [slotStartHour, slotStartMinute] =
        timeSlot.startTime?.split(':').map(Number) || [];
      const [slotEndHour, slotEndMinute] =
        timeSlot.endTime?.split(':').map(Number) || [];

      // Create slot boundary dates (using the entry's date part for consistency)
      const slotStartDate = new Date(entryStart!);
      slotStartDate.setHours(slotStartHour, slotStartMinute, 0, 0);

      const slotEndDate = new Date(entryStart!);
      slotEndDate.setHours(slotEndHour, slotEndMinute, 0, 0);

      if (
        slotEndHour < slotStartHour ||
        (slotEndHour === slotStartHour && slotEndMinute < slotStartMinute)
      ) {
        slotEndDate.setDate(slotEndDate.getDate() + 1);
      }

      // To be considered as within the time slot, one of the following must be true:
      // 1. The entry start is between the slot start and end
      // 2. The entry end is between the slot start and end
      // 3. The entry start is before the slot start and the entry end is after the slot end
      const startEntryIsBetweenSlots =
        entryStart! >= slotStartDate && entryStart! < slotEndDate;
      const endEntryIsBetweenSlots =
        entryEnd && entryEnd > slotStartDate && entryEnd <= slotEndDate;
      const entriesContainsSlot =
        entryEnd && entryStart <= slotStartDate && entryEnd >= slotEndDate;

      if (
        startEntryIsBetweenSlots ||
        endEntryIsBetweenSlots ||
        entriesContainsSlot
      ) {
        timeSlot.entries.push(formattedEntry);
        timeSlot.workedHours += entryDuration;
        foundSlot = true;
        timeSlotsMap.set(key, timeSlot);
        break;
      }
    }

    // If entry doesn't fit into any time slot, add it to orphan entries
    if (!foundSlot) {
      // We're transitioning from a regular slot to an orphan entry
      if (lastEntryWasInTimeSlot) {
        currentOrphanGroup++;
      }

      // Get or create the orphan slot for the current orphan group
      if (!orphanSlotMap.has(currentOrphanGroup)) {
        orphanSlotMap.set(currentOrphanGroup, {
          startTime: null,
          endTime: null,
          timeSpan: null,
          entries: [],
          workedHours: 0,
        });
      }

      const orphanSlot = orphanSlotMap.get(currentOrphanGroup);
      if (orphanSlot) {
        orphanSlot.entries.push(formattedEntry);
        orphanSlot.workedHours += entryDuration;
      }

      lastEntryWasInTimeSlot = false;
    } else {
      lastEntryWasInTimeSlot = true;
    }
  });

  // Flatten maps into an array
  const slots = [timeSlotsMap, orphanSlotMap].flatMap(slot =>
    Array.from(slot.values()),
  );
  slots.sort((a, b) => {
    // Comparing orphan entries (not associated to a timeSlot)
    if (a.startTime === null && b.startTime === null) {
      return (
        a.entries?.[0]?.start?.localeCompare(b.entries?.[0]?.start || '') || 0
      );
    }
    // Comparing orphan entry against timeSlotted entry
    if (a.startTime === null) {
      const firstStartEntry = a.entries?.[0]?.start || ''; // In case the start entry is null (shouldn't happen but just in case)
      return firstStartEntry.localeCompare(b.startTime || '');
    }
    // Comparing timeSlotted entry against orphan entry
    if (b.startTime === null) {
      return a.startTime?.localeCompare(b.entries?.[0]?.start || '') || 0;
    }
    // Comparing timeSlotted entries
    return a.startTime?.localeCompare(b.startTime || '') || 0;
  });
  return slots;
};

/**
 * Gets the most recent entry pair (start-end) from time tracking entries
 * @param timeTrackingEntries - Array of time tracking entries with time in ISO format and type (START/END)
 * @returns Object with the newest start and end times, end may be null if there's no matching end entry
 */
export const getNewestEntryPair = (
  timeTrackingEntries: TimeTrackingEntry[],
): ProcessedEntry => {
  if (!timeTrackingEntries || timeTrackingEntries.length === 0) {
    return {
      pairId: undefined,
      start: null,
      end: null,
      isNightShift: false,
      hasLocationIncidence: false,
    };
  }

  // Sort entries by time (newest first)
  const sortedEntries = [...timeTrackingEntries].sort(
    (a, b) => new Date(b.time).getTime() - new Date(a.time).getTime(),
  );

  // Find the newest START entry
  const newestStart = sortedEntries.find(entry => entry.type === 'START');

  if (!newestStart) {
    return {
      pairId: undefined,
      start: null,
      end: null,
      isNightShift: false,
      hasLocationIncidence: false,
    };
  }

  // Find the newest END entry that's after the start entry
  const newestStartTime = new Date(newestStart.time);
  const newestEnd = sortedEntries.find(
    entry => entry.type === 'END' && new Date(entry.time) > newestStartTime,
  );

  return {
    start: formatUTCDate(newestStart.time, 'HH:mm'),
    end: newestEnd ? formatUTCDate(newestEnd.time, 'HH:mm') : null,
    isNightShift: newestEnd
      ? endIsNightShifted(newestEnd.time, newestStart.time)
      : false,
    hasLocationIncidence:
      newestStart.locationIncidence || Boolean(newestEnd?.locationIncidence),
  };
};

export const daySummaryWithAutoClosedEntry = (daySummary: DaySummary) =>
  daySummary.timeTrackingEntries.some(
    entry => entry.source === EntrySource.AUTO_CLOSE,
  );

// Discard duplicates and hidden incidences (also non-tag incidences if alsoHideNonTag is true)
export const parseIncidences = (
  incidences: Incidence[],
  alsoHideNonTag = false,
) => {
  const incidencesToHide = alsoHideNonTag
    ? [...HIDDEN_INCIDENCES, ...NON_TAG_INCIDENCES]
    : HIDDEN_INCIDENCES;
  return [
    ...new Set(
      incidences.filter(
        i =>
          Object.keys(Incidence).includes(i) && !incidencesToHide.includes(i),
      ),
    ),
  ];
};

export const getPeriodFromStringDates = (
  startDate: string,
  endDate: string,
) => {
  const startUTC0 = new Date(`${startDate}T00:00:00`);
  const endUTC0 = new Date(`${endDate}T00:00:00`);
  /* Need to bring dates to UTC0 because recognizePeriod checks for weekdays
  and new Date() may return different dates based on the local timezone */
  return recognizePeriod(startUTC0, endUTC0);
};

type DayScheduleStatusParams = Pick<
  DaySummary,
  'isWorkday' | 'scheduleSource'
> & {
  holidays?: DaySummary['holidays'];
  timeOffRequests?: DaySummary['timeOffRequests'];
};

export const parseDayScheduleStatus = ({
  holidays = [],
  isWorkday,
  scheduleSource,
  timeOffRequests = [],
}: DayScheduleStatusParams) => {
  // Day not planned within a schedule
  const isUnscheduledDay = scheduleSource === ScheduleSource.NO_SCHEDULE;
  const isHolidayOrTimeOff = holidays[0] || timeOffRequests[0];
  const isRestDay = !isWorkday && scheduleSource !== ScheduleSource.NO_SCHEDULE;

  return {
    ...(isHolidayOrTimeOff && {
      holiday: {
        name:
          holidays[0]?.holidayName || timeOffRequests[0]?.timeOffPolicyTypeName,
        type: holidays[0]?.holidayId
          ? ('HOLIDAY' as const)
          : ('TIME_OFF' as const),
        icon:
          iconsPolicyTypes[timeOffRequests[0]?.timeOffPolicyTypeIcon] ||
          IconCalendarEvent,
        hours: timeOffRequests[0]?.hours || 0,
      },
    }),
    isScheduledDayOff: isRestDay,
    isUnscheduledDay,
  };
};

export const mapTrueKeys = <T extends string>(obj?: Record<string, boolean>) =>
  Object.entries(obj ?? {})
    .filter(([, v]) => v)
    .map(([k]) => k as T);

// FIXME: REMOVE in future PR - Override to isWorkday until backend fixes the workday logic
export const getEffectiveWorkdayStatus = (dayInfo: DaySummary) => {
  // Holidays with shifts are considered workdays
  const isHolidayOnShift =
    dayInfo.scheduleSource === ScheduleSource.SHIFT &&
    dayInfo.holidays.length > 0;
  return isHolidayOnShift || dayInfo.isWorkday;
};

export const shouldShowTimeSlotInfo = (dayInfo: DaySummary): boolean => {
  if (
    dayInfo.holidays.length > 0 &&
    dayInfo.scheduleSource !== ScheduleSource.SHIFT
  ) {
    return false;
  }

  return dayInfo.estimatedHours > 0 || dayInfo.isWorkday;
};

export const matchShiftAssociations = (
  shiftAssociations: ShiftAssociations[],
  timeSlot: TimeSlot | TimeSlotWithEntries,
) =>
  shiftAssociations.find(shift =>
    shift.timeSlots.some(
      slot =>
        slot.startTime === timeSlot.startTime &&
        slot.endTime === timeSlot.endTime,
    ),
  );

export const getTimeSlotName = (
  dayInfo: DaySummary,
  timeSlotWithEntries: TimeSlotWithEntries,
) => {
  if (dayInfo.shiftAssociations) {
    return matchShiftAssociations(
      dayInfo.shiftAssociations,
      timeSlotWithEntries,
    )?.name;
  }
  return dayInfo.workScheduleName;
};

/**
 * Gets the scheduled hours for a specific time slot, handling single/multiple slots
 * @param timeSlotWithEntries - The time slot with entries
 * @param dayInfo - The day summary information
 * @returns scheduled hours for this time slot
 */
export const getAdjustedTimeSpan = (
  timeSlotWithEntries: TimeSlotWithEntries,
  dayInfo: DaySummary,
): number | null => {
  if (!dayInfo.scheduledHours) {
    return 0;
  }

  // Single time slot /no time slots: use day's scheduled hours
  if (!dayInfo.timeSlots || dayInfo.timeSlots.length <= 1) {
    return dayInfo.scheduledHours;
  }

  // Multiple time slots: use the slot's timeSpan
  return timeSlotWithEntries.timeSpan;
};

/**
 * Removes the seconds from a timestamp string (hh:mm:ss) to return only hours and minutes
 * @param time - The timestamp string to slice
 * @returns The sliced time string
 */
export const sliceTimeToHoursAndMinutes = (time: string | null) =>
  time?.slice(0, 5) || '';

/**
 * Converts a time string (hh:mm) to minutes (number)
 * @param time - The time string to convert
 * @returns The time in minutes
 */
export const stringTimeToMinutes = (time: string | null) => {
  if (!time) {
    return 0;
  }
  const [hours, minutes] = sliceTimeToHoursAndMinutes(time).split(':');
  return Number(hours) * 60 + Number(minutes);
};

// Type for selected site in map
export type SelectedSite = {
  siteId: number;
  siteName: string;
  location: SiteLocation;
  markingLocation: SiteCoordinates | null;
  entrySource: EntrySource;
  entryType: EntryType;
};

// Type for form site IDs (legacy, still needed for map building)
export type FormSiteIds = {
  start: { siteId: number };
  end: { siteId: number };
};

// Build selected site from form values
export const buildSelectedSiteFromForm = (
  formValues: FormSiteIds | undefined,
  sites: UserSite[],
  dayInfo: DaySummary | undefined,
  entryType: EntryType,
): SelectedSite | null => {
  if (!formValues) return null;

  const formSiteId =
    entryType === EntryType.START
      ? formValues.start.siteId
      : formValues.end.siteId;

  if (!formSiteId || formSiteId === -1 || formSiteId === REMOTE_WORK_SITE_VALUE)
    return null;

  const site = sites.find(s => s.id === formSiteId);
  if (!site?.location) return null;

  // Try to get marking location and entry source from original entries
  let markingLocation: SiteCoordinates | null = null;
  let entrySource: EntrySource = EntrySource.WEB;
  if (dayInfo?.timeTrackingEntries) {
    const entry = dayInfo.timeTrackingEntries.find(e => e.type === entryType);
    if (entry) {
      markingLocation = entry.location ?? null;
      entrySource = entry.source ?? EntrySource.WEB;
    }
  }

  // If no original entry location, use site location (web entry)
  if (!markingLocation) {
    markingLocation = {
      latitude: site.location.latitude,
      longitude: site.location.longitude,
    };
    entrySource = EntrySource.WEB;
  }

  return {
    siteId: site.id,
    siteName: site.name,
    location: site.location,
    markingLocation,
    entrySource,
    entryType,
  };
};
