/**
 * CSV Parser and Formatter - RFC 4180 compliant
 *
 * A lightweight, cross-platform CSV implementation that works in both
 * Node.js and Browser environments with zero dependencies.
 *
 * High-performance RFC 4180 compliant CSV parser and formatter.
 *
 * @see https://tools.ietf.org/html/rfc4180
 */
import { formatNumberForCsv } from "./csv-number.js";
// =============================================================================
// Helper Functions
// =============================================================================
/**
 * Escape special regex characters
 */
function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
 * Check if a transform function is synchronous (1 argument) vs async (2 arguments)
 */
export function isSyncTransform(transform) {
    return transform.length === 1;
}
/**
 * Check if a validate function is synchronous (1 argument) vs async (2 arguments)
 */
export function isSyncValidate(validate) {
    return validate.length === 1;
}
/**
 * Check if headers are unique
 */
function validateUniqueHeaders(headers) {
    const seen = new Set();
    const duplicates = [];
    for (const header of headers) {
        if (header !== null && header !== undefined) {
            if (seen.has(header)) {
                duplicates.push(header);
            }
            seen.add(header);
        }
    }
    if (duplicates.length > 0) {
        throw new Error(`Duplicate headers found ${JSON.stringify(duplicates)}`);
    }
}
// =============================================================================
// Parse Functions
// =============================================================================
/**
 * Parse a CSV string into rows of fields
 */
