import { columnReference, identifier, } from "../sql-parser/nodes.js";
import { expressionToSql } from "../sql-parser/compile.js";
import { getColumnName } from "../sql-parser/ast-helpers.js";
import { Ident, QIdent } from "../../sql-parser/tokenizer.js";
import { parseJsonPointer, buildSqliteJsonPath, } from "../../../schema-definition/json-pointer.js";
import { isJsonType } from "../../../schema-definition/json-type.js";
import { getStoredSchema } from "../../../stored-schema/get-stored-schema.js";
import { LixSchemaViewMap } from "../../../database/schema-view-map.js";
export const escapeSqlString = (input) => input.replace(/'/g, "''");
export const literal = (value) => {
    if (value === undefined || value === null) {
        return "NULL";
    }
    if (typeof value === "number") {
        if (!Number.isFinite(value)) {
            return "NULL";
        }
        return String(value);
    }
    if (typeof value === "bigint") {
        return value.toString();
    }
    if (typeof value === "boolean") {
        return value ? "1" : "0";
    }
    if (typeof value === "string") {
        return `'${escapeSqlString(value)}'`;
    }
    if (value instanceof Uint8Array) {
        let hex = "";
        for (const byte of value) {
            hex += byte.toString(16).padStart(2, "0");
        }
        return `X'${hex}'`;
    }
    if (value instanceof Date) {
        return `'${escapeSqlString(value.toISOString())}'`;
    }
    const serialized = JSON.stringify(value);
    if (serialized === undefined) {
        return "NULL";
    }
    return `'${escapeSqlString(serialized)}'`;
};
export const isExpressionValue = (value) => typeof value === "object" &&
    value !== null &&
    value.kind === "expression" &&
    typeof value.sql === "string";
export function deserializeJsonParameter(value) {
    if (value == null)
        return null;
    if (typeof value !== "string") {
        return value;
    }
    return JSON.parse(value);
}
export function buildColumnValueMap(columns, values) {
    if (columns.length !== values.length)
        return null;
    const map = new Map();
    for (let i = 0; i < columns.length; i++) {
        const columnName = columns[i];
        if (columnName === undefined)
            return null;
        map.set(columnName, values[i]);
    }
    return map;
}
export function buildColumnExpressionMap(columns, expressions) {
    if (columns.length !== expressions.length)
        return null;
    const map = new Map();
    for (let i = 0; i < columns.length; i++) {
        const columnName = columns[i];
        if (columnName === undefined)
            return null;
        const expr = expressions[i];
        if (expr === undefined)
            return null;
        map.set(columnName, expr);
    }
    return map;
}
/**
 * Builds a map from lower-case property names to their canonical casing.
 *
 * @example
 * ```ts
 * const lookup = buildPropertyLookup(schema);
 * lookup.get("name"); // => "name"
 * ```
 */
export function buildPropertyLookup(schema) {
    const map = new Map();
    const properties = schema.properties ?? {};
    for (const property of Object.keys(properties)) {
        map.set(property.toLowerCase(), property);
    }
    return map;
}
/**
 * Finds the index of a keyword in a token stream, starting at a given offset.
 */
export function findKeyword(tokens, start, keyword) {
    const target = keyword.toUpperCase();
    for (let i = start; i < tokens.length; i++) {
        const image = tokens[i]?.image;
        if (!image)
            continue;
        if (image.toUpperCase() === target) {
            return i;
        }
    }
    return -1;
}
/**
 * Extracts the SQL identifier name from either a quoted or unquoted token.
 */
export function extractIdentifier(token) {
    if (!token?.image)
        return null;
    if (token.tokenType === QIdent) {
        return token.image.slice(1, -1).replace(/""/g, '"');
    }
    if (token.tokenType === Ident) {
        return token.image;
    }
    return null;
}
export function classifyViewVariant(name) {
    const lower = name.toLowerCase();
    if (lower.endsWith("_by_version"))
        return "by_version";
    if (lower.endsWith("_history"))
        return "history";
    return "base";
}
/**
 * Removes variant suffixes from a view name to recover the stored schema key.
 */
export function baseSchemaKey(name) {
    const lower = name.toLowerCase();
    if (lower.endsWith("_by_version"))
        return name.slice(0, -11);
    if (lower.endsWith("_history"))
        return name.slice(0, -8);
    return name;
}
const VIEW_VARIANT_TO_KEY = {
    base: "state",
    by_version: "state_by_version",
    history: "state_history",
};
/**
 * Returns true if a specific view variant is enabled for the schema.
 */
export function isEntityViewVariantEnabled(schema, variant) {
    const selected = schema?.["x-lix-entity-views"];
    if (!Array.isArray(selected)) {
        return true;
    }
    const viewKey = VIEW_VARIANT_TO_KEY[variant];
    return selected.some((entry) => typeof entry === "string" && entry.toLowerCase() === viewKey);
}
export function resolveStoredSchemaKey(schema, fallbackKey) {
    const key = schema?.["x-lix-key"];
    if (typeof key === "string" && key.length > 0) {
        return key;
    }
    return fallbackKey;
}
function candidateStoredSchemaKeys(schemaKey) {
    if (!schemaKey) {
        return [];
    }
    const candidates = [schemaKey];
    if (!schemaKey.startsWith("lix_")) {
        candidates.push(`lix_${schemaKey}`);
    }
    return candidates;
}
export function loadLixSchemaDefinition(engine, schemaKey) {
    for (const candidate of candidateStoredSchemaKeys(schemaKey)) {
        const schema = getStoredSchema({ engine, key: candidate });
        if (schema) {
            return schema;
        }
    }
    return null;
}
export function extractPrimaryKeys(schema) {
    const pk = schema["x-lix-primary-key"];
    if (!Array.isArray(pk) || pk.length === 0) {
        return null;
    }
    const descriptors = [];
    for (const entry of pk) {
        if (typeof entry !== "string" || entry.length === 0) {
            return null;
        }
        if (!entry.startsWith("/")) {
            return null;
        }
        const segments = parseJsonPointer(entry);
        if (segments.length === 0) {
            return null;
        }
        const root = segments[0].toLowerCase();
        const column = segments[segments.length - 1].toLowerCase();
        descriptors.push({
            column,
            rootColumn: root,
            path: segments.map((segment) => segment),
        });
    }
    return descriptors;
}
/**
 * Derives column expressions for JSON pointers referenced by a schema's primary key.
 * These descriptors allow entity view rewrites to match WHERE clauses that target
 * nested structures using either the pointer name (e.g. `/value/x-lix-key`) or
 * friendly aliases such as `key`.
 *
 * @example
 * const columns = collectPointerColumnDescriptors({ schema });
 * // columns[0]?.alias === "key"
 * // columns[0]?.expression === `json_extract(snapshot_content, '$.value."x-lix-key"')`
 */
export function collectPointerColumnDescriptors(args) {
    const primaryKeys = args.primaryKeys ?? extractPrimaryKeys(args.schema);
    if (!primaryKeys || primaryKeys.length === 0) {
        return [];
    }
    const descriptors = new Map();
    for (const descriptor of primaryKeys) {
        if (!descriptor.path || descriptor.path.length <= 1) {
            continue;
        }
        const lastSegment = descriptor.path.at(-1) ?? "";
        const aliasInfo = buildPointerAliasInfo(lastSegment);
        if (!aliasInfo)
            continue;
        const aliasLower = aliasInfo.alias.toLowerCase();
        if (descriptors.has(aliasLower)) {
            continue;
        }
        const expressionPath = descriptor.path;
        const matcherSet = new Set();
        for (const matcher of aliasInfo.matchers) {
            if (matcher.length > 0) {
                matcherSet.add(matcher.toLowerCase());
            }
        }
        if (descriptor.column.length > 0) {
            matcherSet.add(descriptor.column.toLowerCase());
        }
        descriptors.set(aliasLower, {
            alias: aliasInfo.alias,
            path: expressionPath,
            matchers: Array.from(matcherSet),
        });
    }
    return Array.from(descriptors.values());
}
/**
 * Helper that returns an explicitly provided column value or a fallback default.
 */
export function getColumnOrDefault(columnMap, column, defaultValue) {
    if (columnMap.has(column)) {
        return columnMap.get(column);
    }
    const lowerColumn = column.toLowerCase();
    if (columnMap.has(lowerColumn)) {
        return columnMap.get(lowerColumn);
    }
    return defaultValue;
}
/**
 * Evaluates metadata overrides defined via x-lix-override-lixcols. Values that are strings are treated
 * as CEL expressions; other primitive values are passed through unchanged.
 */
export function resolveMetadataDefaults(args) {
    const { defaults, cel } = args;
    const resolved = new Map();
    if (!defaults || typeof defaults !== "object") {
        return resolved;
    }
    const context = args.context ?? {};
    for (const [key, raw] of Object.entries(defaults)) {
        if (typeof raw === "string") {
            if (!cel) {
                throw new Error(`Encountered x-lix-override-lixcols entry "${key}" but CEL evaluation is not initialised.`);
            }
            const value = cel.evaluate(raw, { ...context });
            if (value !== undefined) {
                resolved.set(key, value);
                context[key] = value;
            }
            continue;
        }
        if (raw !== undefined) {
            const normalized = normalizeOverrideValue(raw);
            resolved.set(key, normalized);
            context[key] = normalized;
        }
    }
    return resolved;
}
export function normalizeOverrideValue(value) {
    if (typeof value !== "string") {
        return value;
    }
    const trimmed = value.trim();
    if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
        try {
            return JSON.parse(trimmed);
        }
        catch (error) {
            void error;
        }
    }
    return value;
}
export function buildSnapshotObjectExpression(args) {
    const properties = Object.keys(args.schema.properties ?? {});
    if (properties.length === 0) {
        return "json_object()";
    }
    const entries = [];
    for (const prop of properties) {
        const def = (args.schema.properties ?? {})[prop];
        const lower = prop.toLowerCase();
        if (!args.columnMap.has(lower)) {
            const defaultExpr = renderDefaultSnapshotValue({
                propertyName: prop,
                definition: def,
                literal: args.literal,
                cel: args.cel,
                context: args.context,
                resolvedDefaults: args.resolvedDefaults,
            });
            entries.push(`'${escapeSqlString(prop)}', ${defaultExpr}`);
            continue;
        }
        const rawValue = args.columnMap.get(lower);
        const expression = args.columnExpressions.get(lower) ?? null;
        const snapshotExpr = renderSnapshotValue({
            definition: def,
            value: rawValue,
            expression,
        });
        entries.push(`'${escapeSqlString(prop)}', ${snapshotExpr}`);
    }
    return `json_object(${entries.join(", ")})`;
}
export function buildCelContext(args) {
    const context = {};
    for (const [lowerName, actualName] of args.propertyLowerToActual.entries()) {
        if (!args.columnMap.has(lowerName))
            continue;
        const raw = args.columnMap.get(lowerName);
        if (raw === undefined || isExpressionValue(raw))
            continue;
        context[actualName] = raw;
    }
    return context;
}
export function renderPointerExpression(args) {
    const path = args.descriptor.path;
    if (path.length === 0) {
        return null;
    }
    const root = path[0].toLowerCase();
    const baseValue = args.columnMap.get(root);
    const baseExpr = (() => {
        const expr = args.columnExpressions.get(root);
        if (expr)
            return expr;
        if (baseValue === undefined)
            return null;
        if (isExpressionValue(baseValue)) {
            return baseValue.sql;
        }
        return literal(baseValue);
    })();
    if (!baseExpr) {
        return null;
    }
    if (path.length === 1) {
        return baseExpr;
    }
    const jsonPath = buildSqliteJsonPath(path.slice(1));
    return `json_extract(${baseExpr}, '${jsonPath}')`;
}
export function renderDefaultSnapshotValue(args) {
    const { propertyName, definition, literal: literalFn, cel, context, resolvedDefaults, } = args;
    if (resolvedDefaults.has(propertyName)) {
        const cached = resolvedDefaults.get(propertyName);
        return renderSnapshotValue({ definition, value: cached, expression: null });
    }
    if (!definition || typeof definition !== "object") {
        return "NULL";
    }
    const record = definition;
    const celExpression = record["x-lix-default"];
    if (typeof celExpression === "string") {
        if (!cel) {
            throw new Error(`Encountered x-lix-default on ${propertyName} but CEL evaluation is not initialised.`);
        }
        const value = cel.evaluate(celExpression, { ...context });
        resolvedDefaults.set(propertyName, value);
        return renderSnapshotValue({
            definition,
            value,
            expression: null,
        });
    }
    if (record.default !== undefined) {
        resolvedDefaults.set(propertyName, record.default);
        return literalFn(record.default);
    }
    return "NULL";
}
export function renderSnapshotValue(args) {
    const { definition, value, expression } = args;
    if (expression && expression !== "NULL") {
        if (definition &&
            typeof definition === "object" &&
            isJsonType(definition)) {
            const trimmed = expression.trim().toLowerCase();
            if (trimmed.startsWith("json(") ||
                trimmed.startsWith("json_quote(") ||
                trimmed.startsWith("json_extract(") ||
                trimmed.startsWith("json_set(") ||
                trimmed.startsWith("case")) {
                return expression;
            }
            return `CASE WHEN json_valid(${expression}) THEN json(${expression}) ELSE json_quote(${expression}) END`;
        }
        return expression;
    }
    if (value === undefined || value === null) {
        return "NULL";
    }
    if (definition && typeof definition === "object" && isJsonType(definition)) {
        const literalExpr = literal(value);
        return `CASE WHEN json_valid(${literalExpr}) THEN json(${literalExpr}) ELSE json_quote(${literalExpr}) END`;
    }
    if (isExpressionValue(value)) {
        return value.sql;
    }
    return literal(value);
}
export function resolveSchemaDefinition(storedSchemas, viewName) {
    const candidates = buildSchemaKeyCandidates(viewName);
    for (const candidate of candidates) {
        const direct = storedSchemas.get(candidate);
        if (direct && isSchemaDefinition(direct)) {
            return direct;
        }
    }
    const normalizedCandidates = new Set(candidates.map((candidate) => candidate.toLowerCase()));
    for (const [key, value] of storedSchemas) {
        if (!value)
            continue;
        if (!isSchemaDefinition(value))
            continue;
        if (normalizedCandidates.has(key.toLowerCase())) {
            return value;
        }
    }
    const builtin = resolveBuiltinSchemaDefinition(candidates);
    if (builtin) {
        return builtin;
    }
    return null;
}
function buildSchemaKeyCandidates(viewName) {
    const candidates = new Set();
    const addCandidate = (value) => {
        if (typeof value === "string" && value.length > 0) {
            candidates.add(value);
        }
    };
    addCandidate(viewName);
    const base = baseSchemaKey(viewName);
    addCandidate(base);
    const baseValue = base ?? viewName;
    const lowerBase = baseValue.toLowerCase();
    if (lowerBase.startsWith("lix_")) {
        addCandidate(baseValue.slice(4));
    }
    else {
        addCandidate(`lix_${baseValue}`);
    }
    return Array.from(candidates);
}
function isSchemaDefinition(value) {
    return typeof value === "object" && value !== null;
}
function resolveBuiltinSchemaDefinition(candidates) {
    for (const candidate of candidates) {
        if (!candidate)
            continue;
        const normalized = stripLixPrefix(candidate);
        const direct = LixSchemaViewMap[normalized];
        if (direct) {
            return direct;
        }
        const lower = normalized.toLowerCase();
        for (const [key, schema] of Object.entries(LixSchemaViewMap)) {
            if (key.toLowerCase() === lower) {
                return schema;
            }
        }
    }
    return null;
}
function stripLixPrefix(candidate) {
    if (!candidate)
        return candidate;
    if (candidate.toLowerCase().startsWith("lix_")) {
        return candidate.slice(4);
    }
    return candidate;
}
function buildPointerAliasInfo(segment) {
    if (!segment) {
        return null;
    }
    const stripped = segment.replace(/^x-lix-/, "");
    let alias = stripped.replace(/[^A-Za-z0-9_]/g, "_");
    if (alias.length === 0) {
        return null;
    }
    if (/^[0-9]/.test(alias)) {
        alias = `_${alias}`;
    }
    const matchers = new Set();
    matchers.add(alias);
    matchers.add(alias.toLowerCase());
    if (stripped.length > 0) {
        matchers.add(stripped);
        matchers.add(stripped.toLowerCase());
    }
    matchers.add(segment);
    matchers.add(segment.toLowerCase());
    const sanitizedSegment = segment.replace(/[^A-Za-z0-9_]/g, "_");
    if (sanitizedSegment.length > 0) {
        matchers.add(sanitizedSegment);
        matchers.add(sanitizedSegment.toLowerCase());
    }
    return {
        alias,
        matchers: Array.from(matchers),
    };
}
export function resolveEntityView(args) {
    const { storedSchemas, viewName } = args;
    if (!storedSchemas || !viewName) {
        return null;
    }
    const variant = classifyViewVariant(viewName);
    if (variant === "history") {
        return null;
    }
    const schema = resolveSchemaDefinition(storedSchemas, viewName);
    if (!schema) {
        return null;
    }
    if (!isEntityViewVariantEnabled(schema, variant)) {
        return null;
    }
    if (args.rejectImmutable && schema["x-lix-immutable"]) {
        throw new Error(`Schema ${schema["x-lix-key"] ?? viewName} is immutable`);
    }
    const storedSchemaKey = resolveStoredSchemaKey(schema, baseSchemaKey(viewName) ?? viewName);
    return {
        schema,
        variant,
        storedSchemaKey,
        propertyLowerToActual: buildPropertyLookup(schema),
    };
}
export function collectEqualityConditions(whereClause) {
    if (!whereClause) {
        return [];
    }
    if ("sql_text" in whereClause) {
        return [];
    }
    const conditions = [];
    const visit = (expression) => {
        switch (expression.node_kind) {
            case "binary_expression":
                if (expression.operator === "and") {
                    visit(expression.left);
                    visit(expression.right);
                    return;
                }
                if (expression.operator === "=") {
                    if (expression.left.node_kind === "column_reference") {
                        const column = getColumnName(expression.left).toLowerCase();
                        conditions.push({ column, expression: expression.right });
                        return;
                    }
                    if (expression.right.node_kind === "column_reference") {
                        const column = getColumnName(expression.right).toLowerCase();
                        conditions.push({ column, expression: expression.left });
                        return;
                    }
                }
                break;
            case "grouped_expression":
                visit(expression.expression);
                return;
            default:
                return;
        }
    };
    visit(whereClause);
    return conditions;
}
function buildPrimaryKeyMatcherMap(primaryKeys) {
    const map = new Map();
    if (!primaryKeys || primaryKeys.length === 0) {
        return map;
    }
    for (let index = 0; index < primaryKeys.length; index++) {
        const descriptor = primaryKeys[index];
        if (!descriptor)
            continue;
        const allSegments = new Set();
        allSegments.add(descriptor.column.toLowerCase());
        allSegments.add(descriptor.rootColumn.toLowerCase());
        for (const segment of descriptor.path) {
            allSegments.add(segment.toLowerCase());
        }
        const aliasInfo = buildPointerAliasInfo(descriptor.path.at(-1) ?? descriptor.column);
        if (aliasInfo) {
            for (const matcher of aliasInfo.matchers) {
                allSegments.add(matcher.toLowerCase());
            }
        }
        for (const matcher of allSegments) {
            if (!map.has(matcher)) {
                map.set(matcher, index);
            }
        }
    }
    return map;
}
function serializeEqualityValue(expression) {
    switch (expression.node_kind) {
        case "parameter": {
            const position = expression.position ?? -1;
            if (position >= 0) {
                return `?${position + 1}`;
            }
            const placeholder = expression.placeholder ?? "?";
            return placeholder === "" ? "?" : placeholder;
        }
        case "literal":
            return expressionToSql(expression);
        case "raw_fragment":
            return expression.sql_text;
        default:
            return null;
    }
}
export function collectPrimaryKeyEqualityExpressions(args) {
    const { whereClause, primaryKeys } = args;
    if (!primaryKeys || primaryKeys.length === 0) {
        return null;
    }
    const equalityConditions = collectEqualityConditions(whereClause);
    if (equalityConditions.length === 0) {
        return null;
    }
    const matcherMap = buildPrimaryKeyMatcherMap(primaryKeys);
    if (matcherMap.size === 0) {
        return null;
    }
    const matched = new Map();
    for (const condition of equalityConditions) {
        const targetIndex = matcherMap.get(condition.column);
        if (targetIndex === undefined) {
            continue;
        }
        if (matched.has(targetIndex)) {
            continue;
        }
        const sql = serializeEqualityValue(condition.expression);
        if (!sql) {
            return null;
        }
        matched.set(targetIndex, sql);
    }
    if (matched.size !== primaryKeys.length) {
        return null;
    }
    const ordered = primaryKeys.map((_pk, index) => matched.get(index));
    if (ordered.some((value) => value === undefined)) {
        return null;
    }
    return ordered;
}
export function buildEntityIdExpressionFromEquality(parts) {
    if (!parts || parts.length === 0) {
        return null;
    }
    if (parts.some((part) => part.length === 0)) {
        return null;
    }
    if (parts.length === 1) {
        return parts[0] ?? null;
    }
    const wrapped = parts.map((part) => `(${part})`);
    return `(${wrapped.join(" || '~' || ")})`;
}
const VIEW_METADATA_COLUMN_MAP = {
    entity_id: "entity_id",
    lixcol_entity_id: "entity_id",
    schema_key: "schema_key",
    lixcol_schema_key: "schema_key",
    file_id: "file_id",
    lixcol_file_id: "file_id",
    plugin_key: "plugin_key",
    lixcol_plugin_key: "plugin_key",
    version_id: "version_id",
    lixcol_version_id: "version_id",
    metadata: "metadata",
    lixcol_metadata: "metadata",
    untracked: "untracked",
    lixcol_untracked: "untracked",
};
export function rewriteViewWhereClause(whereClause, options) {
    if (!whereClause) {
        return {
            expression: null,
            hasVersionReference: false,
            usesJsonProperty: false,
        };
    }
    if ("sql_text" in whereClause) {
        return null;
    }
    const pointerFlags = {
        hasVersionReference: false,
        usesJsonProperty: false,
    };
    const rewritten = rewritePredicateExpression(whereClause, options.propertyLowerToActual, options.stateTableAlias, pointerFlags, options.pointerAliasToPath, options.pushdownableJsonProperties, options.rewriteSubqueryStatement);
    if (!rewritten) {
        return null;
    }
    return {
        expression: rewritten,
        hasVersionReference: pointerFlags.hasVersionReference,
        usesJsonProperty: pointerFlags.usesJsonProperty,
    };
}
function rewritePredicateExpression(expression, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement) {
    switch (expression.node_kind) {
        case "binary_expression": {
            const operator = expression.operator;
            if (operator === "and" || operator === "or") {
                const left = rewritePredicateExpression(expression.left, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement);
                if (!left)
                    return null;
                const right = rewritePredicateExpression(expression.right, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement);
                if (!right)
                    return null;
                return {
                    node_kind: "binary_expression",
                    left,
                    operator,
                    right,
                };
            }
            const supportedOperators = new Set([
                "=",
                "!=",
                "<>",
                ">",
                ">=",
                "<",
                "<=",
                "like",
                "not_like",
                "match",
                "glob",
                "regexp",
                "is",
                "is_not",
            ]);
            if (!supportedOperators.has(operator)) {
                return null;
            }
            const left = rewritePredicateExpression(expression.left, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement);
            if (!left)
                return null;
            const right = rewritePredicateExpression(expression.right, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement);
            if (!right)
                return null;
            return {
                node_kind: "binary_expression",
                left,
                operator,
                right,
            };
        }
        case "unary_expression": {
            if (expression.operator === "minus" || expression.operator === "not") {
                const operand = rewritePredicateExpression(expression.operand, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement);
                if (!operand)
                    return null;
                return {
                    node_kind: "unary_expression",
                    operator: expression.operator,
                    operand,
                };
            }
            return null;
        }
        case "grouped_expression": {
            const rewritten = rewritePredicateExpression(expression.expression, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement);
            if (!rewritten)
                return null;
            return {
                node_kind: "grouped_expression",
                expression: rewritten,
            };
        }
        case "column_reference":
            return rewriteColumnReference(expression, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
        case "function_call": {
            const args = [];
            for (const arg of expression.arguments) {
                if (arg.node_kind === "all_columns") {
                    args.push(arg);
                    continue;
                }
                const rewritten = rewritePredicateExpression(arg, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties, rewriteSubqueryStatement);
                if (!rewritten)
                    return null;
                args.push(rewritten);
            }
            const over = rewriteWindowOverForPredicate(expression.over, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
            if (expression.over && !over) {
                return null;
            }
            const call = {
                node_kind: "function_call",
                name: expression.name,
                arguments: args,
                over,
            };
            return call;
        }
        case "in_list_expression": {
            const operand = rewritePredicateExpression(expression.operand, propertyLowerToActual, stateTableAlias, flags, undefined, undefined, rewriteSubqueryStatement);
            if (!operand)
                return null;
            const items = [];
            for (const item of expression.items) {
                const rewritten = rewritePredicateExpression(item, propertyLowerToActual, stateTableAlias, flags, undefined, undefined, rewriteSubqueryStatement);
                if (!rewritten)
                    return null;
                items.push(rewritten);
            }
            return {
                node_kind: "in_list_expression",
                operand,
                items,
                negated: expression.negated,
            };
        }
        case "between_expression": {
            const operand = rewritePredicateExpression(expression.operand, propertyLowerToActual, stateTableAlias, flags, undefined, undefined, rewriteSubqueryStatement);
            if (!operand)
                return null;
            const start = rewritePredicateExpression(expression.start, propertyLowerToActual, stateTableAlias, flags, undefined, undefined, rewriteSubqueryStatement);
            if (!start)
                return null;
            const end = rewritePredicateExpression(expression.end, propertyLowerToActual, stateTableAlias, flags, undefined, undefined, rewriteSubqueryStatement);
            if (!end)
                return null;
            return {
                node_kind: "between_expression",
                operand,
                start,
                end,
                negated: expression.negated,
            };
        }
        case "literal":
        case "parameter":
            return expression;
        case "subquery_expression": {
            if (!rewriteSubqueryStatement) {
                return expression;
            }
            const statement = rewriteSubqueryStatement(expression.statement);
            if (statement === expression.statement) {
                return expression;
            }
            return {
                ...expression,
                statement,
            };
        }
        default:
            return null;
    }
}
function rewriteColumnReference(column, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties) {
    const terminal = column.path[column.path.length - 1];
    if (!terminal) {
        return null;
    }
    const columnName = terminal.value;
    const lower = columnName.toLowerCase();
    const pointerPath = pointerAliasToPath?.get(lower);
    if (pointerPath) {
        if (pushdownableJsonProperties && pushdownableJsonProperties.has(lower)) {
            flags.usesJsonProperty = true;
        }
        return createJsonExtractExpressionFromPath(pointerPath, stateTableAlias);
    }
    const property = propertyLowerToActual.get(lower);
    if (property) {
        if (pushdownableJsonProperties && pushdownableJsonProperties.has(lower)) {
            flags.usesJsonProperty = true;
        }
        return createJsonExtractExpression(property, stateTableAlias);
    }
    const metadata = VIEW_METADATA_COLUMN_MAP[lower];
    if (metadata) {
        if (metadata === "version_id") {
            flags.hasVersionReference = true;
        }
        return columnReference([stateTableAlias, metadata]);
    }
    const aliasLower = stateTableAlias.toLowerCase();
    if (column.path.length === 2 &&
        column.path[0]?.value.toLowerCase() === aliasLower) {
        const target = column.path[1].value;
        if (target.toLowerCase() === "version_id") {
            flags.hasVersionReference = true;
        }
        return columnReference([stateTableAlias, target]);
    }
    return null;
}
function rewriteWindowOverForPredicate(over, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties) {
    if (!over) {
        return null;
    }
    if (over.node_kind === "window_reference") {
        return over;
    }
    const specification = rewriteWindowSpecificationForPredicate(over, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
    return specification;
}
function rewriteWindowSpecificationForPredicate(spec, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties) {
    const partitionBy = [];
    for (const expression of spec.partition_by) {
        const rewritten = rewritePredicateExpression(expression, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
        if (!rewritten) {
            return null;
        }
        partitionBy.push(rewritten);
    }
    const orderBy = [];
    for (const item of spec.order_by) {
        const rewritten = rewritePredicateExpression(item.expression, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
        if (!rewritten) {
            return null;
        }
        orderBy.push({
            ...item,
            expression: rewritten,
        });
    }
    let frame = null;
    if (spec.frame) {
        frame = rewriteWindowFrameForPredicate(spec.frame, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
        if (!frame) {
            return null;
        }
    }
    return {
        ...spec,
        partition_by: partitionBy,
        order_by: orderBy,
        frame,
    };
}
function rewriteWindowFrameForPredicate(frame, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties) {
    const start = rewriteWindowFrameBoundForPredicate(frame.start, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
    if (!start) {
        return null;
    }
    let end = null;
    if (frame.end) {
        end = rewriteWindowFrameBoundForPredicate(frame.end, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
        if (!end) {
            return null;
        }
    }
    return {
        ...frame,
        start,
        end,
    };
}
function rewriteWindowFrameBoundForPredicate(bound, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties) {
    if (!bound.offset) {
        return bound;
    }
    const offset = rewritePredicateExpression(bound.offset, propertyLowerToActual, stateTableAlias, flags, pointerAliasToPath, pushdownableJsonProperties);
    if (!offset) {
        return null;
    }
    return {
        ...bound,
        offset,
    };
}
function createJsonExtractExpression(property, stateTableAlias) {
    const path = buildSqliteJsonPath([property]);
    return {
        node_kind: "function_call",
        name: identifier("json_extract"),
        arguments: [
            columnReference([stateTableAlias, "snapshot_content"]),
            createLiteralNode(path),
        ],
        over: null,
    };
}
function createJsonExtractExpressionFromPath(pathSegments, stateTableAlias) {
    const path = buildSqliteJsonPath(pathSegments);
    return {
        node_kind: "function_call",
        name: identifier("json_extract"),
        arguments: [
            columnReference([stateTableAlias, "snapshot_content"]),
            createLiteralNode(path),
        ],
        over: null,
    };
}
function createLiteralNode(value) {
    return {
        node_kind: "literal",
        value,
    };
}
export function combineWithAnd(left, right) {
    const normalizedLeft = requiresGrouping(left)
        ? { node_kind: "grouped_expression", expression: left }
        : left;
    const normalizedRight = requiresGrouping(right)
        ? { node_kind: "grouped_expression", expression: right }
        : right;
    return {
        node_kind: "binary_expression",
        left: normalizedLeft,
        operator: "and",
        right: normalizedRight,
    };
}
function requiresGrouping(expression) {
    if (expression.node_kind !== "binary_expression") {
        return false;
    }
    return expression.operator === "or";
}
//# sourceMappingURL=shared.js.map