/**
 * Parser and type definitions for table calculation functions
 * Supporting both row-based and pivot-based functions
 */
// ============================================================================
// Function Type Enums
// ============================================================================
/**
 * All supported table calculation function types
 */
export var TableCalculationFunctionType;
(function (TableCalculationFunctionType) {
    // Row functions (operate across rows, within same column)
    TableCalculationFunctionType["ROW"] = "row";
    TableCalculationFunctionType["INDEX"] = "index";
    TableCalculationFunctionType["OFFSET"] = "offset";
    TableCalculationFunctionType["OFFSET_LIST"] = "offset_list";
    TableCalculationFunctionType["LOOKUP"] = "lookup";
    TableCalculationFunctionType["LIST"] = "list";
    // Pivot functions (operate across pivoted columns, within same row)
    TableCalculationFunctionType["PIVOT_COLUMN"] = "pivot_column";
    TableCalculationFunctionType["PIVOT_INDEX"] = "pivot_index";
    TableCalculationFunctionType["PIVOT_OFFSET"] = "pivot_offset";
    TableCalculationFunctionType["PIVOT_OFFSET_LIST"] = "pivot_offset_list";
    TableCalculationFunctionType["PIVOT_ROW"] = "pivot_row";
    TableCalculationFunctionType["PIVOT_WHERE"] = "pivot_where";
    // Aggregate functions (reference column/row totals)
    TableCalculationFunctionType["TOTAL"] = "total";
    TableCalculationFunctionType["ROW_TOTAL"] = "row_total";
})(TableCalculationFunctionType || (TableCalculationFunctionType = {}));
// ============================================================================
// Function Categories
// ============================================================================
/**
 * Row functions - operate across rows in unpivoted data
 */
export const ROW_FUNCTIONS = [
    TableCalculationFunctionType.ROW,
    TableCalculationFunctionType.INDEX,
    TableCalculationFunctionType.OFFSET,
    TableCalculationFunctionType.OFFSET_LIST,
    TableCalculationFunctionType.LOOKUP,
    // LIST is a pure value-construction function (no windowing), but it's grouped
    // with row functions because it's not a pivot function and shares the same
    // compilation path (non-pivoted table calculations).
    TableCalculationFunctionType.LIST,
];
/**
 * Pivot functions - operate across columns in pivoted data
 */
export const PIVOT_FUNCTIONS = [
    TableCalculationFunctionType.PIVOT_COLUMN,
    TableCalculationFunctionType.PIVOT_INDEX,
    TableCalculationFunctionType.PIVOT_OFFSET,
    TableCalculationFunctionType.PIVOT_OFFSET_LIST,
    TableCalculationFunctionType.PIVOT_ROW,
    TableCalculationFunctionType.PIVOT_WHERE,
];
/**
 * Aggregate functions - reference column or row totals
 * Pre-processed in MetricQueryBuilder before function compilation.
 */
export const AGGREGATE_FUNCTIONS = [
    TableCalculationFunctionType.TOTAL,
    TableCalculationFunctionType.ROW_TOTAL,
];
// ============================================================================
// Parser Functions
// ============================================================================
/**
 * Parse table calculation SQL to extract function calls
 */
