import { DispoStagedRecordType } from "@capakraken/db";
import {
  VacationType,
  normalizeCanonicalResourceIdentity,
  normalizeDispoRoleToken,
  normalizeDispoUtilizationToken,
} from "@capakraken/shared";
import { readWorksheetMatrix, toColumnLetter, type WorksheetCellValue } from "./read-workbook.js";
import {
  DISPO_PLANNING_SHEET,
  type ParsedPlanningAssignment,
  type ParsedPlanningAvailabilityRule,
  type ParsedPlanningVacation,
  type ParsedPlanningWorkbook,
  type ParsedUnresolvedRecord,
  deriveRoleTokens,
  normalizeNullableWorkbookValue,
  normalizeText,
} from "./shared.js";

const DISPO_HEADER_ROW = 5;
const DISPO_DATE_ROW = 2;
const DISPO_SLOT_ROW = 3;
const DISPO_DATA_START_ROW = 6;
const DISPO_EID_COLUMN = 3;
const DISPO_CHAPTER_COLUMN = 4;
const DISPO_TYPE_OF_WORK_COLUMN = 5;
const DISPO_UNIT_SPECIFIC_FIELD_COLUMN = 7;
const DISPO_PLANNING_START_COLUMN = 11;
const SLOT_HOURS = 4;
const WEEKDAY_LABELS = new Set(["MO", "DI", "MI", "DO", "FR", "SA", "SO"]);
const BERLIN_DATE_FORMATTER = new Intl.DateTimeFormat("en-CA", {
  day: "2-digit",
  month: "2-digit",
  timeZone: "Europe/Berlin",
  year: "numeric",
});

interface PlanningColumn {
  assignmentDate: Date;
  columnLetter: string;
  columnNumber: number;
  halfDayPart: "AFTERNOON" | "MORNING" | null;
  slotLabel: string;
  weekdayLabel: string | null;
}

interface PlanningRowMetadata {
  chapter: string | null;
  eid: string;
  typeOfWork: string | null;
  unitSpecificField: string | null;
}

interface AssignmentAccumulator {
  assignmentDate: Date;
  chapterToken: string | null;
  firstColumnNumber: number;
  hoursPerDay: number;
  isInternal: boolean;
  isTbd: boolean;
  isUnassigned: boolean;
  projectKey: string | null;
  rawToken: string;
  resourceExternalId: string;
  roleName: string | null;
  roleToken: string | null;
  slotCount: number;
  sourceRow: number;
  utilizationCategoryCode: string | null;
  warnings: Set<string>;
  winProbability: number | null;
}

interface VacationAccumulator {
  endDate: Date;
  firstColumnNumber: number;
  halfDayParts: Set<string>;
  holidayName: string | null;
  isPublicHoliday: boolean;
  note: string | null;
  rawToken: string;
  resourceExternalId: string;
  sourceRow: number;
  startDate: Date;
  vacationType: VacationType;
  warnings: Set<string>;
}

interface AvailabilityAccumulator {
  availableHours: number | null;
  effectiveEndDate: Date;
  effectiveStartDate: Date;
  firstColumnNumber: number;
  isResolved: boolean;
  percentage: number | null;
  rawToken: string;
  resourceExternalId: string;
  ruleType: string;
  sourceRow: number;
  warnings: Set<string>;
}

interface NaTokenHandlingResult {
  availableHours?: number | null;
  isPublicHoliday: boolean;
  kind: "availability" | "assignment" | "skip" | "vacation";
  note?: string | null;
  percentage?: number | null;
  projectKey?: string | null;
  ruleType?: string;
  vacationType?: VacationType;
  warning?: string;
}

interface ParsedAssignmentToken {
  chapterToken: string | null;
  isInternal: boolean;
  isTbd: boolean;
  isUnassigned: boolean;
  projectKey: string | null;
  roleName: string | null;
  roleToken: string | null;
  utilizationCategoryCode: string | null;
  winProbability: number | null;
}

function isWeekdayLabel(value: string | null): boolean {
  return value !== null && WEEKDAY_LABELS.has(value.toUpperCase());
}

function toDateOnlyInBerlin(value: WorksheetCellValue): Date | null {
  if (!(value instanceof Date)) {
    return null;
  }

  const parts = BERLIN_DATE_FORMATTER.formatToParts(value);
  const year = Number(parts.find((part) => part.type === "year")?.value);
  const month = Number(parts.find((part) => part.type === "month")?.value);
  const day = Number(parts.find((part) => part.type === "day")?.value);

  if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
    return null;
  }

  return new Date(Date.UTC(year, month - 1, day));
}

