import { LixSchemaViewMap } from "../database/schema-view-map.js";
/**
 * Maps table names to their corresponding schema keys.
 * Uses LixSchemaViewMap to derive the mappings dynamically.
 */
export function determineSchemaKeys(compiledQuery) {
    const tableNames = new Set();
    // Extract table names from the compiled query
    try {
        // The compiled query has a 'query' property that contains the actual AST
        const queryNode = compiledQuery.query || compiledQuery;
        // Get table names from FROM clause
        if (queryNode.from) {
            extractTableNamesFromQueryNode(queryNode.from, tableNames);
        }
        // Handle UNION/INTERSECT/EXCEPT set operations
        if (queryNode.setOperations) {
            for (const set of queryNode.setOperations) {
                extractTableNamesFromQueryNode(set, tableNames);
            }
        }
        // Get table names from JOIN clauses
        if (queryNode.joins) {
            for (const join of queryNode.joins) {
                extractTableNamesFromQueryNode(join, tableNames);
            }
        }
        // Get table names from WHERE clause subqueries
        if (queryNode.where) {
            extractTableNamesFromQueryNode(queryNode.where, tableNames);
        }
        // Get table names from SELECT clause subqueries
        if (queryNode.selections) {
            for (const selection of queryNode.selections) {
                extractTableNamesFromQueryNode(selection, tableNames);
            }
        }
    }
    catch (error) {
        console.warn("Failed to extract table names from compiled query:", error);
        return []; // Return empty array to fall back to always re-executing
    }
    // Build table to schema key mapping from LixSchemaViewMap
    const tableToSchemaMap = {};
    // Add mappings from LixSchemaViewMap
    for (const [schemaKey, schemaDefinition] of Object.entries(LixSchemaViewMap)) {
        const lixKey = schemaDefinition["x-lix-key"];
        if (lixKey) {
            tableToSchemaMap[schemaKey] = lixKey;
        }
    }
    // Some consumer-visible views (e.g. "file") are backed by descriptor tables.
    // Map those view names back to their descriptor schema keys for invalidation.
    const descriptorViewAliases = {
        file: "file_descriptor",
        directory: "directory_descriptor",
    };
    for (const [viewName, descriptorName] of Object.entries(descriptorViewAliases)) {
        const descriptorSchemaKey = tableToSchemaMap[descriptorName];
        if (descriptorSchemaKey) {
            tableToSchemaMap[viewName] = descriptorSchemaKey;
        }
    }
    // Add special tables/views that don't come from LixSchemaViewMap
    const specialMappings = {
        change: "change",
        state: "state",
        state_by_version: "state_by_version",
        version: "lix_version",
        active_version: "lix_active_version",
    };
    Object.assign(tableToSchemaMap, specialMappings);
    const schemaKeys = [];
    for (const tableName of tableNames) {
        const schemaKey = tableToSchemaMap[tableName];
        if (schemaKey) {
            schemaKeys.push(schemaKey);
        }
    }
    // Special case: the merged 'version' views are composed from descriptor + tip state
    // Changes to those underlying schema keys should invalidate queries against 'version'.
    if (tableNames.has("version") || tableNames.has("version_by_version")) {
        if (!schemaKeys.includes("lix_version_descriptor"))
            schemaKeys.push("lix_version_descriptor");
        if (!schemaKeys.includes("lix_version_tip"))
            schemaKeys.push("lix_version_tip");
    }
    // Try to detect literal schema_key filters from the compiled SQL and parameters.
    // This allows more precise dependency tracking for state_by_version queries.
    const sqlText = (compiledQuery?.sql ??
        compiledQuery?.query?.sql);
    const params = (compiledQuery?.parameters ??
        compiledQuery?.query?.parameters);
    if (typeof sqlText === "string" && Array.isArray(params)) {
        const lower = sqlText.toLowerCase();
        const idx = lower.indexOf("schema_key");
        if (idx >= 0) {
            // Count placeholders up to and including first occurrence of schema_key equality
            // This assumes placeholders are '?'.
            const before = lower.slice(0, idx);
            const qCount = (before.match(/\?/g) || []).length;
            // Look ahead for the first '?' after schema_key reference
            const after = lower.slice(idx);
            const aheadQIndex = after.indexOf("?");
            if (aheadQIndex >= 0) {
                const paramIndex = qCount; // parameter index of that placeholder
                const val = params[paramIndex];
                if (typeof val === "string" && !schemaKeys.includes(val)) {
                    schemaKeys.push(val);
                }
            }
        }
    }
    return schemaKeys;
}
/**
 * Extracts literal `schema_key`, `entity_id`, and `version_id` filters from a compiled query AST.
 */