export function parseTableCalculationFunctions(sql) {
    const functions = [];
    let match;
    // Parse pivot_offset function calls with proper nested parentheses handling
    const pivotOffsetRegex = /pivot_offset\s*\(/gi;
    match = pivotOffsetRegex.exec(sql);
    while (match !== null) {
        const startIdx = match.index;
        const argsStart = match.index + match[0].length;
        // Find the matching closing parenthesis
        let parenCount = 1;
        let endIdx = -1;
        let commaIdx = -1;
        for (let i = argsStart; i < sql.length; i += 1) {
            if (sql[i] === '(') {
                parenCount += 1;
            }
            else if (sql[i] === ')') {
                parenCount -= 1;
                if (parenCount === 0) {
                    endIdx = i;
                    break;
                }
            }
            else if (sql[i] === ',' && parenCount === 1 && commaIdx === -1) {
                // Found the comma separating the two arguments at the top level
                commaIdx = i;
            }
        }
        if (endIdx !== -1 && commaIdx !== -1) {
            const expression = sql.slice(argsStart, commaIdx).trim();
            const offsetStr = sql.slice(commaIdx + 1, endIdx).trim();
            const columnOffset = parseInt(offsetStr, 10);
            if (!Number.isNaN(columnOffset)) {
                functions.push({
                    type: TableCalculationFunctionType.PIVOT_OFFSET,
                    expression,
                    columnOffset,
                    rawSql: sql.slice(startIdx, endIdx + 1),
                });
            }
        }
        match = pivotOffsetRegex.exec(sql);
    }
    // Parse pivot_column function calls
    const pivotColumnRegex = /pivot_column\s*\(\s*\)/gi;
    match = pivotColumnRegex.exec(sql);
    while (match !== null) {
        functions.push({
            type: TableCalculationFunctionType.PIVOT_COLUMN,
            rawSql: match[0],
        });
        match = pivotColumnRegex.exec(sql);
    }
    // Parse pivot_index function calls with proper nested parentheses handling
    const pivotIndexRegex = /pivot_index\s*\(/gi;
    match = pivotIndexRegex.exec(sql);
    while (match !== null) {
        const startIdx = match.index;
        const argsStart = match.index + match[0].length;
        // Find the matching closing parenthesis
        let parenCount = 1;
        let endIdx = -1;
        let commaIdx = -1;
        for (let i = argsStart; i < sql.length; i += 1) {
            if (sql[i] === '(') {
                parenCount += 1;
            }
            else if (sql[i] === ')') {
                parenCount -= 1;
                if (parenCount === 0) {
                    endIdx = i;
                    break;
                }
            }
            else if (sql[i] === ',' && parenCount === 1 && commaIdx === -1) {
                // Found the comma separating the two arguments at the top level
                commaIdx = i;
            }
        }
        if (endIdx !== -1 && commaIdx !== -1) {
            const expression = sql.slice(argsStart, commaIdx).trim();
            const indexStr = sql.slice(commaIdx + 1, endIdx).trim();
            const pivotIndex = parseInt(indexStr, 10);
            if (!Number.isNaN(pivotIndex)) {
                functions.push({
                    type: TableCalculationFunctionType.PIVOT_INDEX,
                    expression,
                    pivotIndex,
                    rawSql: sql.slice(startIdx, endIdx + 1),
                });
            }
        }
        match = pivotIndexRegex.exec(sql);
    }
    // Parse pivot_where function calls using a simpler approach
    // We'll find the function start, then carefully parse the two arguments
    const pivotWhereRegex = /pivot_where\s*\(/gi;
    let pivotMatch = pivotWhereRegex.exec(sql);
    while (pivotMatch !== null) {
        const startIdx = pivotMatch.index + pivotMatch[0].length;
        let parenCount = 1;
        let commaIdx = -1;
        let endIdx = -1;
        // Find the comma separating the two arguments and the closing parenthesis
        for (let i = startIdx; i < sql.length; i += 1) {
            if (sql[i] === '(') {
                parenCount += 1;
            }
            else if (sql[i] === ')') {
                parenCount -= 1;
                if (parenCount === 0) {
                    endIdx = i;
                    break;
                }
            }
            else if (sql[i] === ',' && parenCount === 1 && commaIdx === -1) {
                commaIdx = i;
            }
        }
        if (commaIdx !== -1 && endIdx !== -1) {
            const selectExpression = sql.slice(startIdx, commaIdx).trim();
            const valueExpression = sql.slice(commaIdx + 1, endIdx).trim();
            const rawSql = sql.slice(pivotMatch.index, endIdx + 1);
            functions.push({
                type: TableCalculationFunctionType.PIVOT_WHERE,
                selectExpression,
                valueExpression,
                rawSql,
            });
        }
        pivotMatch = pivotWhereRegex.exec(sql);
    }
    // Parse pivot_offset_list function calls
    const pivotOffsetListRegex = /pivot_offset_list\s*\(\s*([^,()]+(?:\([^)]*\))?[^,()]*)\s*,\s*(-?\d+)\s*,\s*(\d+)\s*\)/gi;
    match = pivotOffsetListRegex.exec(sql);
    while (match !== null) {
        functions.push({
            type: TableCalculationFunctionType.PIVOT_OFFSET_LIST,
            expression: match[1].trim(),
            columnOffset: parseInt(match[2], 10),
            numValues: parseInt(match[3], 10),
            rawSql: match[0],
        });
        match = pivotOffsetListRegex.exec(sql);
    }
    // Parse pivot_row function calls
    const pivotRowRegex = /pivot_row\s*\(\s*([^()]+(?:\([^)]*\))?[^()]*)\s*\)/gi;
    match = pivotRowRegex.exec(sql);
    while (match !== null) {
        functions.push({
            type: TableCalculationFunctionType.PIVOT_ROW,
            expression: match[1].trim(),
            rawSql: match[0],
        });
        match = pivotRowRegex.exec(sql);
    }
    // Parse row_total function calls (before total to avoid partial match)
    // NOTE: This uses balanced-paren matching (not quote-based field extraction) because it
    // identifies function types in expressions, not field IDs. For the quote-based field ID
    // extraction used in CTE generation, see extractTotalReferences() and buildTotalFieldRegex().
    const rowTotalStartRegex = /\brow_total\s*\(/gi;
    let rowTotalMatch = rowTotalStartRegex.exec(sql);
    while (rowTotalMatch !== null) {
        const argsStart = rowTotalMatch.index + rowTotalMatch[0].length;
        let parenCount = 1;
        let endIdx = -1;
        for (let i = argsStart; i < sql.length; i += 1) {
            if (sql[i] === '(') {
                parenCount += 1;
            }
            else if (sql[i] === ')') {
                parenCount -= 1;
                if (parenCount === 0) {
                    endIdx = i;
                    break;
                }
            }
        }
        if (endIdx !== -1) {
            const expression = sql.slice(argsStart, endIdx).trim();
            functions.push({
                type: TableCalculationFunctionType.ROW_TOTAL,
                expression,
                rawSql: sql.slice(rowTotalMatch.index, endIdx + 1),
            });
        }
        rowTotalMatch = rowTotalStartRegex.exec(sql);
    }
    // Parse total function calls (uses \b to avoid matching row_total)
    const totalStartRegex = /\btotal\s*\(/gi;
    let totalMatch = totalStartRegex.exec(sql);
    while (totalMatch !== null) {
        // Skip if this is actually row_total
        const startIdx = totalMatch.index;
        const beforeMatch = sql.slice(Math.max(0, startIdx - 4), startIdx);
        if (!beforeMatch.endsWith('row_')) {
            const argsStart = startIdx + totalMatch[0].length;
            let parenCount = 1;
            let endIdx = -1;
            for (let i = argsStart; i < sql.length; i += 1) {
                if (sql[i] === '(') {
                    parenCount += 1;
                }
                else if (sql[i] === ')') {
                    parenCount -= 1;
                    if (parenCount === 0) {
                        endIdx = i;
                        break;
                    }
                }
            }
            if (endIdx !== -1) {
                const expression = sql.slice(argsStart, endIdx).trim();
                functions.push({
                    type: TableCalculationFunctionType.TOTAL,
                    expression,
                    rawSql: sql.slice(startIdx, endIdx + 1),
                });
            }
        }
        totalMatch = totalStartRegex.exec(sql);
    }
    // Parse row function calls
    const rowRegex = /row\s*\(\s*\)/gi;
    match = rowRegex.exec(sql);
    while (match !== null) {
        functions.push({
            type: TableCalculationFunctionType.ROW,
            rawSql: match[0],
        });
        match = rowRegex.exec(sql);
    }
    // Parse offset function calls
    // The \b word boundary ensures we don't match pivot_offset
    const offsetRegex = /\boffset\(/gi;
    match = offsetRegex.exec(sql);
    while (match !== null) {
        const startIdx = match.index;
        // Parse the function arguments properly, handling nested parentheses
        let parenDepth = 1;
        let i = startIdx + match[0].length;
        const argStart = i;
        let firstArgEnd = -1;
        let secondArgStart = -1;
        while (i < sql.length && parenDepth > 0) {
            if (sql[i] === '(') {
                parenDepth += 1;
            }
            else if (sql[i] === ')') {
                parenDepth -= 1;
            }
            else if (sql[i] === ',' &&
                parenDepth === 1 &&
                firstArgEnd === -1) {
                firstArgEnd = i;
                secondArgStart = i + 1;
            }
            i += 1;
        }
        if (firstArgEnd !== -1 && parenDepth === 0) {
            const column = sql.slice(argStart, firstArgEnd).trim();
            const rowOffset = sql.slice(secondArgStart, i - 1).trim();
            const rawSql = sql.slice(startIdx, i);
            functions.push({
                type: TableCalculationFunctionType.OFFSET,
                column,
                rowOffset,
                rawSql,
            });
        }
        match = offsetRegex.exec(sql);
    }
    // Parse index function calls (but not pivot_index)
    const indexRegex = /\bindex\s*\(\s*([^,()]+(?:\([^)]*\))?[^,()]*)\s*,\s*(\d+)\s*\)/gi;
    match = indexRegex.exec(sql);
    while (match !== null) {
        // Skip if this is actually a pivot_index
        const startIdx = match.index;
        const beforeMatch = sql.slice(Math.max(0, startIdx - 6), startIdx);
        if (!beforeMatch.endsWith('pivot_')) {
            functions.push({
                type: TableCalculationFunctionType.INDEX,
                expression: match[1].trim(),
                rowIndex: parseInt(match[2], 10),
                rawSql: match[0],
            });
        }
        match = indexRegex.exec(sql);
    }
    // Parse offset_list function calls (but not pivot_offset_list)
    const offsetListRegex = /\boffset_list\s*\(\s*([^,()]+(?:\([^)]*\))?[^,()]*)\s*,\s*(-?\d+)\s*,\s*(\d+)\s*\)/gi;
    match = offsetListRegex.exec(sql);
    while (match !== null) {
        const startIdx = match.index;
        const beforeMatch = sql.slice(Math.max(0, startIdx - 6), startIdx);
        if (!beforeMatch.endsWith('pivot_')) {
            functions.push({
                type: TableCalculationFunctionType.OFFSET_LIST,
                column: match[1].trim(),
                rowOffset: parseInt(match[2], 10),
                numValues: parseInt(match[3], 10),
                rawSql: match[0],
            });
        }
        match = offsetListRegex.exec(sql);
    }
    // Parse list function calls (but not offset_list)
    // Use balanced parenthesis matching since list() has variable arity
    const listStartRegex = /\blist\s*\(/gi;
    let listMatch = listStartRegex.exec(sql);
    while (listMatch !== null) {
        // Check this isn't offset_list
        const startIdx = listMatch.index;
        const beforeMatch = sql.slice(Math.max(0, startIdx - 7), startIdx);
        if (!beforeMatch.endsWith('offset_')) {
            const argsStart = startIdx + listMatch[0].length;
            let parenCount = 1;
            let endIdx = -1;
            for (let i = argsStart; i < sql.length; i += 1) {
                if (sql[i] === '(') {
                    parenCount += 1;
                }
                else if (sql[i] === ')') {
                    parenCount -= 1;
                    if (parenCount === 0) {
                        endIdx = i;
                        break;
                    }
                }
            }
            if (endIdx !== -1) {
                const argsStr = sql.slice(argsStart, endIdx);
                // Split by commas at top level (not inside nested parens).
                // Note: this doesn't track string quotes, so list('a,b', c) would
                // mis-split. Acceptable since values are typically column references.
                const values = [];
                let depth = 0;
                let lastSplit = 0;
                for (let i = 0; i < argsStr.length; i += 1) {
                    if (argsStr[i] === '(')
                        depth += 1;
                    else if (argsStr[i] === ')')
                        depth -= 1;
                    else if (argsStr[i] === ',' && depth === 0) {
                        values.push(argsStr.slice(lastSplit, i).trim());
                        lastSplit = i + 1;
                    }
                }
                values.push(argsStr.slice(lastSplit).trim());
                functions.push({
                    type: TableCalculationFunctionType.LIST,
                    values: values.filter((v) => v.length > 0),
                    rawSql: sql.slice(startIdx, endIdx + 1),
                });
            }
        }
        listMatch = listStartRegex.exec(sql);
    }
    // Parse lookup function calls
    const lookupRegex = /\blookup\s*\(\s*([^,()]+(?:\([^)]*\))?[^,()]*)\s*,\s*([^,()]+(?:\([^)]*\))?[^,()]*)\s*,\s*([^,()]+(?:\([^)]*\))?[^,()]*)\s*\)/gi;
    match = lookupRegex.exec(sql);
    while (match !== null) {
        functions.push({
            type: TableCalculationFunctionType.LOOKUP,
            value: match[1].trim(),
            lookupColumn: match[2].trim(),
            resultColumn: match[3].trim(),
            rawSql: match[0],
        });
        match = lookupRegex.exec(sql);
    }
    // Sort functions by their position in the SQL from last to first (reverse order)
    functions.sort((a, b) => {
        const posA = sql.indexOf(a.rawSql);
        const posB = sql.indexOf(b.rawSql);
        return posB - posA; // Reversed: B - A instead of A - B
    });
    return functions;
}
/**
 * Checks if the given function type is a pivot function.
 * Pivot functions operate across pivot columns (horizontal).
 * @param type - The table calculation function type
 * @returns True if the function is a pivot function
 */