function getDateKey(value: Date): string {
  return value.toISOString().slice(0, 10);
}

function getSlotHalfDayPart(slotLabel: string | null): "AFTERNOON" | "MORNING" | null {
  const normalized = normalizeText(slotLabel)?.toLowerCase() ?? null;
  if (!normalized) {
    return null;
  }

  if (normalized.includes("9.-13")) {
    return "MORNING";
  }
  if (normalized.includes("14.-18")) {
    return "AFTERNOON";
  }

  return null;
}

function isPlanningSummaryRow(row: ReadonlyArray<WorksheetCellValue>): boolean {
  if ((row[0] ?? null) !== null || (row[1] ?? null) !== null) {
    return false;
  }

  const repeatedLabels = row
    .slice(DISPO_EID_COLUMN - 1, 9)
    .map((value) => normalizeNullableWorkbookValue(value))
    .filter((value): value is string => value !== null);
  if (repeatedLabels.length === 0) {
    return false;
  }

  const normalizedLabels = new Set(repeatedLabels.map((value) => value.toLowerCase()));
  const label = repeatedLabels[0] ?? null;

  return normalizedLabels.size === 1 && label !== null && label.startsWith("(") && label.endsWith(")");
}

function buildPlanningColumns(rows: ReadonlyArray<ReadonlyArray<WorksheetCellValue>>) {
  const columns: PlanningColumn[] = [];
  const headerWidth = Math.max(rows[DISPO_DATE_ROW - 1]?.length ?? 0, rows[DISPO_SLOT_ROW - 1]?.length ?? 0);

  for (let columnNumber = DISPO_PLANNING_START_COLUMN; columnNumber <= headerWidth; columnNumber += 1) {
    const slotLabel = normalizeNullableWorkbookValue(rows[DISPO_SLOT_ROW - 1]?.[columnNumber - 1]);
    if (!slotLabel) {
      continue;
    }

    const currentHeaderValue = rows[DISPO_DATE_ROW - 1]?.[columnNumber - 1] ?? null;
    const previousHeaderLabel = normalizeNullableWorkbookValue(rows[DISPO_DATE_ROW - 1]?.[columnNumber - 2]);
    const currentHeaderLabel = normalizeNullableWorkbookValue(currentHeaderValue);
    const nextHeaderValue = rows[DISPO_DATE_ROW - 1]?.[columnNumber] ?? null;

    const assignmentDate =
      toDateOnlyInBerlin(currentHeaderValue) ??
      toDateOnlyInBerlin(nextHeaderValue);

    if (!assignmentDate) {
      continue;
    }

    const weekdayLabel = isWeekdayLabel(currentHeaderLabel)
      ? currentHeaderLabel
      : isWeekdayLabel(previousHeaderLabel)
        ? previousHeaderLabel
        : null;

    columns.push({
      assignmentDate,
      columnLetter: toColumnLetter(columnNumber),
      columnNumber,
      halfDayPart: getSlotHalfDayPart(slotLabel),
      slotLabel,
      weekdayLabel,
    });
  }

  return columns;
}

function normalizePlanningToken(token: string): string {
  return token
    .trim()
    .replace(/\s+/g, " ")
    .replace(/\s+(?:HB|SB)_?\s*$/i, "")
    .trim();
}

function extractBracketTokens(token: string): string[] {
  return Array.from(token.matchAll(/\[([^\]]+)\]/g), (match) => match[1]?.trim() ?? "").filter(Boolean);
}

function extractUtilizationToken(token: string): { utilizationToken: string | null; winProbability: number | null } {
  const matches = Array.from(token.matchAll(/\{([A-Z]+)(\d{0,3})\}/gi));
  const lastMatch = matches.at(-1);
  if (!lastMatch) {
    return {
      utilizationToken: null,
      winProbability: null,
    };
  }

  const utilizationToken = lastMatch[1]?.toUpperCase() ?? null;
  const winProbability = lastMatch[2] ? Number(lastMatch[2]) : null;
  return {
    utilizationToken,
    winProbability: Number.isFinite(winProbability) ? winProbability : null,
  };
}

function extractRoleToken(token: string, metadata: PlanningRowMetadata): string | null {
  const explicitRoleToken = token.match(/^(2D|3D|PM|AD)\b/i)?.[1]?.toUpperCase() ?? null;
  if (explicitRoleToken) {
    return explicitRoleToken;
  }

  return deriveRoleTokens(metadata.chapter, metadata.typeOfWork, metadata.unitSpecificField)[0] ?? null;
}