export function extractLiteralFilters(compiledQuery) {
    const schemaKeys = new Set();
    const versionIds = new Set();
    const entityIds = new Set();
    const fileIds = new Set();
    const tableNames = new Set();
    const tableAliases = new Map();
    const root = compiledQuery?.query ?? compiledQuery;
    if (!root) {
        return { schemaKeys: [], versionIds: [], entityIds: [], fileIds: [] };
    }
    const rootQuery = root?.query ?? root;
    if (rootQuery?.from)
        extractTableNamesFromQueryNode(rootQuery.from, tableNames, tableAliases);
    if (rootQuery?.joins) {
        for (const join of rootQuery.joins) {
            extractTableNamesFromQueryNode(join, tableNames, tableAliases);
        }
    }
    if (rootQuery?.where)
        extractTableNamesFromQueryNode(rootQuery.where, tableNames, tableAliases);
    const fileTablesPresent = tableNames.has("file") || tableNames.has("file_by_version");
    const onlyFileTablesPresent = fileTablesPresent &&
        tableNames.size > 0 &&
        Array.from(tableNames).every(isFileTableName);
    const record = (column, values) => {
        if (!column || !values || values.length === 0)
            return;
        const target = column === "schema_key"
            ? schemaKeys
            : column === "version_id"
                ? versionIds
                : column === "entity_id"
                    ? entityIds
                    : column === "file_id"
                        ? fileIds
                        : undefined;
        if (!target)
            return;
        for (const value of values) {
            if (typeof value === "string") {
                target.add(value);
            }
        }
    };
    const visitFilterNode = (node) => {
        if (!node)
            return;
        switch (node.kind) {
            case "WhereNode":
                visitFilterNode(node.where);
                return;
            case "HavingNode":
                visitFilterNode(node.having);
                return;
            case "OnNode":
                visitFilterNode(node.on);
                return;
            case "ParensNode":
                visitFilterNode(node.node);
                return;
            case "AndNode":
            case "OrNode":
                visitFilterNode(node.left);
                visitFilterNode(node.right);
                return;
            case "UnaryOperationNode":
                visitFilterNode(node.operand);
                return;
            case "BinaryOperationNode": {
                const operator = node.operator?.operator ?? node.operator;
                const leftColumn = extractColumnReference(node.leftOperand);
                const rightColumn = extractColumnReference(node.rightOperand);
                const leftValues = extractLiteralValues(node.leftOperand);
                const rightValues = extractLiteralValues(node.rightOperand);
                if (leftColumn?.column === "schema_key" ||
                    leftColumn?.column === "version_id" ||
                    leftColumn?.column === "entity_id" ||
                    leftColumn?.column === "file_id") {
                    record(leftColumn.column, rightValues);
                }
                if (rightColumn?.column === "schema_key" ||
                    rightColumn?.column === "version_id" ||
                    rightColumn?.column === "entity_id" ||
                    rightColumn?.column === "file_id") {
                    record(rightColumn.column, leftValues);
                }
                // Heuristic: file queries filter on 'id' column; capture as file_id when file table is present.
                if (fileTablesPresent) {
                    if (isFileIdColumn(leftColumn, onlyFileTablesPresent, tableAliases))
                        record("file_id", rightValues);
                    if (isFileIdColumn(rightColumn, onlyFileTablesPresent, tableAliases))
                        record("file_id", leftValues);
                }
                // For IN/NOT IN operators, both sides can carry literals. Already handled above.
                // Dive deeper in case operands contain additional filters (e.g. nested expressions)
                if (operator === "in" || operator === "not in") {
                    // Operands already processed.
                }
                visitFilterNode(node.leftOperand);
                visitFilterNode(node.rightOperand);
                return;
            }
            case "ValueListNode":
            case "TupleNode":
            case "ListNode":
                for (const value of node.values ?? node.items ?? []) {
                    visitFilterNode(value);
                }
                return;
            default:
                if (Array.isArray(node)) {
                    for (const child of node)
                        visitFilterNode(child);
                    return;
                }
        }
    };
    const extractLiteralValues = (node) => {
        if (!node)
            return undefined;
        switch (node.kind) {
            case "ValueNode": {
                const value = unwrapValue(node.value);
                return value !== undefined ? [value] : undefined;
            }
            case "PrimitiveValueListNode": {
                const values = [];
                for (const v of node.values ?? []) {
                    if (typeof v === "string")
                        values.push(v);
                }
                return values.length > 0 ? values : undefined;
            }
            case "ValueListNode": {
                const values = [];
                for (const child of node.values ?? []) {
                    const nested = extractLiteralValues(child);
                    if (nested)
                        values.push(...nested);
                }
                return values.length > 0 ? values : undefined;
            }
            case "TupleNode": {
                const values = [];
                for (const child of node.values ?? []) {
                    const nested = extractLiteralValues(child);
                    if (nested)
                        values.push(...nested);
                }
                return values.length > 0 ? values : undefined;
            }
            case "ParensNode":
                return extractLiteralValues(node.node);
            default:
                return undefined;
        }
    };
    const unwrapValue = (value) => {
        if (typeof value === "string")
            return value;
        if (value &&
            typeof value === "object" &&
            "value" in value &&
            typeof value.value === "string") {
            return value.value;
        }
        return undefined;
    };
    const queryNode = root?.query ?? root;
    if (queryNode?.where)
        visitFilterNode(queryNode.where);
    if (queryNode?.having)
        visitFilterNode(queryNode.having);
    if (queryNode?.joins) {
        for (const join of queryNode.joins) {
            if (join?.on)
                visitFilterNode(join.on);
        }
    }
    return {
        schemaKeys: Array.from(schemaKeys),
        versionIds: Array.from(versionIds),
        entityIds: Array.from(entityIds),
        fileIds: Array.from(fileIds),
    };
}
/**
 * Extracts table names from Kysely AST nodes recursively.
 */
