import type { ContactCard, NameComponent } from "@/lib/jmap/types"; import { generateUUID } from "@/lib/utils"; export interface CsvColumnMapping { firstName: number; lastName: number; email: number; phone: number; company: number; jobTitle: number; address: number; city: number; region: number; postcode: number; country: number; website: number; note: number; nickname: number; } export interface CsvParseResult { headers: string[]; rows: string[][]; delimiter: string; totalRows: number; } function detectDelimiter(text: string): string { const line = text.split("\n")[0] || ""; const counts: Record = { ",": 0, ";": 0, "\t": 0 }; for (const ch of line) { if (ch in counts) counts[ch]++; } const best = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]; return best && best[1] > 0 ? best[0] : ","; } function parseCsvLine(line: string, delimiter: string): string[] { const fields: string[] = []; let current = ""; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (inQuotes) { if (ch === '"') { if (i + 1 < line.length && line[i + 1] === '"') { current += '"'; i++; } else { inQuotes = false; } } else { current += ch; } } else if (ch === '"') { inQuotes = true; } else if (ch === delimiter) { fields.push(current.trim()); current = ""; } else { current += ch; } } fields.push(current.trim()); return fields; } export function parseCSV(text: string): CsvParseResult { const delimiter = detectDelimiter(text); const rawLines = text.split(/\r?\n/); const headers = parseCsvLine(rawLines[0] || "", delimiter); const rows: string[][] = []; for (let i = 1; i < rawLines.length; i++) { const line = rawLines[i].trim(); if (!line) continue; const fields = parseCsvLine(line, delimiter); if (fields.length > 0 && fields.some((f) => f.length > 0)) { rows.push(fields); } } return { headers, rows, delimiter, totalRows: rows.length }; } const NAME_PATTERNS = [ /^(?:first[\s_-]?name|given[\s_-]?name|forename|vorname|prénom|nombre|名)$/i, ]; const LAST_NAME_PATTERNS = [ /^(?:last[\s_-]?name|surname|family[\s_-]?name|nachname|nom|姓)$/i, ]; const EMAIL_PATTERNS = [ /^(?:e?-?mail|email[\s_-]?address|e?-?mail[\s_-]?address|e-mail-adresse)$/i, ]; const PHONE_PATTERNS = [ /^(?:phone|telephone|tel|mobile|cell|handy|telefon|téléphone|电话)$/i, ]; const COMPANY_PATTERNS = [ /^(?:company|organization|org|firma|unternehmen|entreprise|société|公司)$/i, ]; const JOB_TITLE_PATTERNS = [ /^(?:job[\s_-]?title|title|position|role|funktion|beruf|poste)$/i, ]; const ADDRESS_PATTERNS = [ /^(?:address|addr|street|straße|adresse|rue)$/i, ]; const CITY_PATTERNS = [ /^(?:city|town|ort|stadt|ville)$/i, ]; const REGION_PATTERNS = [ /^(?:state|province|region|bundesland|région)$/i, ]; const POSTCODE_PATTERNS = [ /^(?:zip|postal[\s_-]?code|postcode|plz|code[\s_-]?postal)$/i, ]; const COUNTRY_PATTERNS = [ /^(?:country|land|pays)$/i, ]; const WEBSITE_PATTERNS = [ /^(?:website|url|web|homepage|site)$/i, ]; const NOTE_PATTERNS = [ /^(?:note|notes|comments|bemerkung|notiz|remarque)$/i, ]; const NICKNAME_PATTERNS = [ /^(?:nickname|nick|alias|spitzname|surnom)$/i, ]; function findColumnIndex(headers: string[], patterns: RegExp[]): number { for (const pattern of patterns) { const idx = headers.findIndex((h) => pattern.test(h)); if (idx >= 0) return idx; } return -1; } export function autoMapColumns(headers: string[]): CsvColumnMapping { return { firstName: findColumnIndex(headers, NAME_PATTERNS), lastName: findColumnIndex(headers, LAST_NAME_PATTERNS), email: findColumnIndex(headers, EMAIL_PATTERNS), phone: findColumnIndex(headers, PHONE_PATTERNS), company: findColumnIndex(headers, COMPANY_PATTERNS), jobTitle: findColumnIndex(headers, JOB_TITLE_PATTERNS), address: findColumnIndex(headers, ADDRESS_PATTERNS), city: findColumnIndex(headers, CITY_PATTERNS), region: findColumnIndex(headers, REGION_PATTERNS), postcode: findColumnIndex(headers, POSTCODE_PATTERNS), country: findColumnIndex(headers, COUNTRY_PATTERNS), website: findColumnIndex(headers, WEBSITE_PATTERNS), note: findColumnIndex(headers, NOTE_PATTERNS), nickname: findColumnIndex(headers, NICKNAME_PATTERNS), }; } function getCol(row: string[], colIndex: number): string { if (colIndex < 0 || colIndex >= row.length) return ""; return row[colIndex]?.trim() || ""; } export function mapRowToContact( row: string[], mapping: CsvColumnMapping, addressBookIds: Record, ): ContactCard | null { const id = `import-csv-${generateUUID()}`; const firstName = getCol(row, mapping.firstName); const lastName = getCol(row, mapping.lastName); const email = getCol(row, mapping.email); const phone = getCol(row, mapping.phone); const company = getCol(row, mapping.company); const jobTitle = getCol(row, mapping.jobTitle); const address = getCol(row, mapping.address); const city = getCol(row, mapping.city); const region = getCol(row, mapping.region); const postcode = getCol(row, mapping.postcode); const country = getCol(row, mapping.country); const website = getCol(row, mapping.website); const note = getCol(row, mapping.note); const nickname = getCol(row, mapping.nickname); if (!email && !firstName && !lastName) return null; const components: NameComponent[] = []; if (firstName) components.push({ kind: "given", value: firstName }); if (lastName) components.push({ kind: "surname", value: lastName }); const contact: ContactCard = { id, addressBookIds, }; if (components.length > 0) { contact.name = { components, isOrdered: true }; } else if (email) { contact.name = { full: email.split("@")[0] }; } if (email) { contact.emails = { e0: { address: email }, }; } if (phone) { contact.phones = { p0: { number: phone }, }; } if (company) { contact.organizations = { o0: { name: company }, }; } if (jobTitle) { contact.titles = { t0: { name: jobTitle, kind: "title" }, }; } if (address || city || region || postcode || country) { contact.addresses = { a0: { street: address || undefined, locality: city || undefined, region: region || undefined, postcode: postcode || undefined, country: country || undefined, }, }; } if (website) { contact.onlineServices = { u0: { uri: website }, }; } if (note) { contact.notes = { n0: { note }, }; } if (nickname) { contact.nicknames = { n0: { name: nickname }, }; } return contact; } export function detectDuplicatesByEmail( existingContacts: ContactCard[], incoming: ContactCard[], ): Map { const dupes = new Map(); const existingEmails = new Map(); for (const c of existingContacts) { if (c.emails) { for (const e of Object.values(c.emails)) { existingEmails.set(e.address.toLowerCase(), c.id); } } } incoming.forEach((card, idx) => { if (card.emails) { for (const e of Object.values(card.emails)) { const match = existingEmails.get(e.address.toLowerCase()); if (match) { dupes.set(idx, match); return; } } } }); return dupes; }