/**
 * Shared helpers for local file parsing tools.
 * Extracted from localFileTools.ts for modularity.
 */
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { safePath } from "../security/index.js";
/**
 * resolveLocalPath — validates and resolves file paths through the security sandbox.
 *
 * Allows: cwd, home dir (for GAIA tasks), temp dir, and monorepo root.
 * Blocks: ~/.ssh, ~/.aws, ~/.ethereum, .env, symlink escape.
 */
function findMonorepoRoot() {
    let dir = process.cwd();
    for (let i = 0; i < 10; i++) {
        if (existsSync(path.join(dir, "package.json")) && existsSync(path.join(dir, ".git")))
            return dir;
        const parent = path.dirname(dir);
        if (parent === dir)
            break;
        dir = parent;
    }
    return process.cwd();
}
const _monorepoRoot = findMonorepoRoot();
export function resolveLocalPath(inputPath) {
    return safePath(inputPath, {
        allowHome: true,
        allowTemp: true,
        allowedRoots: [process.cwd(), _monorepoRoot],
    });
}
export function clampInt(value, fallback, min, max) {
    const n = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
    if (!Number.isFinite(n))
        return fallback;
    return Math.max(min, Math.min(max, n));
}
export function truncateCell(value, maxChars) {
    if (value === null || value === undefined)
        return null;
    if (typeof value === "number")
        return value;
    if (typeof value === "boolean")
        return value;
    if (value instanceof Date)
        return value.toISOString();
    const s = String(value);
    if (s.length <= maxChars)
        return s;
    return s.slice(0, maxChars) + "...";
}
export function toText(value) {
    if (value === null || value === undefined)
        return "";
    if (typeof value === "string")
        return value;
    if (typeof value === "number" || typeof value === "boolean")
        return String(value);
    if (value instanceof Date)
        return value.toISOString();
    return String(value);
}
export function toNumberOrNull(value) {
    if (value === null || value === undefined)
        return null;
    if (typeof value === "number" && Number.isFinite(value))
        return value;
    const s = toText(value).trim();
    if (!s)
        return null;
    // Strip common formatting (currency, thousands separators, %). Keep digits/sign/decimal/exponent.
    const cleaned = s.replace(/[, $£€¥%]/g, "").replace(/[^\d.+\-eE]/g, "");
    const n = Number.parseFloat(cleaned);
    return Number.isFinite(n) ? n : null;
}
export function toIntegerOrNull(value) {
    if (value === null || value === undefined)
        return null;
    if (typeof value === "number" && Number.isFinite(value))
        return Math.trunc(value);
    const s = toText(value).trim();
    if (!s)
        return null;
    // For mixed strings (e.g. "102 Main St"), use the first integer-like substring.
    const m = s.match(/-?\d+/);
    if (!m)
        return null;
    const n = Number.parseInt(m[0], 10);
    return Number.isFinite(n) ? n : null;
}
export function mean(nums) {
    if (!nums.length)
        throw new Error("mean: empty array");
    return nums.reduce((s, n) => s + n, 0) / nums.length;
}
export function pstdev(nums) {
    if (!nums.length)
        throw new Error("pstdev: empty array");
    const m = mean(nums);
    const v = nums.reduce((s, n) => s + (n - m) * (n - m), 0) / nums.length;
    return Math.sqrt(v);
}
export function stdev(nums) {
    if (nums.length < 2)
        throw new Error("stdev: need at least 2 values");
    const m = mean(nums);
    const v = nums.reduce((s, n) => s + (n - m) * (n - m), 0) / (nums.length - 1);
    return Math.sqrt(v);
}
export const WHERE_OPS = [
    "eq",
    "ne",
    "contains",
    "starts_with",
    "ends_with",
    "matches_regex",
    "gt",
    "gte",
    "lt",
    "lte",
    "is_empty",
    "not_empty",
    "is_even",
    "is_odd",
];
export function resolveColumnIndex(headers, column) {
    if (typeof column === "number" && Number.isFinite(column)) {
        const idx = Math.trunc(column);
        return idx >= 0 && idx < headers.length ? idx : -1;
    }
    const name = String(column ?? "").trim();
    if (!name)
        return -1;
    const needle = name.toLowerCase();
    return headers.findIndex((h) => String(h ?? "").trim().toLowerCase() === needle);
}
export function compileWhere(headers, whereRaw) {
    if (!Array.isArray(whereRaw))
        return [];
    const out = [];
    for (const raw of whereRaw) {
        if (!raw || typeof raw !== "object")
            continue;
        const obj = raw;
        const column = obj.column;
        const op = String(obj.op ?? "").trim();
        if (!op)
            continue;
        if (!WHERE_OPS.includes(op))
            continue;
        const columnIndex = resolveColumnIndex(headers, column);
        if (columnIndex < 0)
            continue;
        out.push({
            columnIndex,
            op,
            value: obj.value ?? null,
            caseSensitive: obj.caseSensitive === true,
        });
    }
    return out;
}
export function rowMatchesWhere(row, where) {
    if (where.length === 0)
        return true;
    for (const clause of where) {
        const cell = row[clause.columnIndex];
        if (clause.op === "is_empty") {
            const s = toText(cell).trim();
            if (s.length !== 0)
                return false;
            continue;
        }
        if (clause.op === "not_empty") {
            const s = toText(cell).trim();
            if (s.length === 0)
                return false;
            continue;
        }
        if (clause.op === "contains") {
            const hay = clause.caseSensitive ? toText(cell) : toText(cell).toLowerCase();
            const needle = clause.caseSensitive ? toText(clause.value) : toText(clause.value).toLowerCase();
            if (!hay.includes(needle))
                return false;
            continue;
        }
        if (clause.op === "starts_with") {
            const hay = clause.caseSensitive ? toText(cell) : toText(cell).toLowerCase();
            const needle = clause.caseSensitive ? toText(clause.value) : toText(clause.value).toLowerCase();
            if (!hay.startsWith(needle))
                return false;
            continue;
        }
        if (clause.op === "ends_with") {
            const hay = clause.caseSensitive ? toText(cell) : toText(cell).toLowerCase();
            const needle = clause.caseSensitive ? toText(clause.value) : toText(clause.value).toLowerCase();
            if (!hay.endsWith(needle))
                return false;
            continue;
        }
        if (clause.op === "matches_regex") {
            const pattern = toText(clause.value).trim();
            if (!pattern)
                return false;
            if (pattern.length > 200)
                return false;
            let re;
            try {
                // caseSensitive=false uses /i
                re = new RegExp(pattern, clause.caseSensitive ? "" : "i");
            }
            catch {
                return false;
            }
            if (!re.test(toText(cell)))
                return false;
            continue;
        }
        if (clause.op === "eq" || clause.op === "ne") {
            const a = clause.caseSensitive ? toText(cell).trim() : toText(cell).trim().toLowerCase();
            const b = clause.caseSensitive
                ? toText(clause.value).trim()
                : toText(clause.value).trim().toLowerCase();
            const ok = a === b;
            if (clause.op === "eq" && !ok)
                return false;
            if (clause.op === "ne" && ok)
                return false;
            continue;
        }
        if (clause.op === "is_even" || clause.op === "is_odd") {
            const n = toIntegerOrNull(cell);
            if (n === null)
                return false;
            const isEven = Math.abs(n) % 2 === 0;
            if (clause.op === "is_even" && !isEven)
                return false;
            if (clause.op === "is_odd" && isEven)
                return false;
            continue;
        }
        // Numeric comparisons.
        const n = toNumberOrNull(cell);
        const v = toNumberOrNull(clause.value);
        if (n === null || v === null)
            return false;
        if (clause.op === "gt" && !(n > v))
            return false;
        if (clause.op === "gte" && !(n >= v))
            return false;
        if (clause.op === "lt" && !(n < v))
            return false;
        if (clause.op === "lte" && !(n <= v))
            return false;
    }
    return true;
}
export async function getXlsx() {
    try {
        const mod = await import("xlsx");
        return mod.default ?? mod;
    }
    catch (err) {
        throw new Error("Missing optional dependency: xlsx. Install it (or run npm install in packages/mcp-local) to use XLSX parsing.");
    }
}
export async function getPapaParse() {
    try {
        const mod = await import("papaparse");
        return mod.default ?? mod;
    }
    catch {
        return null;
    }
}
export async function getPdfParseModule() {
    try {
        return await import("pdf-parse");
    }
    catch {
        throw new Error("Missing optional dependency: pdf-parse. Install it (or run npm install in packages/mcp-local) to use PDF parsing.");
    }
}
export async function getYauzl() {
    try {
        const mod = await import("yauzl");
        return mod.default ?? mod;
    }
    catch {
        throw new Error("Missing optional dependency: yauzl. Install it (or run npm install in packages/mcp-local) to use ZIP/DOCX/PPTX parsing.");
    }
}
export async function loadCsvTable(args, opts) {
    const encoding = String(args?.encoding ?? "utf8");
    const text = await readFile(opts.filePath, { encoding });
    const hasHeader = args?.hasHeader !== false;
    const delimiter = typeof args?.delimiter === "string" ? args.delimiter : undefined;
    const papa = await getPapaParse();
    let rows = [];
    let parseErrors = [];
    if (papa?.parse) {
        const result = papa.parse(text, {
            ...(delimiter ? { delimiter } : {}),
            skipEmptyLines: true,
            dynamicTyping: false,
        });
        rows = Array.isArray(result?.data) ? result.data : [];
        parseErrors = Array.isArray(result?.errors) ? result.errors : [];
    }
    else {
        // Minimal fallback parser: split by newlines and delimiter (no quote handling).
        const lines = String(text).split(/\r?\n/).filter((l) => l.trim().length > 0);
        rows = lines.map((l) => l.split(delimiter ?? ","));
    }
    const normalizedAll = rows
        .filter((r) => Array.isArray(r))
        .map((r) => r.slice(0, opts.maxCols).map((c) => truncateCell(c, opts.maxCellChars)));
    const headerRow = hasHeader ? normalizedAll[0] : undefined;
    const dataRowsAll = hasHeader ? normalizedAll.slice(1) : normalizedAll;
    const dataRows = dataRowsAll.slice(0, opts.maxScanRows);
    const colCount = Math.max(headerRow ? headerRow.length : 0, ...dataRows.map((r) => r.length));
    const headers = headerRow
        ? headerRow.map((h) => String(h ?? "").trim())
        : Array.from({ length: colCount }, (_, i) => `col_${i + 1}`);
    return {
        encoding,
        hasHeader,
        delimiter: delimiter ?? null,
        parseErrors,
        headers,
        dataRows,
    };
}
export async function loadXlsxTable(args, opts) {
    const XLSX = await getXlsx();
    const wb = XLSX.readFile(opts.filePath, { cellDates: true, dense: true });
    const sheets = Array.isArray(wb?.SheetNames) ? wb.SheetNames : [];
    if (sheets.length === 0)
        throw new Error(`No sheets found in workbook: ${opts.filePath}`);
    const requestedSheet = typeof args?.sheetName === "string" ? args.sheetName.trim() : "";
    const sheetName = requestedSheet || sheets[0];
    const sheet = wb.Sheets?.[sheetName];
    if (!sheet) {
        throw new Error(`Sheet not found: \"${sheetName}\". Available sheets: ${sheets.join(", ")}`);
    }
    const headerRow = clampInt(args?.headerRow, 1, 0, 1000);
    const rangeA1 = typeof args?.rangeA1 === "string" ? args.rangeA1.trim() : "";
    const table = XLSX.utils.sheet_to_json(sheet, {
        header: 1,
        blankrows: false,
        defval: "",
        ...(rangeA1 ? { range: rangeA1 } : {}),
    });
    const normalizedAll = table
        .filter((r) => Array.isArray(r))
        .map((r) => r.slice(0, opts.maxCols).map((c) => truncateCell(c, opts.maxCellChars)));
    const headerIdx = headerRow > 0 ? headerRow - 1 : -1;
    const header = headerIdx >= 0 ? normalizedAll[headerIdx] : undefined;
    const dataRowsAll = headerIdx >= 0 ? normalizedAll.slice(headerIdx + 1) : normalizedAll;
    const dataRows = dataRowsAll.slice(0, opts.maxScanRows);
    const colCount = Math.max(header ? header.length : 0, ...dataRows.map((r) => r.length));
    const headers = header
        ? header.map((h) => String(h ?? "").trim())
        : Array.from({ length: colCount }, (_, i) => `col_${i + 1}`);
    return {
        sheets,
        sheetName,
        headerRow,
        rangeA1: rangeA1 || null,
        headers,
        dataRows,
    };
}
//# sourceMappingURL=localFileHelpers.js.map