using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using ClosedXML.Excel;
namespace OpenNest.IO.Bom;
/// Strict demand reader for Parts sheets with Part Name and Qty Required columns.
public static class PartQuantityReader
{
public static IReadOnlyDictionary Read(string path)
{
using var workbook = new XLWorkbook(path);
if (!workbook.TryGetWorksheet("Parts", out var sheet))
throw new InvalidDataException("Workbook must contain a Parts worksheet.");
var nameColumn = FindColumn(sheet, "Part Name");
var quantityColumn = FindColumn(sheet, "Qty Required");
var quantities = new Dictionary(StringComparer.Ordinal);
var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1;
for (var row = 2; row <= lastRow; row++)
{
var nameCell = sheet.Cell(row, nameColumn);
var quantityCell = sheet.Cell(row, quantityColumn);
if (nameCell.IsEmpty() && quantityCell.IsEmpty())
continue;
var name = nameCell.GetString();
// Do not truncate fractional quantities, infer a default, or silently normalize identifiers.
if (
string.IsNullOrWhiteSpace(name)
|| !double.TryParse(
quantityCell.GetString(),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var number
)
|| !double.IsFinite(number)
|| number < 0
|| number > int.MaxValue
|| number != System.Math.Truncate(number)
)
throw new InvalidDataException(
$"Invalid part name or nonnegative integer quantity at Parts row {row}."
);
if (!quantities.TryAdd(name, (int)number))
throw new InvalidDataException($"Duplicate part name at Parts row {row}: {name}");
}
if (!quantities.Values.Any(q => q > 0))
throw new InvalidDataException("Workbook contains no positive part demand.");
return quantities;
}
private static int FindColumn(IXLWorksheet sheet, string name)
{
var matches = sheet.Row(1).CellsUsed().Where(c => c.GetString() == name).ToList();
if (matches.Count != 1)
throw new InvalidDataException($"Parts must contain exactly one '{name}' column.");
return matches[0].Address.ColumnNumber;
}
}