function extractProjectKey(token: string): string | null {
  const bracketTokens = extractBracketTokens(token).filter((entry) => !entry.startsWith("_"));
  const lastToken = bracketTokens.at(-1) ?? null;
  return lastToken && lastToken.toLowerCase() !== "tbd" ? lastToken : null;
}

function slugifyProjectKeyFragment(value: string): string {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 48);
}

function extractLabel(token: string): string | null {
  const stripped = token
    .replace(/^(2D|3D|PM|AD)\s+/i, "")
    .replace(/\[[^\]]+\]/g, " ")
    .replace(/\{[^}]+\}/g, " ")
    .replace(/\s+(?:HB|SB)_?\s*$/i, " ")
    .replace(/\s+/g, " ")
    .trim();

  return stripped.length > 0 ? stripped : null;
}

function parsePercentage(value: string): number | null {
  const percentageMatch = value.match(/(\d+(?:[.,]\d+)?)\s*%/);
  if (percentageMatch) {
    const normalized = Number(percentageMatch[1]?.replace(",", "."));
    return Number.isFinite(normalized) ? normalized : null;
  }

  const fteMatch = value.match(/FTE:\s*(\d+(?:[.,]\d+)?)/i);
  if (fteMatch) {
    const normalized = Number(fteMatch[1]?.replace(",", "."));
    return Number.isFinite(normalized) ? Math.round(normalized * 10000) / 100 : null;
  }

  return null;
}

function classifyNaPlanningToken(rawToken: string): NaTokenHandlingResult {
  const normalized = rawToken.toLowerCase();
  const label = extractLabel(rawToken);
  const { utilizationToken } = extractUtilizationToken(rawToken);

  if (!rawToken.toUpperCase().startsWith("[_NA]")) {
    return {
      isPublicHoliday: false,
      kind: "assignment",
    };
  }

  if (normalized.includes("public holiday")) {
    return {
      isPublicHoliday: true,
      kind: "vacation",
      note: label,
      vacationType: VacationType.PUBLIC_HOLIDAY,
    };
  }

  if (normalized.includes("part-time")) {
    return {
      availableHours: null,
      isPublicHoliday: false,
      kind: "availability",
      percentage: parsePercentage(rawToken),
      ruleType: "PART_TIME",
    };
  }

  if (normalized.includes("not bookable")) {
    return {
      availableHours: 0,
      isPublicHoliday: false,
      kind: "availability",
      percentage: 0,
      ruleType: "NOT_BOOKABLE",
    };
  }

  if (normalized.includes("reduced spanish working hours")) {
    return {
      availableHours: 7,
      isPublicHoliday: false,
      kind: "availability",
      percentage: 87.5,
      ruleType: "REDUCED_SPANISH_WORKING_HOURS",
      warning: "Assumed 7h/day for reduced Spanish working hours",
    };
  }

  if (normalized.includes("parental leave")) {
    return {
      isPublicHoliday: false,
      kind: "vacation",
      note: label,
      vacationType: VacationType.OTHER,
    };
  }

  if (utilizationToken && utilizationToken !== "NA" && utilizationToken !== "UN") {
    const projectKey = label ? `NA-${slugifyProjectKeyFragment(label)}` : null;
    return {
      isPublicHoliday: false,
      kind: projectKey ? "assignment" : "skip",
      note: label,
      projectKey,
    };
  }

  return {
    isPublicHoliday: false,
    kind: "skip",
    note: label,
  };
}

function buildAssignmentAccumulator(
  column: PlanningColumn,
  metadata: PlanningRowMetadata,
  rawToken: string,
): AssignmentAccumulator | null {
  const roleToken = extractRoleToken(rawToken, metadata);
  const roleName = normalizeDispoRoleToken(roleToken);
  const { utilizationToken, winProbability } = extractUtilizationToken(rawToken);
  const utilizationCategoryCode = normalizeDispoUtilizationToken(utilizationToken);
  const naHandling = classifyNaPlanningToken(rawToken);
  const projectKey = extractProjectKey(rawToken) ?? naHandling.projectKey ?? null;
  const isTbd = /\[tbd\]/i.test(rawToken);
  const isUnassigned = utilizationToken === "UN";
  const isInternal = ["MD", "MO", "PD"].includes(utilizationToken ?? "");

  if (isUnassigned) {
    return null;
  }

  return {
    assignmentDate: column.assignmentDate,
    chapterToken: roleToken,
    firstColumnNumber: column.columnNumber,
    hoursPerDay: SLOT_HOURS,
    isInternal,
    isTbd,
    isUnassigned: false,
    projectKey,
    rawToken,
    resourceExternalId: metadata.eid,
    roleName,
    roleToken,
    slotCount: 1,
    sourceRow: 0,
    utilizationCategoryCode,
    warnings: new Set<string>(),
    winProbability,
  };
}

