Parse a Kenmei CSV export file.

export const parseKenmeiCsvExport = (
csvString: string,
options: Partial<KenmeiParseOptions> = {},
): KenmeiExport => {
const parseOptions = { ...DEFAULT_PARSE_OPTIONS, ...options };

try {
// Replace Windows line breaks with Unix style
const normalizedCsv = csvString.replace(/\r\n/g, "\n");

// Parse CSV rows properly, respecting quoted fields
const rows = parseCSVRows(normalizedCsv);

if (rows.length < 2) {
throw new Error("CSV file does not contain enough data");
}

// Get headers from the first line, normalize them to avoid issues with spaces or case
const headers = rows[0].map((header) => header.trim().toLowerCase());

// Validate required headers
const requiredHeaders = ["title"];
for (const required of requiredHeaders) {
if (!headers.includes(required)) {
throw new Error(`CSV is missing required header: ${required}`);
}
}

// Create mapping for various possible column names
const columnMappings = {
chapter: [
"last_chapter_read",
"chapters_read",
"chapter",
"current_chapter",
],
volume: ["last_volume_read", "volumes_read", "volume", "current_volume"],
status: ["status", "reading_status"],
score: ["score", "rating"],
url: ["series_url", "url", "link"],
notes: ["notes", "comments"],
date: ["last_read_at", "updated_at", "date"],
};

// Parse manga entries
const manga: KenmeiManga[] = [];

// Skip the header row
for (let i = 1; i < rows.length; i++) {
const values = rows[i];

// Skip rows that don't have enough fields (likely incomplete/malformed data)
if (values.length < 2) {
console.warn(
`Skipping row ${i + 1} with insufficient fields: ${values.join(",")}`,
);
continue;
}

// Skip rows where the title is just a number or looks like a chapter reference
const potentialTitle = values[headers.indexOf("title")];
if (
/^(Chapter|Ch\.|Vol\.|Volume) \d+$/i.test(potentialTitle) ||
/^\d+$/.test(potentialTitle)
) {
console.warn(
`Skipping row ${i + 1} with invalid title: "${potentialTitle}"`,
);
continue;
}

// Create an object mapping headers to values
const entry: Record<string, string> = {};
headers.forEach((header, index) => {
if (index < values.length) {
entry[header] = values[index];
}
});

// Find values using flexible column names
const findValue = (mappings: string[]): string | undefined => {
for (const mapping of mappings) {
if (entry[mapping] !== undefined) {
return entry[mapping];
}
}
return undefined;
};

// Parse numeric values safely
const parseIntSafe = (value: string | undefined): number | undefined => {
if (!value) return undefined;
// Remove any non-numeric characters except decimal point
const cleanValue = value.replace(/[^\d.]/g, "");
if (!cleanValue) return undefined;
const parsed = parseInt(cleanValue, 10);
return isNaN(parsed) ? undefined : parsed;
};

// Get values using flexible column mappings
const chapterValue = findValue(columnMappings.chapter);
const volumeValue = findValue(columnMappings.volume);
const statusValue = findValue(columnMappings.status);
const scoreValue = findValue(columnMappings.score);
const urlValue = findValue(columnMappings.url);
const notesValue = findValue(columnMappings.notes);
const dateValue = findValue(columnMappings.date);
const lastReadAt =
entry.last_read_at || entry["last read at"] || undefined;
// Parse chapter and volume numbers
const chaptersRead = parseIntSafe(chapterValue);
const volumesRead = parseIntSafe(volumeValue);
// Convert to proper types
const mangaEntry: KenmeiManga = {
id: parseInt(entry.id || "0"),
title: entry.title,
status: validateStatus(statusValue),
score: scoreValue ? parseFloat(scoreValue) : 0,
url: urlValue || "",
cover_url: entry.cover_url,
chapters_read: chaptersRead !== undefined ? chaptersRead : 0,
total_chapters: entry.total_chapters
? parseInt(entry.total_chapters)
: undefined,
volumes_read: volumesRead,
total_volumes: entry.total_volumes
? parseInt(entry.total_volumes)
: undefined,
notes: notesValue || "",
last_read_at: lastReadAt,
created_at: entry.created_at || dateValue || new Date().toISOString(),
updated_at: entry.updated_at || dateValue || new Date().toISOString(),
author: entry.author,
alternative_titles: entry.alternative_titles
? entry.alternative_titles.split(";")
: undefined,
};

manga.push(mangaEntry);
}

// Process in batches if needed and validation is enabled
if (parseOptions.validateStructure) {
const result = processKenmeiMangaBatches(manga, 100, parseOptions);

if (
result.validationErrors.length > 0 &&
!parseOptions.allowPartialData
) {
throw new Error(
`${result.validationErrors.length} validation errors found in CSV import`,
);
}

// Use the processed entries if we have them
if (result.processedEntries.length > 0) {
manga.length = 0; // Clear the array
manga.push(...result.processedEntries);
}
}

// Create the export object
const kenmeiExport: KenmeiExport = {
export_date: new Date().toISOString(),
user: {
username: "CSV Import User",
id: 0,
},
manga,
};

console.log(`Successfully parsed ${manga.length} manga entries from CSV`);
return kenmeiExport;
} catch (error) {
if (error instanceof Error) {
console.error("CSV parsing error:", error.message);
throw new Error(`Failed to parse CSV: ${error.message}`);
}
console.error("Unknown CSV parsing error");
throw new Error("Failed to parse CSV: Unknown error");
}
};