export function isPivotFunction(type) {
    return PIVOT_FUNCTIONS.includes(type);
}
/**
 * Checks if the given function type is a row function.
 * Row functions operate across rows (vertical).
 * @param type - The table calculation function type
 * @returns True if the function is a row function
 */
export function isRowFunction(type) {
    return ROW_FUNCTIONS.includes(type);
}
/**
 * Checks if any of the parsed functions are pivot functions.
 * @param functions - Array of parsed table calculation functions
 * @returns True if any function is a pivot function
 */
export function hasPivotFunctions(functions) {
    return functions.some((f) => isPivotFunction(f.type));
}
/**
 * Checks if any of the parsed functions are row functions.
 * @param functions - Array of parsed table calculation functions
 * @returns True if any function is a row function
 */
export function hasRowFunctions(functions) {
    return functions.some((f) => isRowFunction(f.type));
}
/**
 * Checks if the given function type is an aggregate function (total/row_total).
 */
export function isAggregateFunction(type) {
    return AGGREGATE_FUNCTIONS.includes(type);
}
/**
 * Checks if any of the parsed functions are aggregate functions (total/row_total).
 */
export function hasAggregateFunctions(functions) {
    return functions.some((f) => isAggregateFunction(f.type));
}
/**
 * Builds regexes for matching total("field") and row_total("field") in compiled SQL.
 * Used by both extractTotalReferences (detection) and MetricQueryBuilder.replaceTotalReferences (rewriting)
 * to ensure consistent matching logic.
 *
 * @param quoteChar - Warehouse-specific quote char (e.g. `"` for Postgres, `` ` `` for BigQuery).
 *                    When omitted, matches any of `"`, `'`, or `` ` `` (used for detection where
 *                    the warehouse dialect isn't relevant).
 */