function buildVacationAccumulator(
  column: PlanningColumn,
  metadata: PlanningRowMetadata,
  rawToken: string,
  vacationType: VacationType,
  input: { holidayName?: string | null; isPublicHoliday: boolean; note?: string | null },
): VacationAccumulator {
  const halfDayParts = new Set<string>();
  if (column.halfDayPart) {
    halfDayParts.add(column.halfDayPart);
  }

  return {
    endDate: column.assignmentDate,
    firstColumnNumber: column.columnNumber,
    halfDayParts,
    holidayName: input.holidayName ?? null,
    isPublicHoliday: input.isPublicHoliday,
    note: input.note ?? null,
    rawToken,
    resourceExternalId: metadata.eid,
    sourceRow: 0,
    startDate: column.assignmentDate,
    vacationType,
    warnings: new Set<string>(),
  };
}

function buildAvailabilityAccumulator(
  column: PlanningColumn,
  metadata: PlanningRowMetadata,
  rawToken: string,
  input: {
    availableHours?: number | null;
    percentage?: number | null;
    ruleType?: string;
    warning?: string;
  } = {},
): AvailabilityAccumulator {
  const percentage = input.percentage ?? parsePercentage(rawToken);
  const availableHours = percentage !== null
    ? Math.round((percentage / 100) * 8 * 100) / 100
    : (input.availableHours ?? (8 - SLOT_HOURS));
  const warnings = new Set<string>();
  if (input.warning) {
    warnings.add(input.warning);
  }

  return {
    availableHours,
    effectiveEndDate: column.assignmentDate,
    effectiveStartDate: column.assignmentDate,
    firstColumnNumber: column.columnNumber,
    isResolved: false,
    percentage,
    rawToken,
    resourceExternalId: metadata.eid,
    ruleType: input.ruleType ?? "PART_TIME",
    sourceRow: 0,
    warnings,
  };
}