export function parseCsv(input, options = {}) {
    const { delimiter = ",", quote: quoteOption = '"', escape: escapeOption = '"', skipEmptyLines = false, ignoreEmpty = false, trim = false, ltrim = false, rtrim = false, headers = false, renameHeaders = false, comment, maxRows, skipLines = 0, skipRows = 0, strictColumnHandling = false, discardUnmappedColumns = false, transform, validate } = options;
    const shouldSkipEmpty = skipEmptyLines || ignoreEmpty;
    // Handle quote: null/false to disable quoting
    const quoteEnabled = quoteOption !== null && quoteOption !== false;
    const quote = quoteEnabled ? String(quoteOption) : "";
    const escape = escapeOption !== null && escapeOption !== false ? String(escapeOption) : "";
    const rows = [];
    const invalidRows = [];
    let currentRow = [];
    let currentField = "";
    let inQuotes = false;
    let i = 0;
    let lineNumber = 0;
    let dataRowCount = 0;
    let skippedDataRows = 0;
    // Header handling
    let headerRow = null;
    let headersLength = 0;
    let useHeaders = false;
    let headerRowProcessed = false;
    // Determine header mode
    if (headers === true) {
        useHeaders = true;
    }
    else if (Array.isArray(headers)) {
        headerRow = headers;
        headersLength = headers.filter(h => h !== null && h !== undefined).length;
        validateUniqueHeaders(headers);
        useHeaders = true;
        if (!renameHeaders) {
            headerRowProcessed = true; // We already have headers, don't wait for first row
        }
    }
    else if (typeof headers === "function") {
        useHeaders = true;
    }
    // Pre-compute trim function to avoid repeated condition checks
    const trimField = trim || (ltrim && rtrim)
        ? (s) => s.trim()
        : ltrim
            ? (s) => s.trimStart()
            : rtrim
                ? (s) => s.trimEnd()
                : (s) => s;
    const processRow = (row) => {
        // If we have headers defined and this is the first data row (for headers: true)
        // or we need to validate headers from a function
        if (useHeaders && !headerRowProcessed) {
            // First row is headers
            if (typeof headers === "function") {
                const transformed = headers(row);
                validateUniqueHeaders(transformed);
                headerRow = transformed;
            }
            else if (!Array.isArray(headers)) {
                validateUniqueHeaders(row);
                headerRow = row;
            }
            headersLength = headerRow.filter(h => h !== null && h !== undefined).length;
            headerRowProcessed = true;
            // If renameHeaders and custom headers provided, discard this row
            if (renameHeaders) {
                return false;
            }
            // For headers: true, don't add header row to data
            if (headers === true || typeof headers === "function") {
                return false;
            }
            return false;
        }
        // Skip data rows
        if (skippedDataRows < skipRows) {
            skippedDataRows++;
            return false;
        }
        // Column validation when using headers
        if (headerRow && headerRow.length > 0) {
            const expectedCols = headersLength;
            const actualCols = row.length;
            if (actualCols > expectedCols) {
                if (strictColumnHandling && !discardUnmappedColumns) {
                    // Mark as invalid but continue
                    invalidRows.push({
                        row,
                        reason: `Column header mismatch expected: ${expectedCols} columns got: ${actualCols}`
                    });
                    return false;
                }
                else {
                    // Default: trim extra columns
                    row.length = headerRow.length;
                }
            }
            else if (actualCols < expectedCols) {
                if (strictColumnHandling) {
                    invalidRows.push({
                        row,
                        reason: `Column header mismatch expected: ${expectedCols} columns got: ${actualCols}`
                    });
                    return false;
                }
                // Pad with empty strings
                while (row.length < headerRow.length) {
                    row.push("");
                }
            }
        }
        return true;
    };
    const len = input.length;
    while (i < len) {
        const char = input[i];
        if (inQuotes && quoteEnabled) {
            // Inside quoted field
            if (escape && char === escape && input[i + 1] === quote) {
                // Escaped quote
                currentField += quote;
                i += 2;
            }
            else if (char === quote) {
                // End of quoted field
                inQuotes = false;
                i++;
            }
            else if (char === "\r") {
                // Normalize CRLF to LF inside quoted fields
                if (input[i + 1] === "\n") {
                    i++; // Skip \r, will add \n on next iteration
                }
                else {
                    currentField += "\n"; // Convert standalone \r to \n
                    i++;
                }
            }
            else {
                currentField += char;
                i++;
            }
        }
        else {
            // Outside quoted field
            if (quoteEnabled && char === quote && currentField === "") {
                // Start of quoted field
                inQuotes = true;
                i++;
            }
            else if (char === delimiter) {
                // Field separator
                currentRow.push(trimField(currentField));
                currentField = "";
                i++;
            }
            else if (char === "\n" || char === "\r") {
                // End of row - handle \r\n, \r, and \n
                if (char === "\r" && input[i + 1] === "\n") {
                    i++; // Skip the \n in \r\n
                }
                currentRow.push(trimField(currentField));
                currentField = "";
                lineNumber++;
                // Skip lines at beginning
                if (lineNumber <= skipLines) {
                    currentRow = [];
                    i++;
                    continue;
                }
                // Skip comment lines
                if (comment && currentRow[0]?.startsWith(comment)) {
                    currentRow = [];
                    i++;
                    continue;
                }
                // Skip empty lines
                const isEmpty = currentRow.length === 1 && currentRow[0] === "";
                if (shouldSkipEmpty && isEmpty) {
                    currentRow = [];
                    i++;
                    continue;
                }
                // Process row (handles headers, validation)
                if (processRow(currentRow)) {
                    rows.push(currentRow);
                    dataRowCount++;
                }
                currentRow = [];
                i++;
                // Check max rows - after resetting currentRow
                if (maxRows !== undefined && dataRowCount >= maxRows) {
                    break;
                }
            }
            else {
                currentField += char;
                i++;
            }
        }
    }
    // Handle last field/row
    if (currentField !== "" || currentRow.length > 0) {
        currentRow.push(trimField(currentField));
        // Skip lines at beginning
        if (lineNumber >= skipLines) {
            // Skip comment lines
            if (!(comment && currentRow[0]?.startsWith(comment))) {
                // Skip empty lines
                const isEmpty = currentRow.length === 1 && currentRow[0] === "";
                if (!(shouldSkipEmpty && isEmpty)) {
                    if (!(maxRows !== undefined && dataRowCount >= maxRows)) {
                        if (processRow(currentRow)) {
                            rows.push(currentRow);
                        }
                    }
                }
            }
        }
    }
    // Convert to objects if headers enabled
    if (useHeaders && headerRow) {
        let dataRows = rows.map(row => {
            const obj = {};
            headerRow.forEach((header, index) => {
                if (header !== null && header !== undefined) {
                    obj[header] = row[index] ?? "";
                }
            });
            return obj;
        });
        // Apply transform if provided
        if (transform) {
            dataRows = dataRows
                .map(row => transform(row))
                .filter((row) => row !== null && row !== undefined);
        }
        // Apply validate if provided
        if (validate) {
            const validatedRows = [];
            for (const row of dataRows) {
                const result = validate(row);
                if (typeof result === "boolean") {
                    if (result) {
                        validatedRows.push(row);
                    }
                    else {
                        invalidRows.push({ row: Object.values(row), reason: "Validation failed" });
                    }
                }
                else {
                    if (result.isValid) {
                        validatedRows.push(row);
                    }
                    else {
                        invalidRows.push({
                            row: Object.values(row),
                            reason: result.reason || "Validation failed"
                        });
                    }
                }
            }
            dataRows = validatedRows;
        }
        if ((strictColumnHandling || validate) && invalidRows.length > 0) {
            return {
                headers: headerRow.filter((h) => h !== null && h !== undefined),
                rows: dataRows,
                invalidRows
            };
        }
        return {
            headers: headerRow.filter((h) => h !== null && h !== undefined),
            rows: dataRows
        };
    }
    // For array mode (no headers), apply transform and validate
    let resultRows = rows;
    if (transform) {
        resultRows = resultRows
            .map(row => transform(row))
            .filter((row) => row !== null && row !== undefined);
    }
    if (validate) {
        const validatedRows = [];
        const arrayInvalidRows = [];
        for (const row of resultRows) {
            const result = validate(row);
            if (typeof result === "boolean") {
                if (result) {
                    validatedRows.push(row);
                }
                else {
                    arrayInvalidRows.push({ row, reason: "Validation failed" });
                }
            }
            else {
                if (result.isValid) {
                    validatedRows.push(row);
                }
                else {
                    arrayInvalidRows.push({ row, reason: result.reason || "Validation failed" });
                }
            }
        }
        resultRows = validatedRows;
        if (arrayInvalidRows.length > 0) {
            return {
                rows: resultRows,
                invalidRows: arrayInvalidRows
            }; // Return with invalidRows for array mode too
        }
    }
    return resultRows;
}
// =============================================================================
// Format Functions
// =============================================================================
/**
 * Format data as a CSV string
 */