export function buildTotalFieldRegex(quoteChar) {
    const q = quoteChar
        ? quoteChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
        : `"'\``;
    const fieldGroup = `[^${q}]+`;
    return {
        totalRegex: new RegExp(`\\btotal\\s*\\(\\s*[${q}](${fieldGroup})[${q}]\\s*\\)`, 'gi'),
        rowTotalRegex: new RegExp(`\\brow_total\\s*\\(\\s*[${q}](${fieldGroup})[${q}]\\s*\\)`, 'gi'),
    };
}
/**
 * Extracts field IDs referenced by total() and row_total() calls in compiled table calculations.
 * Scans compiledSql for patterns like total("field_id") and row_total("field_id").
 *
 * NOTE: This extracts field IDs from compiled SQL (for CTE generation), which is a different
 * concern from parseTableCalculationFunctions() which identifies function *types* in expressions
 * (for compilation). They are intentionally separate parsers.
 */
export function extractTotalReferences(compiledTableCalcs) {
    const totalFields = new Set();
    const rowTotalFields = new Set();
    const { totalRegex, rowTotalRegex } = buildTotalFieldRegex();
    for (const tc of compiledTableCalcs) {
        let m;
        // Extract row_total references
        rowTotalRegex.lastIndex = 0;
        m = rowTotalRegex.exec(tc.compiledSql);
        while (m !== null) {
            rowTotalFields.add(m[1]);
            m = rowTotalRegex.exec(tc.compiledSql);
        }
        // Extract total references (skip row_total matches)
        totalRegex.lastIndex = 0;
        m = totalRegex.exec(tc.compiledSql);
        while (m !== null) {
            const startIdx = m.index;
            const before = tc.compiledSql.slice(Math.max(0, startIdx - 4), startIdx);
            if (!before.endsWith('row_')) {
                totalFields.add(m[1]);
            }
            m = totalRegex.exec(tc.compiledSql);
        }
    }
    return {
        totalFields: [...totalFields],
        rowTotalFields: [...rowTotalFields],
    };
}
// ============================================================================
// Function Compiler Class
// ============================================================================
/**
 * Compiler for table calculation functions to SQL
 */