export async function parseDispoPlanningWorkbook(
  workbookPath: string,
): Promise<ParsedPlanningWorkbook> {
  const rows = await readWorksheetMatrix(workbookPath, DISPO_PLANNING_SHEET);
  const planningColumns = buildPlanningColumns(rows);
  const assignments = new Map<string, AssignmentAccumulator>();
  const vacations = new Map<string, VacationAccumulator>();
  const availabilityRules = new Map<string, AvailabilityAccumulator>();
  const unresolved: ParsedUnresolvedRecord[] = [];
  const warnings: string[] = [];

  for (let rowNumber = DISPO_DATA_START_ROW; rowNumber <= rows.length; rowNumber += 1) {
    const row = rows[rowNumber - 1] ?? [];
    if (isPlanningSummaryRow(row)) {
      continue;
    }
    const eid = normalizeNullableWorkbookValue(row[DISPO_EID_COLUMN - 1]);

    if (!eid) {
      continue;
    }

    const metadata: PlanningRowMetadata = {
      chapter: normalizeNullableWorkbookValue(row[DISPO_CHAPTER_COLUMN - 1]),
      eid: normalizeCanonicalResourceIdentity(eid),
      typeOfWork: normalizeNullableWorkbookValue(row[DISPO_TYPE_OF_WORK_COLUMN - 1]),
      unitSpecificField: normalizeNullableWorkbookValue(row[DISPO_UNIT_SPECIFIC_FIELD_COLUMN - 1]),
    };

    for (const column of planningColumns) {
      const worksheetValue = row[column.columnNumber - 1];
      if (
        worksheetValue === null ||
        worksheetValue === undefined ||
        typeof worksheetValue === "number" ||
        typeof worksheetValue === "boolean" ||
        worksheetValue instanceof Date
      ) {
        continue;
      }

      const rawCellValue = normalizeNullableWorkbookValue(worksheetValue);
      if (!rawCellValue) {
        continue;
      }

      const rawToken = normalizePlanningToken(rawCellValue);
      const normalizedToken = rawToken.toUpperCase();

      if (normalizedToken === "IN DAYS") {
        continue;
      }

      if (normalizedToken === "[_NA] WEEKEND {NA}") {
        continue;
      }

      if (normalizedToken.startsWith("[_AB]")) {
        const note = extractLabel(rawToken);
        const vacationType = note?.toLowerCase().includes("sick") ? VacationType.SICK : VacationType.ANNUAL;
        const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|VAC|${rawToken}`;
        const existing = vacations.get(key);
        if (existing) {
          existing.endDate = column.assignmentDate;
          if (column.halfDayPart) {
            existing.halfDayParts.add(column.halfDayPart);
          }
        } else {
          const vacation = buildVacationAccumulator(column, metadata, rawToken, vacationType, {
            isPublicHoliday: false,
            note,
          });
          vacation.sourceRow = rowNumber;
          vacations.set(key, vacation);
        }
        continue;
      }

      if (normalizedToken.startsWith("[_NA]") && normalizedToken.includes("PUBLIC HOLIDAY")) {
        const holidayName = extractLabel(rawToken);
        const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|PH|${rawToken}`;
        const existing = vacations.get(key);
        if (existing) {
          existing.endDate = column.assignmentDate;
          if (column.halfDayPart) {
            existing.halfDayParts.add(column.halfDayPart);
          }
        } else {
          const vacation = buildVacationAccumulator(column, metadata, rawToken, VacationType.PUBLIC_HOLIDAY, {
            holidayName,
            isPublicHoliday: true,
            note: holidayName,
          });
          vacation.sourceRow = rowNumber;
          vacations.set(key, vacation);
        }
        continue;
      }

      const naHandling = classifyNaPlanningToken(rawToken);

      if (naHandling.kind === "availability") {
        const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|PT|${rawToken}`;
        const existing = availabilityRules.get(key);
        if (existing) {
          const nextAvailability = buildAvailabilityAccumulator(column, metadata, rawToken, {
            ...(naHandling.availableHours !== undefined ? { availableHours: naHandling.availableHours } : {}),
            ...(naHandling.percentage !== undefined ? { percentage: naHandling.percentage } : {}),
            ...(naHandling.ruleType !== undefined ? { ruleType: naHandling.ruleType } : {}),
            ...(naHandling.warning !== undefined ? { warning: naHandling.warning } : {}),
          });
          existing.availableHours = nextAvailability.availableHours;
          existing.percentage = nextAvailability.percentage;
          if (naHandling.warning) {
            existing.warnings.add(naHandling.warning);
          }
        } else {
          const availabilityRule = buildAvailabilityAccumulator(column, metadata, rawToken, {
            ...(naHandling.availableHours !== undefined ? { availableHours: naHandling.availableHours } : {}),
            ...(naHandling.percentage !== undefined ? { percentage: naHandling.percentage } : {}),
            ...(naHandling.ruleType !== undefined ? { ruleType: naHandling.ruleType } : {}),
            ...(naHandling.warning !== undefined ? { warning: naHandling.warning } : {}),
          });
          availabilityRule.sourceRow = rowNumber;
          availabilityRules.set(key, availabilityRule);
        }
        continue;
      }

      if (naHandling.kind === "vacation" && naHandling.vacationType) {
        const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|VAC|${rawToken}`;
        const existing = vacations.get(key);
        if (existing) {
          existing.endDate = column.assignmentDate;
          if (column.halfDayPart) {
            existing.halfDayParts.add(column.halfDayPart);
          }
        } else {
          const vacation = buildVacationAccumulator(
            column,
            metadata,
            rawToken,
            naHandling.vacationType,
            {
              holidayName: naHandling.isPublicHoliday ? extractLabel(rawToken) : null,
              isPublicHoliday: naHandling.isPublicHoliday,
              note: naHandling.note ?? extractLabel(rawToken),
            },
          );
          vacation.sourceRow = rowNumber;
          if (naHandling.warning) {
            vacation.warnings.add(naHandling.warning);
          }
          vacations.set(key, vacation);
        }
        continue;
      }

      if (naHandling.kind === "skip") {
        continue;
      }

      if (normalizedToken.startsWith("[_UN]")) {
        continue;
      }

      const assignment = buildAssignmentAccumulator(column, metadata, rawToken);
      if (!assignment) {
        continue;
      }

      assignment.sourceRow = rowNumber;
      if (!assignment.utilizationCategoryCode) {
        assignment.warnings.add(`Unable to resolve utilization category from token "${rawToken}"`);
      }

      if (!assignment.projectKey && !assignment.isInternal && !assignment.isTbd) {
        unresolved.push({
          sourceRow: rowNumber,
          sourceColumn: column.columnLetter,
          recordType: DispoStagedRecordType.ASSIGNMENT,
          resourceExternalId: metadata.eid,
          projectKey: null,
          message: `Unable to resolve project key from planning token "${rawToken}"`,
          resolutionHint: "Add a WBS token or classify this cell as an internal bucket before commit",
          warnings: Array.from(assignment.warnings),
          normalizedData: {
            assignmentDate: getDateKey(column.assignmentDate),
            rawToken,
            roleToken: assignment.roleToken,
            utilizationCategoryCode: assignment.utilizationCategoryCode,
          },
        });
        continue;
      }

      if (assignment.isTbd) {
        unresolved.push({
          sourceRow: rowNumber,
          sourceColumn: column.columnLetter,
          recordType: DispoStagedRecordType.PROJECT,
          resourceExternalId: metadata.eid,
          projectKey: null,
          message: `Planning token "${rawToken}" references [tbd] and requires project resolution`,
          resolutionHint: "Resolve [tbd] rows to a real WBS/project before commit",
          warnings: Array.from(assignment.warnings),
          normalizedData: {
            assignmentDate: getDateKey(column.assignmentDate),
            rawToken,
            roleToken: assignment.roleToken,
            utilizationCategoryCode: assignment.utilizationCategoryCode,
            winProbability: assignment.winProbability,
          },
        });
      }

      const key = `${metadata.eid}|${getDateKey(column.assignmentDate)}|ASN|${rawToken}`;
      const existing = assignments.get(key);
      if (existing) {
        existing.hoursPerDay += SLOT_HOURS;
        existing.slotCount += 1;
      } else {
        assignments.set(key, assignment);
      }
    }
  }

  const parsedAssignments: ParsedPlanningAssignment[] = Array.from(assignments.values()).map((entry) => ({
    assignmentDate: entry.assignmentDate,
    chapterToken: entry.chapterToken,
    hoursPerDay: entry.hoursPerDay,
    isInternal: entry.isInternal,
    isTbd: entry.isTbd,
    isUnassigned: entry.isUnassigned,
    percentage: entry.slotCount * 50,
    projectKey: entry.projectKey,
    rawToken: entry.rawToken,
    resourceExternalId: entry.resourceExternalId,
    roleName: entry.roleName,
    roleToken: entry.roleToken,
    slotFraction: entry.slotCount / 2,
    sourceColumn: toColumnLetter(entry.firstColumnNumber),
    sourceRow: entry.sourceRow,
    utilizationCategoryCode: entry.utilizationCategoryCode,
    warnings: Array.from(entry.warnings),
    winProbability: entry.winProbability,
  }));

  const parsedVacations: ParsedPlanningVacation[] = Array.from(vacations.values()).map((entry) => ({
    endDate: entry.endDate,
    halfDayPart: entry.halfDayParts.size === 1 ? Array.from(entry.halfDayParts)[0] ?? null : null,
    holidayName: entry.holidayName,
    isHalfDay: entry.halfDayParts.size === 1,
    isPublicHoliday: entry.isPublicHoliday,
    note: entry.note,
    rawToken: entry.rawToken,
    resourceExternalId: entry.resourceExternalId,
    sourceColumn: toColumnLetter(entry.firstColumnNumber),
    sourceRow: entry.sourceRow,
    startDate: entry.startDate,
    vacationType: entry.vacationType,
    warnings: Array.from(entry.warnings),
  }));

  const parsedAvailabilityRules: ParsedPlanningAvailabilityRule[] = Array.from(availabilityRules.values()).map((entry) => ({
    availableHours: entry.availableHours,
    effectiveEndDate: entry.effectiveEndDate,
    effectiveStartDate: entry.effectiveStartDate,
    isResolved: entry.isResolved,
    percentage: entry.percentage,
    rawToken: entry.rawToken,
    resourceExternalId: entry.resourceExternalId,
    ruleType: entry.ruleType,
    sourceColumn: toColumnLetter(entry.firstColumnNumber),
    sourceRow: entry.sourceRow,
    warnings: Array.from(entry.warnings),
  }));

  return {
    assignments: parsedAssignments,
    availabilityRules: parsedAvailabilityRules,
    unresolved,
    vacations: parsedVacations,
    warnings,
  };
}