function extractTableNamesFromQueryNode(node, tableNames, tableAliases) {
    if (!node)
        return;
    // Handle different Kysely AST node types
    switch (node.kind) {
        case "TableNode": {
            // Extract table name from TableNode
            const tableName = extractTableIdentifier(node.table);
            if (tableName) {
                tableNames.add(tableName);
            }
            break;
        }
        case "FromNode": {
            // Process all tables in FROM clause
            if (node.froms && Array.isArray(node.froms)) {
                for (const from of node.froms) {
                    extractTableNamesFromQueryNode(from, tableNames);
                }
            }
            break;
        }
        case "JoinNode": {
            // Process the joined table
            if (node.table) {
                extractTableNamesFromQueryNode(node.table, tableNames);
            }
            break;
        }
        case "SelectQueryNode": {
            // Process subqueries recursively
            if (node.from) {
                extractTableNamesFromQueryNode(node.from, tableNames);
            }
            if (node.setOperations) {
                for (const set of node.setOperations) {
                    extractTableNamesFromQueryNode(set, tableNames);
                }
            }
            if (node.joins) {
                for (const join of node.joins) {
                    extractTableNamesFromQueryNode(join, tableNames);
                }
            }
            if (node.where) {
                extractTableNamesFromQueryNode(node.where, tableNames);
            }
            if (node.selections) {
                for (const selection of node.selections) {
                    extractTableNamesFromQueryNode(selection, tableNames);
                }
            }
            break;
        }
        case "AliasNode": {
            // Look inside the aliased node
            if (node.node) {
                const aliasedTableName = node.node.kind === "TableNode"
                    ? extractTableIdentifier(node.node.table)
                    : extractTableIdentifier(node.node);
                if (aliasedTableName && node.alias?.name) {
                    tableAliases?.set(node.alias.name, aliasedTableName);
                }
                extractTableNamesFromQueryNode(node.node, tableNames, tableAliases);
            }
            break;
        }
        case "SetOperationNode": {
            if (node.expression) {
                extractTableNamesFromQueryNode(node.expression, tableNames, tableAliases);
            }
            break;
        }
        default: {
            // For other node types, recursively check all properties
            if (typeof node === "object") {
                for (const key in node) {
                    const value = node[key];
                    if (value && typeof value === "object") {
                        if (Array.isArray(value)) {
                            for (const item of value) {
                                if (item && typeof item === "object" && item.kind) {
                                    extractTableNamesFromQueryNode(item, tableNames, tableAliases);
                                }
                            }
                        }
                        else if (value.kind) {
                            extractTableNamesFromQueryNode(value, tableNames, tableAliases);
                        }
                    }
                }
            }
            break;
        }
    }
}
function isFileTableName(tableName) {
    return tableName === "file" || tableName === "file_by_version";
}
function extractColumnReference(node) {
    if (!node)
        return undefined;
    switch (node.kind) {
        case "ReferenceNode": {
            const table = extractTableIdentifier(node.table);
            const nested = extractColumnReference(node.column);
            return {
                table: table ?? nested?.table,
                column: nested?.column,
            };
        }
        case "ColumnNode": {
            const table = extractTableIdentifier(node.table);
            const nested = extractColumnReference(node.column);
            return {
                table: table ?? nested?.table,
                column: nested?.column,
            };
        }
        case "IdentifierNode":
            return { column: typeof node.name === "string" ? node.name : undefined };
        case "AliasNode":
        case "ParensNode":
            return extractColumnReference(node.node);
        default:
            return undefined;
    }
}
function extractTableIdentifier(node) {
    if (!node)
        return undefined;
    switch (node.kind) {
        case "TableNode":
            return extractTableIdentifier(node.table);
        case "SchemableIdentifierNode":
            return extractTableIdentifier(node.identifier);
        case "IdentifierNode":
            return typeof node.name === "string" ? node.name : undefined;
        default:
            return undefined;
    }
}
function isFileIdColumn(columnRef, onlyFileTablesPresent, tableAliases) {
    if (!columnRef || columnRef.column !== "id")
        return false;
    if (columnRef.table) {
        const resolvedTable = tableAliases.get(columnRef.table) ?? columnRef.table;
        return isFileTableName(resolvedTable);
    }
    return onlyFileTablesPresent;
}
//# sourceMappingURL=determine-schema-keys.js.map