export class TableCalculationFunctionCompiler {
    constructor(warehouseSqlBuilder) {
        this.warehouseSqlBuilder = warehouseSqlBuilder;
    }
    /**
     * Compiles all functions in a SQL expression.
     * @param sql - The SQL expression containing pivot functions
     * @param functions - Array of parsed table calculation functions
     * @param orderByClause - Optional ORDER BY clause content for row functions (e.g. '"date" ASC, "revenue" DESC')
     * @returns SQL with functions compiled to their SQL equivalents
     */
    compileFunctions(sql, functions, orderByClause) {
        let processedSql = sql;
        for (const func of functions) {
            switch (func.type) {
                case TableCalculationFunctionType.PIVOT_OFFSET: {
                    const pivotOffsetFunc = func;
                    const compiled = this.compilePivotOffset(pivotOffsetFunc.expression, pivotOffsetFunc.columnOffset);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.PIVOT_COLUMN: {
                    const compiled = this.compilePivotColumn();
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.PIVOT_INDEX: {
                    const pivotIndexFunc = func;
                    const compiled = this.compilePivotIndex(pivotIndexFunc.expression, pivotIndexFunc.pivotIndex);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.PIVOT_WHERE: {
                    const pivotWhereFunc = func;
                    const compiled = this.compilePivotWhere(pivotWhereFunc.selectExpression, pivotWhereFunc.valueExpression);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.PIVOT_OFFSET_LIST: {
                    const pivotOffsetListFunc = func;
                    const compiled = this.compilePivotOffsetList(pivotOffsetListFunc.expression, pivotOffsetListFunc.columnOffset, pivotOffsetListFunc.numValues);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.PIVOT_ROW: {
                    const pivotRowFunc = func;
                    const compiled = this.compilePivotRow(pivotRowFunc.expression);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.ROW: {
                    const compiled = TableCalculationFunctionCompiler.compileRow(orderByClause);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.OFFSET: {
                    const offsetFunc = func;
                    const compiled = TableCalculationFunctionCompiler.compileOffset(offsetFunc.column, offsetFunc.rowOffset, orderByClause);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.INDEX: {
                    const indexFunc = func;
                    const compiled = TableCalculationFunctionCompiler.compileIndex(indexFunc.expression, indexFunc.rowIndex, orderByClause);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.OFFSET_LIST: {
                    const offsetListFunc = func;
                    const compiled = this.compileOffsetList(offsetListFunc.column, offsetListFunc.rowOffset, offsetListFunc.numValues, orderByClause);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.LIST: {
                    const listFunc = func;
                    const compiled = this.compileList(listFunc.values);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                case TableCalculationFunctionType.LOOKUP: {
                    const lookupFunc = func;
                    const compiled = TableCalculationFunctionCompiler.compileLookup(lookupFunc.value, lookupFunc.lookupColumn, lookupFunc.resultColumn);
                    processedSql = processedSql.replace(func.rawSql, compiled);
                    break;
                }
                default:
                    // Not implemented yet
                    break;
            }
        }
        return processedSql;
    }
    /**
     * Compiles a pivot_offset function call to a guarded SQL window function.
     * Converts pivot_offset(expression, n) to LAG or LEAD with adjacency checks.
     * Walks across pivot columns within the same row (Looker semantics): negative
     * n looks at the pivot column n positions to the left, positive n to the right.
     * Returns NULL if the adjacent pivot column at the offset position is not exactly
     * n columns away (i.e. when the offset would skip over missing pivot columns).
     * @param expression - The SQL expression inside pivot_offset
     * @param columnOffset - The offset value (negative for LAG/previous pivot column, positive for LEAD/next pivot column)
     * @returns Compiled SQL with CASE statement checking for adjacent pivot columns on the same row
     */
    compilePivotOffset(expression, columnOffset) {
        const q = this.warehouseSqlBuilder.getFieldQuoteChar();
        if (columnOffset === 0) {
            // No offset, return expression as-is
            return expression;
        }
        const windowFunction = columnOffset < 0 ? 'LAG' : 'LEAD';
        const offsetValue = Math.abs(columnOffset);
        // Partition by row_index so the window walks across pivot columns within a row,
        // ordered by column_index (the pivoted dimension position).
        const windowClause = `OVER (PARTITION BY ${q}row_index${q} ORDER BY ${q}column_index${q})`;
        // Guard: only return the value if the column_index at the offset position
        // is exactly columnOffset away, otherwise return NULL. This prevents
        // skipping over missing pivot columns (e.g. jumping from column 2 to column 5).
        return `CASE WHEN ${windowFunction}(${q}column_index${q}, ${offsetValue}) ${windowClause} = ${q}column_index${q} + (${columnOffset}) THEN ${windowFunction}(${expression}, ${offsetValue}) ${windowClause} ELSE NULL END`;
    }
    /**
     * Compiles a pivot_column function call to return the current column index.
     * The pivot_column() function returns the index of the current pivot column.
     * @returns SQL expression that retrieves the column_index value
     */
    compilePivotColumn() {
        const q = this.warehouseSqlBuilder.getFieldQuoteChar();
        return `${q}column_index${q}`;
    }
    /**
     * Compiles a pivot_index function call to return a value from a specific pivot column.
     * Uses a window function to find the value from the row with the same row_index
     * but the specified column_index.
     * @param expression - The SQL expression to evaluate
     * @param pivotIndex - The specific pivot column index (0-based)
     * @returns SQL with window function to retrieve value from specified column
     */
    compilePivotIndex(expression, pivotIndex) {
        const q = this.warehouseSqlBuilder.getFieldQuoteChar();
        // Use MAX (or any aggregate) with a CASE to get the value from the row
        // with matching row_index and the specified column_index
        // The window partitions by row_index to group all columns of the same row
        return `MAX(CASE WHEN ${q}column_index${q} = ${pivotIndex} THEN ${expression} ELSE NULL END) OVER (PARTITION BY ${q}row_index${q})`;
    }
    /**
     * Compiles a pivot_where function call to return a value from the first pivot column matching a condition.
     * Evaluates the select expression for each pivot column and returns the value expression
     * from the first column where the condition is true.
     * @param selectExpression - Boolean expression to evaluate for each pivot column
     * @param valueExpression - Expression to return when condition matches
     * @returns SQL with conditional aggregation to find first matching pivot column
     */
    compilePivotWhere(selectExpression, valueExpression) {
        const q = this.warehouseSqlBuilder.getFieldQuoteChar();
        // Use MIN with column_index to get the first matching column
        // Then use that column_index to get the value
        return `MAX(CASE WHEN ${q}column_index${q} = (SELECT MIN(${q}column_index${q}) FROM (SELECT ${q}column_index${q}, ${selectExpression} AS condition FROM DUAL) WHERE condition = TRUE) THEN ${valueExpression} ELSE NULL END) OVER (PARTITION BY ${q}row_index${q})`;
    }
    /**
     * Compiles a pivot_offset_list function call to return an array of values from consecutive pivot columns.
     * Uses PostgreSQL array construction syntax with window functions.
     * @param expression - The SQL expression to evaluate
     * @param columnOffset - Starting column offset (negative = previous, positive = next)
     * @param numValues - Number of consecutive values to return
     * @returns SQL array construction with window functions
     */
    compilePivotOffsetList(expression, columnOffset, numValues) {
        const q = this.warehouseSqlBuilder.getFieldQuoteChar();
        // Build array of values from consecutive pivot columns
        const arrayElements = [];
        for (let i = 0; i < numValues; i += 1) {
            const offset = columnOffset + i;
            if (offset === 0) {
                // Current column
                arrayElements.push(expression);
            }
            else {
                // Use LAG or LEAD for other columns, with adjacency guard
                const windowFunction = offset < 0 ? 'LAG' : 'LEAD';
                const offsetValue = Math.abs(offset);
                const windowClause = `OVER (PARTITION BY ${q}row_index${q} ORDER BY ${q}column_index${q})`;
                // Guard: only return the value if the column_index at the offset position
                // is exactly the expected column_index away, otherwise return NULL.
                // This prevents skipping over missing columns (e.g., jumping from column 2 to column 5).
                arrayElements.push(`CASE WHEN ${windowFunction}(${q}column_index${q}, ${offsetValue}) ${windowClause} = ${q}column_index${q} + (${offset}) THEN ${windowFunction}(${expression}, ${offsetValue}) ${windowClause} ELSE NULL END`);
            }
        }
        // Use warehouse-specific array construction
        return this.warehouseSqlBuilder.buildArray(arrayElements);
    }
    /**
     * Compiles a pivot_row function call to return all values from the current row across all pivot columns.
     * Uses warehouse-specific array aggregation to collect all values for the expression across pivot columns.
     * @param expression - The SQL expression to evaluate for each pivot column
     * @returns SQL array aggregation for all pivot values in current row
     */
    compilePivotRow(expression) {
        const q = this.warehouseSqlBuilder.getFieldQuoteChar();
        // Array aggregation with window functions doesn't support ORDER BY inside the aggregate
        // We need to use the base array aggregation without ordering and put ORDER BY in the window clause
        const baseAgg = this.warehouseSqlBuilder.buildArrayAgg(expression);
        return `${baseAgg} OVER (PARTITION BY ${q}row_index${q} ORDER BY ${q}column_index${q} ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)`;
    }
    // ========================================================================
    // Row function compilation methods
    // ========================================================================
    static buildOrderByWindow(orderByClause) {
        return orderByClause ? `OVER (ORDER BY ${orderByClause})` : 'OVER ()';
    }
    static compileRow(orderByClause) {
        return `ROW_NUMBER() ${TableCalculationFunctionCompiler.buildOrderByWindow(orderByClause)}`;
    }
    static compileOffset(column, rowOffset, orderByClause) {
        // Try to parse the offset as a number
        const offsetNum = parseInt(rowOffset, 10);
        // LEAD/LAG require ORDER BY on Snowflake (and it's good practice on all warehouses).
        // If no sorts are defined in the query, fall back to ordering by the column itself.
        const effectiveOrderBy = orderByClause ?? column;
        if (!Number.isNaN(offsetNum)) {
            // It's a valid number, use the original logic
            if (offsetNum === 0) {
                return column;
            }
            const windowFunction = offsetNum < 0 ? 'LAG' : 'LEAD';
            const offsetValue = Math.abs(offsetNum);
            return `${windowFunction}(${column}, ${offsetValue}) ${TableCalculationFunctionCompiler.buildOrderByWindow(effectiveOrderBy)}`;
        }
        // Not a number - it's an expression, assume negative offset (LAG)
        // Most dynamic offsets are for period-over-period comparisons (looking backwards)
        return `LAG(${column}, ${rowOffset}) ${TableCalculationFunctionCompiler.buildOrderByWindow(effectiveOrderBy)}`;
    }
    static compileIndex(expression, rowIndex, orderByClause) {
        if (rowIndex < 1) {
            throw new Error(`index() rowIndex must be >= 1 (got ${rowIndex}). NTH_VALUE is 1-based.`);
        }
        const window = orderByClause
            ? `OVER (ORDER BY ${orderByClause} ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)`
            : 'OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)';
        return `NTH_VALUE(${expression}, ${rowIndex}) ${window}`;
    }
    compileOffsetList(column, rowOffset, numValues, orderByClause) {
        const arrayElements = [];
        for (let i = 0; i < numValues; i += 1) {
            const offset = rowOffset + i;
            if (offset === 0) {
                arrayElements.push(column);
            }
            else {
                const windowFunction = offset < 0 ? 'LAG' : 'LEAD';
                const offsetValue = Math.abs(offset);
                arrayElements.push(`${windowFunction}(${column}, ${offsetValue}) ${TableCalculationFunctionCompiler.buildOrderByWindow(orderByClause)}`);
            }
        }
        return this.warehouseSqlBuilder.buildArray(arrayElements);
    }
    compileList(values) {
        return this.warehouseSqlBuilder.buildArray(values);
    }
    static compileLookup(value, lookupColumn, resultColumn) {
        // MAX returns the largest matching value when multiple rows match the lookup.
        // Looker uses first-match-in-sort-order (FIRST_VALUE with ordering), but that
        // adds significant complexity for an unlikely edge case in practice.
        return `MAX(CASE WHEN ${lookupColumn} = ${value} THEN ${resultColumn} ELSE NULL END) OVER ()`;
    }
}
//# sourceMappingURL=tableCalculationFunctions.js.map