export function formatCsv(data, options = {}) {
    const { delimiter = ",", quote: quoteOption = '"', escape: escapeOption, rowDelimiter = "\n", alwaysQuote = false, quoteColumns = false, quoteHeaders = false, headers, writeHeaders: writeHeadersOption, writeBOM = false, includeEndRowDelimiter = false, alwaysWriteHeaders = false, transform, decimalSeparator = "." } = options;
    // Determine if headers should be written (default: true when headers is provided)
    const shouldWriteHeaders = writeHeadersOption ?? true;
    // If quote is false or null, disable quoting entirely
    const quoteEnabled = quoteOption !== false && quoteOption !== null;
    const quote = quoteEnabled ? String(quoteOption) : "";
    const escape = escapeOption !== undefined && escapeOption !== false && escapeOption !== null
        ? String(escapeOption)
        : quote;
    // Pre-compile regex patterns for performance
    const needsQuoteRegex = quoteEnabled
        ? new RegExp(`[${escapeRegex(delimiter)}${escapeRegex(quote)}\r\n]`)
        : null;
    const escapeQuoteRegex = quoteEnabled ? new RegExp(escapeRegex(quote), "g") : null;
    const escapedQuote = escape + quote;
    const lines = [];
    const shouldQuoteColumn = (index, header, isHeader = false) => {
        const quoteConfig = isHeader ? quoteHeaders : quoteColumns;
        if (typeof quoteConfig === "boolean") {
            return quoteConfig;
        }
        if (Array.isArray(quoteConfig)) {
            return quoteConfig[index] === true;
        }
        if (typeof quoteConfig === "object" && header) {
            return quoteConfig[header] === true;
        }
        return false;
    };
    const formatField = (value, index, header, isHeader = false) => {
        if (value === null || value === undefined) {
            return "";
        }
        const str = typeof value === "number"
            ? formatNumberForCsv(value, decimalSeparator)
            : String(value);
        // If quoting is disabled, return raw string
        if (!quoteEnabled) {
            return str;
        }
        // Check if quoting is needed
        const forceQuote = alwaysQuote || shouldQuoteColumn(index, header, isHeader);
        const needsQuote = forceQuote || needsQuoteRegex.test(str);
        if (needsQuote) {
            // Escape quotes using pre-compiled regex
            const escaped = str.replace(escapeQuoteRegex, escapedQuote);
            return quote + escaped + quote;
        }
        return str;
    };
    const formatRow = (row, rowHeaders, isHeader = false) => {
        return row
            .map((value, index) => formatField(value, index, rowHeaders?.[index], isHeader))
            .join(delimiter);
    };
    // Determine headers
    let keys = null;
    // Helper to apply transform if provided (sync only)
    const applyTransform = (row) => {
        if (transform) {
            // Check if it's a sync transform (1 argument)
            if (transform.length === 1) {
                return transform(row);
            }
            // For async transform in sync context, just return the row unchanged
            // Async transforms should use streaming API
            return row;
        }
        return row;
    };
    // Handle array of objects
    if (data.length > 0 && !Array.isArray(data[0])) {
        const objects = data;
        keys = headers === true ? Object.keys(objects[0]) : Array.isArray(headers) ? headers : null;
        if (keys && shouldWriteHeaders) {
            // Add header row
            lines.push(formatRow(keys, keys, true));
        }
        // Add data rows
        for (const obj of objects) {
            const transformedObj = applyTransform(obj);
            if (transformedObj === null || transformedObj === undefined) {
                continue; // Skip row if transform returns null
            }
            const row = keys ? keys.map(key => transformedObj[key]) : Object.values(transformedObj);
            lines.push(formatRow(row, keys ?? undefined));
        }
    }
    else if (data.length > 0) {
        // Handle 2D array with data
        const arrays = data;
        // Add custom headers if provided
        if (Array.isArray(headers)) {
            keys = headers;
            if (shouldWriteHeaders) {
                lines.push(formatRow(headers, headers, true));
            }
        }
        for (const row of arrays) {
            const transformedRow = applyTransform(row);
            if (transformedRow === null || transformedRow === undefined) {
                continue; // Skip row if transform returns null
            }
            lines.push(formatRow(transformedRow, keys ?? undefined));
        }
    }
    else if (alwaysWriteHeaders && Array.isArray(headers) && shouldWriteHeaders) {
        // Handle empty data with alwaysWriteHeaders
        lines.push(formatRow(headers, headers, true));
    }
    let result = lines.join(rowDelimiter);
    // Add trailing row delimiter
    if (result.length > 0 && includeEndRowDelimiter) {
        result += rowDelimiter;
    }
    // Add BOM for UTF-8
    if (writeBOM) {
        result = "\uFEFF" + result;
    }
    return result;
}
// =============================================================================
// Streaming Parser
// =============================================================================
/**
 * Async CSV parser that yields rows one at a time
 */
