/**
 * CSV Service
 *
 * Parses, stringifies, and filters CSV data for dataset entities.
 * Conventional CSV: the header row is the column list, every cell is a
 * string. There is no typed column schema — callers derive columns from the
 * header. Uses csv-parse and csv-stringify for reliable CSV handling.
 */
import { parse } from 'csv-parse/sync';
import { stringify } from 'csv-stringify/sync';
export class CSVService {
    /**
     * Parse CSV text into string-keyed rows. The header row supplies the
     * column names. Returns an empty array for empty CSV or header-only CSV.
     */
    parseRows(csvText) {
        const trimmed = csvText.trim();
        if (!trimmed) {
            return [];
        }
        return parse(trimmed, {
            columns: true,
            skip_empty_lines: true,
            trim: true,
        });
    }
    /**
     * Column names from the CSV header row. Empty array for empty CSV.
     */
    columns(csvText) {
        const trimmed = csvText.trim();
        if (!trimmed) {
            return [];
        }
        const header = parse(trimmed, {
            columns: false,
            skip_empty_lines: true,
            trim: true,
            to: 1,
        });
        return header.length > 0 ? header[0] : [];
    }
    /**
     * Stringify rows back to CSV text. `columns` fixes the column order and set;
     * cells missing from a row serialize empty, keys not in `columns` are
     * dropped. With `includeHeader: false` only the data rows are emitted (used
     * to append rows after an existing body without rewriting it).
     */
    stringifyRows(rows, columns, includeHeader = true) {
        if (columns.length === 0) {
            return '';
        }
        if (rows.length === 0) {
            return includeHeader ? columns.join(',') + '\n' : '';
        }
        return stringify(rows, { header: includeHeader, columns });
    }
    /**
     * Filter rows. Supports equality (`{ col: 'x' }`), substring
     * (`{ col: { contains } }`), and range (`{ col: { gte, lte } }`). Range
     * comparisons are numeric when both the cell and the bound parse as finite
     * numbers, otherwise lexical — sensible defaults for an untyped CSV.
     */
    filterRows(rows, filter) {
        return rows.filter(row => {
            for (const [column, condition] of Object.entries(filter)) {
                const value = row[column];
                if (typeof condition === 'object' && condition !== null) {
                    const { gte, lte, contains } = condition;
                    if (contains !== undefined) {
                        if (typeof value !== 'string' || !value.includes(contains)) {
                            return false;
                        }
                    }
                    if (gte !== undefined && compareCell(value, gte) < 0) {
                        return false;
                    }
                    if (lte !== undefined && compareCell(value, lte) > 0) {
                        return false;
                    }
                }
                else if (value !== condition) {
                    return false;
                }
            }
            return true;
        });
    }
}
/**
 * Compare a CSV cell against a bound. Numeric when both sides parse as finite
 * numbers, otherwise string comparison. Returns <0, 0, or >0; an
 * absent/undefined cell sorts before any bound.
 */
function compareCell(value, bound) {
    if (value === undefined || value === null)
        return -1;
    const vNum = Number(value);
    const bNum = Number(bound);
    if (value !== '' && !Number.isNaN(vNum) && !Number.isNaN(bNum)) {
        return vNum < bNum ? -1 : vNum > bNum ? 1 : 0;
    }
    const b = String(bound);
    return value < b ? -1 : value > b ? 1 : 0;
}
//# sourceMappingURL=csv-service.js.map