export async function* parseCsvStream(input, options = {}) {
    const { delimiter = ",", quote: quoteOption = '"', escape: escapeOption = '"', skipEmptyLines = false, ignoreEmpty = false, trim = false, ltrim = false, rtrim = false, headers = false, renameHeaders = false, comment, maxRows, skipLines = 0, skipRows = 0, strictColumnHandling = false, discardUnmappedColumns = false } = options;
    const shouldSkipEmpty = skipEmptyLines || ignoreEmpty;
    // Handle quote: null/false to disable quoting
    const quoteEnabled = quoteOption !== null && quoteOption !== false;
    const quote = quoteEnabled ? String(quoteOption) : "";
    const escape = escapeOption !== null && escapeOption !== false ? String(escapeOption) : "";
    let headerRow = null;
    let headersLength = 0;
    let useHeaders = false;
    let headerRowProcessed = false;
    let buffer = "";
    let currentRow = [];
    let currentField = "";
    let inQuotes = false;
    let lineNumber = 0;
    let dataRowCount = 0;
    let skippedDataRows = 0;
    // Determine header mode
    if (headers === true) {
        useHeaders = true;
    }
    else if (Array.isArray(headers)) {
        headerRow = headers;
        headersLength = headers.filter(h => h !== null && h !== undefined).length;
        validateUniqueHeaders(headers);
        useHeaders = true;
        if (!renameHeaders) {
            headerRowProcessed = true;
        }
    }
    else if (typeof headers === "function") {
        useHeaders = true;
    }
    // Pre-compute trim function to avoid repeated condition checks
    const trimField = trim || (ltrim && rtrim)
        ? (s) => s.trim()
        : ltrim
            ? (s) => s.trimStart()
            : rtrim
                ? (s) => s.trimEnd()
                : (s) => s;
    const processRow = (row) => {
        // Header handling
        if (useHeaders && !headerRowProcessed) {
            if (typeof headers === "function") {
                const transformed = headers(row);
                validateUniqueHeaders(transformed);
                headerRow = transformed;
            }
            else if (!Array.isArray(headers)) {
                validateUniqueHeaders(row);
                headerRow = row;
            }
            headersLength = headerRow.filter(h => h !== null && h !== undefined).length;
            headerRowProcessed = true;
            if (renameHeaders) {
                return { valid: false, row: null };
            }
            if (headers === true || typeof headers === "function") {
                return { valid: false, row: null };
            }
            return { valid: false, row: null };
        }
        // Skip data rows
        if (skippedDataRows < skipRows) {
            skippedDataRows++;
            return { valid: false, row: null };
        }
        // Column validation
        if (headerRow && headerRow.length > 0) {
            const expectedCols = headersLength;
            const actualCols = row.length;
            if (actualCols > expectedCols) {
                if (strictColumnHandling && !discardUnmappedColumns) {
                    return { valid: false, row: null };
                }
                else {
                    // Default: trim extra columns
                    row.length = headerRow.length;
                }
            }
            else if (actualCols < expectedCols) {
                if (strictColumnHandling) {
                    return { valid: false, row: null };
                }
                while (row.length < headerRow.length) {
                    row.push("");
                }
            }
        }
        // Convert to object if using headers
        if (useHeaders && headerRow) {
            const obj = {};
            headerRow.forEach((header, index) => {
                if (header !== null && header !== undefined) {
                    obj[header] = row[index] ?? "";
                }
            });
            return { valid: true, row: obj };
        }
        return { valid: true, row };
    };
    const processBuffer = function* () {
        let i = 0;
        const len = buffer.length;
        while (i < len) {
            const char = buffer[i];
            if (inQuotes && quoteEnabled) {
                if (escape && char === escape && buffer[i + 1] === quote) {
                    currentField += quote;
                    i += 2;
                }
                else if (char === quote) {
                    inQuotes = false;
                    i++;
                }
                else if (i === len - 1) {
                    // Need more data for quoted field
                    buffer = buffer.slice(i);
                    return;
                }
                else if (char === "\r") {
                    // Normalize CRLF to LF inside quoted fields
                    if (buffer[i + 1] === "\n") {
                        i++; // Skip \r, will add \n on next iteration
                    }
                    else {
                        currentField += "\n"; // Convert standalone \r to \n
                        i++;
                    }
                }
                else {
                    currentField += char;
                    i++;
                }
            }
            else {
                if (quoteEnabled && char === quote && currentField === "") {
                    inQuotes = true;
                    i++;
                }
                else if (char === delimiter) {
                    currentRow.push(trimField(currentField));
                    currentField = "";
                    i++;
                }
                else if (char === "\n" || char === "\r") {
                    if (char === "\r" && buffer[i + 1] === "\n") {
                        i++;
                    }
                    currentRow.push(trimField(currentField));
                    currentField = "";
                    lineNumber++;
                    if (lineNumber <= skipLines) {
                        currentRow = [];
                        i++;
                        continue;
                    }
                    if (comment && currentRow[0]?.startsWith(comment)) {
                        currentRow = [];
                        i++;
                        continue;
                    }
                    const isEmpty = currentRow.length === 1 && currentRow[0] === "";
                    if (shouldSkipEmpty && isEmpty) {
                        currentRow = [];
                        i++;
                        continue;
                    }
                    const result = processRow(currentRow);
                    if (result.valid && result.row) {
                        dataRowCount++;
                        if (maxRows !== undefined && dataRowCount > maxRows) {
                            return;
                        }
                        yield result.row;
                    }
                    currentRow = [];
                    i++;
                }
                else {
                    currentField += char;
                    i++;
                }
            }
        }
        buffer = "";
    };
    // Handle string input
    if (typeof input === "string") {
        buffer = input;
        yield* processBuffer();
        // Handle last row
        if (currentField !== "" || currentRow.length > 0) {
            currentRow.push(trimField(currentField));
            if (!(maxRows !== undefined && dataRowCount >= maxRows)) {
                const result = processRow(currentRow);
                if (result.valid && result.row) {
                    yield result.row;
                }
            }
        }
        return;
    }
    // Handle async iterable
    for await (const chunk of input) {
        buffer += chunk;
        yield* processBuffer();
    }
    // Handle last row
    if (currentField !== "" || currentRow.length > 0) {
        currentRow.push(trimField(currentField));
        if (!(maxRows !== undefined && dataRowCount >= maxRows)) {
            const result = processRow(currentRow);
            if (result.valid && result.row) {
                yield result.row;
            }
        }
    }
}
