import { sql } from "kysely";
import { validateStateMutation } from "./validate-state-mutation.js";
import { insertTransactionState } from "../transaction/insert-transaction-state.js";
import { encodeStatePkPart, parseStatePk, serializeStatePk, } from "./primary-key.js";
import { getTimestampSync } from "../../engine/functions/timestamp.js";
import { insertVTableLog } from "./insert-vtable-log.js";
import { commit } from "./commit.js";
import { internalQueryBuilder } from "../../engine/internal-query-builder.js";
import { LixStoredSchemaSchema } from "../../stored-schema/schema-definition.js";
import { withRuntimeCache } from "../../engine/with-runtime-cache.js";
import { getStateCacheTables } from "../cache/schema.js";
import { updateFilePathCache } from "../../filesystem/file/cache/update-file-path-cache.js";
import { composeDirectoryPath } from "../../filesystem/directory/ensure-directories.js";
const LIX_OPEN_TRANSACTION = Symbol("lix_open_transaction");
export function setHasOpenTransaction(engine, value) {
    engine.runtimeCacheRef[LIX_OPEN_TRANSACTION] = value;
}
export function hasOpenTransaction(engine) {
    const cache = engine.runtimeCacheRef;
    const current = cache[LIX_OPEN_TRANSACTION];
    if (current === true) {
        return true;
    }
    if (current === false) {
        return false;
    }
    // fall through on initial
    const rows = engine.sqlite.exec({
        sql: "SELECT 1 FROM lix_internal_transaction_state LIMIT 1",
        returnValue: "resultRows",
    });
    setHasOpenTransaction(engine, Array.isArray(rows) && rows.length > 0);
    return engine.runtimeCacheRef[LIX_OPEN_TRANSACTION] === true;
}
// Virtual table schema definition
const VTAB_CREATE_SQL = `CREATE TABLE x(
	_pk HIDDEN TEXT NOT NULL PRIMARY KEY,
	source_tag TEXT,
	entity_id TEXT,
	schema_key TEXT,
	file_id TEXT,
	version_id TEXT,
	plugin_key TEXT,
	snapshot_content TEXT,
	schema_version TEXT,
	created_at TEXT,
	updated_at TEXT,
	inherited_from_version_id TEXT,
	change_id TEXT,
	untracked INTEGER,
	commit_id TEXT,
	metadata TEXT,
	writer_key TEXT
) WITHOUT ROWID;`;
const STATE_VTAB_COLUMN_NAMES = [
    "_pk",
    "source_tag",
    "entity_id",
    "schema_key",
    "file_id",
    "version_id",
    "plugin_key",
    "snapshot_content",
    "schema_version",
    "created_at",
    "updated_at",
    "inherited_from_version_id",
    "change_id",
    "untracked",
    "commit_id",
    "metadata",
    "writer_key",
];
const VERSION_CACHE_LIMIT = 100;
const VERSION_CACHE_QUERY = internalQueryBuilder
    .selectFrom("version")
    .select("id")
    .limit(VERSION_CACHE_LIMIT)
    .compile();
export function applyStateVTable(engine) {
    const { sqlite } = engine;
    // Statement/transaction-scoped writer value set via withWriterKey helper.
    let currentWriterKey = null;
    // Register a custom SQLite function for encoding primary key parts
    engine.sqlite.createFunction({
        name: "lix_encode_pk_part",
        deterministic: true,
        arity: 1,
        xFunc: (_ctxPtr, ...args) => {
            const part = args[0];
            if (part === null || part === undefined)
                return "";
            return encodeStatePkPart(String(part));
        },
    });
    // Register a setter UDF that stores the writer for the current statement.
    sqlite.createFunction({
        name: "lix_set_writer_key",
        deterministic: false,
        arity: 1,
        xFunc: (_ctxPtr, ...args) => {
            const v = args[0];
            currentWriterKey =
                v === null || v === undefined || v === "" ? null : String(v);
            return 1; // value unused
        },
    });
    // Getter UDF for nested-scoped restorations in withWriterKey helper
    sqlite.createFunction({
        name: "lix_get_writer_key",
        deterministic: false,
        arity: 0,
        xFunc: () => {
            return currentWriterKey ?? null;
        },
    });
    // Create virtual table using the proper SQLite WASM API (following vtab-experiment pattern)
    const capi = sqlite.sqlite3.capi;
    const module = new capi.sqlite3_module();
    // Store cursor state
    const cursorStates = new Map();
    /**
     * Flag to prevent recursion when updating cache state.
     *
     * The guard ensures that while we're marking cache as fresh, any nested state queries
     * bypass the cache and use materialized state directly, preventing recursion.
     *
     * Why is this needed is unclear. Queries are executed in sync. Why concurrent
     * reads simultaneously update the cache is not clear. Given that state
     * materialization is rare, this workaround has been deemed sufficient.
     *
     * This is a temporary fix and should be revisited in the future.
     */
    let isUpdatingCacheState = false;
    module.installMethods({
        xCreate: (dbHandle, _pAux, _argc, _argv, pVTab) => {
            const result = capi.sqlite3_declare_vtab(dbHandle, VTAB_CREATE_SQL);
            if (result !== capi.SQLITE_OK) {
                return result;
            }
            sqlite.sqlite3.vtab.xVtab.create(pVTab);
            return capi.SQLITE_OK;
        },
        xConnect: (dbHandle, _pAux, _argc, _argv, pVTab) => {
            const result = capi.sqlite3_declare_vtab(dbHandle, VTAB_CREATE_SQL);
            if (result !== capi.SQLITE_OK) {
                return result;
            }
            sqlite.sqlite3.vtab.xVtab.create(pVTab);
            return capi.SQLITE_OK;
        },
        xBegin: () => {
            setHasOpenTransaction(engine, true);
            // TODO comment in after all internal v-table logic uses underlying state view
            // // assert that we are not already in a transaction (the lix_internal_change_in_transaction table is empty)
            // const existingChangesInTransaction = executeSync({
            // 	lix: { sqlite },
            // 	query: db.selectFrom("lix_internal_change_in_transaction").selectAll(),
            // });
            // if (existingChangesInTransaction.length > 0) {
            // 	const errorMessage = "Transaction already in progress";
            // 	if (canLog()) {
            // 		createLixOwnLogSync({
            // 			lix: { sqlite, db: db as any },
            // 			key: "lix_state_xbegin_error",
            // 			level: "error",
            // 			message: `xBegin error: ${errorMessage}`,
            // 		});
            // 	}
            // 	throw new Error(errorMessage);
            // }
        },
        xCommit: () => {
            const rc = commit({ engine: engine });
            currentWriterKey = null; // clear after statement/txn
            setHasOpenTransaction(engine, false);
            return rc;
        },
        xRollback: () => {
            sqlite.exec({
                sql: "DELETE FROM lix_internal_transaction_state",
                returnValue: "resultRows",
            });
            currentWriterKey = null;
            setHasOpenTransaction(engine, false);
        },
        xBestIndex: (pVTab, pIdxInfo) => {
            try {
                const idxInfo = sqlite.sqlite3.vtab.xIndexInfo(pIdxInfo);
                // Track which columns have equality constraints
                const usableConstraints = [];
                let argIndex = 0;
                // Column mapping (matching the CREATE TABLE order in xCreate/xConnect)
                const columnMap = STATE_VTAB_COLUMN_NAMES;
                // Process constraints
                // @ts-expect-error - idxInfo.$nConstraint is not defined in the type
                for (let i = 0; i < idxInfo.$nConstraint; i++) {
                    // @ts-expect-error - idxInfo.nthConstraint is not defined in the type
                    const constraint = idxInfo.nthConstraint(i);
                    // Only handle equality constraints that are usable
                    if (constraint.$op === capi.SQLITE_INDEX_CONSTRAINT_EQ &&
                        constraint.$usable) {
                        const columnName = columnMap[constraint.$iColumn];
                        if (columnName) {
                            usableConstraints.push(columnName);
                            // Mark this constraint as used
                            // @ts-expect-error - idxInfo.nthConstraintUsage is not defined in the type
                            idxInfo.nthConstraintUsage(i).$argvIndex = ++argIndex;
                        }
                    }
                }
                const fullTableCost = 1000000; // Default cost for full table scan
                const fullTableRows = 10000000;
                // Set the index string to pass column names to xFilter
                if (usableConstraints.length > 0) {
                    const idxStr = usableConstraints.join(",");
                    // @ts-expect-error - idxInfo.$idxStr is not defined in the type
                    idxInfo.$idxStr = sqlite.sqlite3.wasm.allocCString(idxStr, false);
                    // @ts-expect-error - idxInfo.$needToFreeIdxStr is not defined in the type
                    idxInfo.$needToFreeIdxStr = 1; // We don't need SQLite to free this string
                    // Lower cost when we can use filters (more selective)
                    // @ts-expect-error - idxInfo.$estimatedCost is not defined in the type
                    idxInfo.$estimatedCost =
                        fullTableCost / (usableConstraints.length + 1);
                    // @ts-expect-error - idxInfo.$estimatedRows is not defined in the type
                    idxInfo.$estimatedRows = Math.ceil(fullTableRows / (usableConstraints.length + 1));
                }
                else {
                    // @ts-expect-error - idxInfo.$needToFreeIdxStr is not defined in the type
                    idxInfo.$needToFreeIdxStr = 0;
                    // Higher cost for full table scan
                    // @ts-expect-error - idxInfo.$estimatedCost is not defined in the type
                    idxInfo.$estimatedCost = fullTableCost;
                    // @ts-expect-error - idxInfo.$estimatedRows is not defined in the type
                    idxInfo.$estimatedRows = fullTableRows;
                }
                return capi.SQLITE_OK;
            }
            finally {
                // Always log timing even if error occurs
            }
        },
        xDisconnect: () => {
            return capi.SQLITE_OK;
        },
        xDestroy: () => {
            return capi.SQLITE_OK;
        },
        xOpen: (_pVTab, pCursor) => {
            const cursor = sqlite.sqlite3.vtab.xCursor.create(pCursor);
            cursorStates.set(cursor.pointer, {
                results: [],
                rowIndex: 0,
            });
            return capi.SQLITE_OK;
        },
        xClose: (pCursor) => {
            cursorStates.delete(pCursor);
            return capi.SQLITE_OK;
        },
        xFilter: (pCursor, idxNum, idxStrPtr, argc, argv) => {
            // throw new Error("xFilter read. Query preprocessor failed at rewrite.");
            const cursorState = cursorStates.get(pCursor);
            const idxStr = sqlite.sqlite3.wasm.cstrToJs(idxStrPtr);
            // Debug: Track recursion depth
            const recursionKey = "_vtab_recursion_depth";
            // @ts-expect-error - using global for debugging
            const currentDepth = (globalThis[recursionKey] || 0) + 1;
            // @ts-expect-error - using global for debugging
            globalThis[recursionKey] = currentDepth;
            if (currentDepth > 10) {
                // @ts-expect-error - using global for debugging
                globalThis[recursionKey] = 0; // Reset
                throw new Error(`Virtual table recursion depth exceeded: ${currentDepth}`);
            }
            try {
                // Extract filter arguments if provided
                const filters = {};
                if (argc > 0 && argv) {
                    const args = sqlite.sqlite3.capi.sqlite3_values_to_js(argc, argv);
                    // Parse idxStr to understand which columns are being filtered
                    // idxStr format: "column1,column2,..."
                    if (idxStr) {
                        const columns = idxStr.split(",").filter((c) => c.length > 0);
                        for (let i = 0; i < Math.min(columns.length, args.length); i++) {
                            if (args[i] !== null) {
                                filters[columns[i]] = args[i]; // Keep original type
                            }
                        }
                    }
                }
                // If we're updating cache state, we must use resolved state view directly to avoid recursion
                if (isUpdatingCacheState) {
                    // Query directly from resolved state (now includes tombstones)
                    let query = internalQueryBuilder
                        .selectFrom("lix_internal_state_vtable")
                        .select("_pk")
                        .selectAll();
                    // Apply filters
                    for (const [column, value] of Object.entries(filters)) {
                        query = query.where(column, "=", value);
                    }
                    const stateResults = engine.executeSync(query.compile()).rows;
                    cursorState.results = stateResults ?? [];
                    cursorState.rowIndex = 0;
                    return capi.SQLITE_OK;
                }
                let query = internalQueryBuilder
                    .selectFrom("lix_internal_state_vtable")
                    .select("_pk")
                    .selectAll();
                for (const [column, value] of Object.entries(filters)) {
                    query = query.where(column, "=", value);
                }
                const results = engine.executeSync(query.compile()).rows;
                cursorState.results = results ?? [];
                cursorState.rowIndex = 0;
                return capi.SQLITE_OK;
            }
            finally {
                // Always decrement recursion depth
                // @ts-expect-error - using global for debugging
                globalThis[recursionKey] = currentDepth - 1;
            }
        },
        xNext: (pCursor) => {
            const cursorState = cursorStates.get(pCursor);
            cursorState.rowIndex++;
            return capi.SQLITE_OK;
        },
        xEof: (pCursor) => {
            const cursorState = cursorStates.get(pCursor);
            return cursorState.rowIndex >= cursorState.results.length ? 1 : 0;
        },
        xColumn: (pCursor, pContext, iCol) => {
            const cursorState = cursorStates.get(pCursor);
            const row = cursorState.results[cursorState.rowIndex];
            if (!row) {
                capi.sqlite3_result_null(pContext);
                return capi.SQLITE_OK;
            }
            // Handle primary key column (_pk)
            if (iCol === 0) {
                if (Array.isArray(row)) {
                    // For array results, _pk is at index 0
                    capi.sqlite3_result_js(pContext, row[0]);
                }
                else if (row._pk) {
                    // If row already has _pk, use it
                    capi.sqlite3_result_js(pContext, row._pk);
                }
                else {
                    // Generate primary key from row data
                    const tag = row.untracked ? "U" : "C";
                    const primaryKey = serializeStatePk(tag, row.file_id, row.entity_id, row.version_id);
                    capi.sqlite3_result_js(pContext, primaryKey);
                }
                return capi.SQLITE_OK;
            }
            // Handle array-style results from SQLite exec
            let value;
            if (Array.isArray(row)) {
                // For array results, composite_key is at index 0, so we use iCol directly
                value = row[iCol];
            }
            else {
                const columnName = getColumnName(iCol);
                if (columnName === "source_tag") {
                    const tag = inferSourceTag(row);
                    if (tag === null) {
                        capi.sqlite3_result_null(pContext);
                    }
                    else {
                        capi.sqlite3_result_js(pContext, tag);
                    }
                    return capi.SQLITE_OK;
                }
                if (columnName === "writer_key") {
                    // Compute writer on demand from lix_internal_state_writer with inheritance fallback
                    try {
                        const keyRows = engine.executeSync(internalQueryBuilder
                            .selectFrom("lix_internal_state_writer")
                            .select(["writer_key"])
                            .where("file_id", "=", String(row.file_id))
                            .where("version_id", "=", String(row.version_id))
                            .where("entity_id", "=", String(row.entity_id))
                            .where("schema_key", "=", String(row.schema_key))
                            .limit(1)
                            .compile()).rows;
                        let w = (keyRows && keyRows[0] && keyRows[0].writer_key) ||
                            null;
                        if (!w) {
                            const parent = row.inherited_from_version_id;
                            if (parent) {
                                const parentRows = engine.executeSync(internalQueryBuilder
                                    .selectFrom("lix_internal_state_writer")
                                    .select(["writer_key"])
                                    .where("file_id", "=", String(row.file_id))
                                    .where("version_id", "=", String(parent))
                                    .where("entity_id", "=", String(row.entity_id))
                                    .where("schema_key", "=", String(row.schema_key))
                                    .limit(1)
                                    .compile()).rows;
                                w =
                                    (parentRows &&
                                        parentRows[0] &&
                                        parentRows[0].writer_key) ||
                                        null;
                            }
                        }
                        if (w === null || w === undefined) {
                            capi.sqlite3_result_null(pContext);
                        }
                        else {
                            capi.sqlite3_result_js(pContext, w);
                        }
                        return capi.SQLITE_OK;
                    }
                    catch {
                        capi.sqlite3_result_null(pContext);
                        return capi.SQLITE_OK;
                    }
                }
                value = row[columnName];
            }
            // Handle special cases for null values that might be stored as strings
            if (value === "null" &&
                getColumnName(iCol) === "inherited_from_version_id") {
                capi.sqlite3_result_null(pContext);
                return capi.SQLITE_OK;
            }
            if (value === null) {
                capi.sqlite3_result_null(pContext);
            }
            else {
                capi.sqlite3_result_js(pContext, value);
            }
            return capi.SQLITE_OK;
        },
        xRowid: () => {
            // For WITHOUT ROWID tables, xRowid should not be called
            // But if it is, we return an error
            return capi.SQLITE_ERROR;
        },
        xUpdate: (_pVTab, nArg, ppArgv) => {
            try {
                const _timestamp = getTimestampSync({ engine });
                // Extract arguments using the proper SQLite WASM API
                const args = sqlite.sqlite3.capi.sqlite3_values_to_js(nArg, ppArgv);
                // DELETE operation: nArg = 1, args[0] = old primary key
                if (nArg === 1) {
                    const oldPk = args[0];
                    if (!oldPk) {
                        throw new Error("Missing primary key for DELETE operation");
                    }
                    // Use handleStateDelete for all cases - it handles both tracked and untracked
                    handleStateDelete(engine, oldPk, _timestamp);
                    const { fileId, entityId, versionId } = parseStatePk(oldPk);
                    const schemaKey = resolveSchemaKey({
                        fileId,
                        entityId,
                        versionId,
                    });
                    persistWriter({
                        fileId,
                        versionId,
                        entityId,
                        schemaKey,
                        writer: currentWriterKey,
                    });
                    return capi.SQLITE_OK;
                }
                // INSERT operation: nArg = N+2, args[0] = NULL, args[1] = new primary key
                // UPDATE operation: nArg = N+2, args[0] = old primary key, args[1] = new primary key
                const isInsert = args[0] === null;
                const isUpdate = args[0] !== null;
                if (!isInsert && !isUpdate) {
                    throw new Error("Invalid xUpdate operation");
                }
                const baseIndex = 2; // skip old and new primary key slots
                const columnOffset = (name) => {
                    const idx = STATE_VTAB_COLUMN_NAMES.indexOf(name);
                    if (idx === -1)
                        throw new Error(`Unknown column '${name}' in lix_internal_state_vtable`);
                    return idx;
                };
                const valueFor = (name) => args[baseIndex + columnOffset(name)];
                const entity_id = valueFor("entity_id");
                const schema_key = valueFor("schema_key");
                const file_id = valueFor("file_id");
                const version_id = valueFor("version_id");
                const plugin_key = valueFor("plugin_key");
                const snapshotContentValue = valueFor("snapshot_content");
                const snapshot_content = snapshotContentValue === null || snapshotContentValue === undefined
                    ? null
                    : typeof snapshotContentValue === "string"
                        ? snapshotContentValue
                        : JSON.stringify(snapshotContentValue);
                const parsedSnapshot = snapshot_content === null ? null : JSON.parse(snapshot_content);
                const schema_version = valueFor("schema_version");
                const untracked = valueFor("untracked") ?? false;
                const metadataValue = baseIndex + columnOffset("metadata") < args.length
                    ? valueFor("metadata")
                    : null;
                const inheritedFromValue = baseIndex + columnOffset("inherited_from_version_id") < args.length
                    ? valueFor("inherited_from_version_id")
                    : null;
                const requiredFieldValues = [
                    ["entity_id", entity_id],
                    ["schema_key", schema_key],
                    ["file_id", file_id],
                    ["plugin_key", plugin_key],
                ];
                const missingFields = requiredFieldValues
                    .filter(([, value]) => value === null || value === undefined || value === "")
                    .map(([name]) => name);
                if (missingFields.length > 0) {
                    throw new Error(`Missing required fields for state mutation: ${missingFields.join(", ")}`);
                }
                // Persist writer for INSERT/UPDATE
                persistWriter({
                    fileId: String(file_id),
                    versionId: String(version_id),
                    entityId: String(entity_id),
                    schemaKey: String(schema_key),
                    writer: currentWriterKey,
                });
                if (!version_id) {
                    throw new Error("version_id is required for state mutation");
                }
                // Call validation function (same logic as triggers)
                const schemaKey = String(schema_key);
                const existingVersionsCache = withVersionCache(engine);
                validateStateMutation({
                    engine: engine,
                    schema: schemaKey === LixStoredSchemaSchema["x-lix-key"]
                        ? LixStoredSchemaSchema
                        : null,
                    schemaKey,
                    snapshot_content: parsedSnapshot,
                    operation: isInsert ? "insert" : "update",
                    entity_id: String(entity_id),
                    file_id: String(file_id),
                    version_id: String(version_id),
                    untracked: Boolean(untracked),
                    existingVersionsCache,
                    ...(inheritedFromValue !== null && inheritedFromValue !== undefined
                        ? { inherited_from_version_id: String(inheritedFromValue) }
                        : {}),
                });
                const metadataJson = metadataValue === null || metadataValue === undefined
                    ? null
                    : typeof metadataValue === "string"
                        ? metadataValue
                        : JSON.stringify(metadataValue);
                // Use insertTransactionState which handles both tracked and untracked entities
                insertTransactionState({
                    engine: engine,
                    timestamp: _timestamp,
                    data: [
                        {
                            entity_id: String(entity_id),
                            schema_key: String(schema_key),
                            file_id: String(file_id),
                            plugin_key: String(plugin_key),
                            snapshot_content,
                            schema_version: String(schema_version),
                            version_id: String(version_id),
                            untracked: Boolean(untracked),
                            metadata: metadataJson,
                        },
                    ],
                });
                if (schemaKey === "lix_file_descriptor") {
                    const descriptorSnapshot = normalizeFileDescriptorSnapshot(parsedSnapshot);
                    if (descriptorSnapshot) {
                        refreshFilePathCacheEntry({
                            engine,
                            fileId: String(entity_id),
                            versionId: String(version_id),
                            directoryId: descriptorSnapshot.directoryId,
                            name: descriptorSnapshot.name,
                            extension: descriptorSnapshot.extension,
                        });
                    }
                    else {
                        deleteFilePathCacheEntry({
                            engine,
                            fileId: String(entity_id),
                            versionId: String(version_id),
                        });
                    }
                }
                // TODO: This cache copying logic is a temporary workaround for shared commits.
                // The proper solution requires improving cache miss logic to handle commit sharing
                // without duplicating entries. See: https://github.com/opral/lix-sdk/issues/309
                //
                // Handle cache copying for new versions that share commits (v2 cache)
                // Updated for commit-anchored tips: trigger on lix_version_tip writes
                if (isInsert && String(schema_key) === "lix_version_tip") {
                    // Skip tombstone inserts where snapshot_content is null
                    let tipData = null;
                    try {
                        tipData = snapshot_content ? JSON.parse(snapshot_content) : null;
                    }
                    catch {
                        // ignore parse errors; treat as tombstone/malformed and skip
                        tipData = null;
                    }
                    const newVersionId = tipData?.id;
                    const commitId = tipData?.commit_id;
                    if (newVersionId && commitId) {
                        // Find other versions that point to the same commit
                        const existingVersionsWithSameCommit = engine.executeSync(internalQueryBuilder
                            .selectFrom("lix_internal_state_vtable")
                            .select(sql `json_extract(snapshot_content, '$.id')`.as("version_id"))
                            .where("schema_key", "=", "lix_version_tip")
                            .where("version_id", "=", "global")
                            .where("commit_id", "=", commitId)
                            .where(sql `json_extract(snapshot_content, '$.id')`, "!=", newVersionId)
                            .compile()).rows;
                        // If there are existing versions with the same commit, copy their cache entries
                        if (existingVersionsWithSameCommit &&
                            existingVersionsWithSameCommit.length > 0) {
                            const sourceVersionId = existingVersionsWithSameCommit[0].version_id; // Take first existing version
                            // Get all unique schema keys from the source version
                            const tableCache = getStateCacheTables({ engine });
                            for (const tableName of tableCache) {
                                if (tableName === "lix_internal_state_cache" ||
                                    !tableName.startsWith("lix_internal_state_cache_v1_")) {
                                    continue;
                                }
                                // Check if table exists first
                                const tableExists = sqlite.exec({
                                    sql: `SELECT 1 FROM sqlite_schema WHERE type='table' AND name=?`,
                                    bind: [tableName],
                                    returnValue: "resultRows",
                                });
                                if (!tableExists || tableExists.length === 0) {
                                    continue;
                                }
                                const hasSourceRows = sqlite.exec({
                                    sql: `SELECT 1 FROM ${tableName} WHERE version_id = ? AND schema_key NOT IN ('lix_version_tip','lix_version_descriptor') LIMIT 1`,
                                    bind: [sourceVersionId],
                                    returnValue: "resultRows",
                                });
                                if (!hasSourceRows || hasSourceRows.length === 0) {
                                    continue;
                                }
                                // Copy entries from source version to new version using v2 cache structure
                                sqlite.exec({
                                    sql: `
								INSERT OR IGNORE INTO ${tableName} 
								(entity_id, schema_key, file_id, version_id, plugin_key, snapshot_content, schema_version, created_at, updated_at, inherited_from_version_id, is_tombstone, change_id, commit_id)
								SELECT 
									entity_id, schema_key, file_id, ?, plugin_key, snapshot_content, schema_version, created_at, updated_at, 
									inherited_from_version_id,
									is_tombstone, change_id, commit_id
								FROM ${tableName}
								WHERE version_id = ?
						`,
                                    bind: [newVersionId, sourceVersionId],
                                });
                            }
                        }
                    }
                }
                return capi.SQLITE_OK;
            }
            catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                // Log error for debugging
                insertVTableLog({
                    engine: engine,
                    timestamp: getTimestampSync({
                        engine: engine,
                    }),
                    key: "lix_state_xupdate_error",
                    level: "error",
                    message: "xUpdate error",
                    payload: { message: errorMessage },
                });
                throw error; // Re-throw to propagate error
            }
        },
    }, false);
    function persistWriter(args) {
        if (args.writer && args.writer.length > 0) {
            // UPSERT writer
            engine.executeSync(internalQueryBuilder
                .insertInto("lix_internal_state_writer")
                .values({
                file_id: args.fileId,
                version_id: args.versionId,
                entity_id: args.entityId,
                schema_key: args.schemaKey,
                writer_key: args.writer,
            })
                .onConflict((oc) => oc
                .columns(["file_id", "version_id", "entity_id", "schema_key"])
                .doUpdateSet({ writer_key: args.writer }))
                .compile());
        }
        else {
            // DELETE writer row (no NULL storage)
            engine.executeSync(internalQueryBuilder
                .deleteFrom("lix_internal_state_writer")
                .where("file_id", "=", args.fileId)
                .where("version_id", "=", args.versionId)
                .where("entity_id", "=", args.entityId)
                .where("schema_key", "=", args.schemaKey)
                .compile());
        }
    }
    function resolveSchemaKey(args) {
        const [entity] = engine.executeSync(internalQueryBuilder
            .selectFrom("lix_internal_state_vtable")
            .select(["schema_key"])
            .where("file_id", "=", args.fileId)
            .where("entity_id", "=", args.entityId)
            .where("version_id", "=", args.versionId)
            .limit(1)
            .compile()).rows;
        return entity?.schema_key ?? "";
    }
    // Register the vtable under a clearer internal name
    capi.sqlite3_create_module(sqlite.pointer, "lix_internal_state_vtable", module, 0);
    // Create the internal vtable (raw state surface)
    sqlite.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS lix_internal_state_vtable USING lix_internal_state_vtable();`);
}
export function handleStateDelete(engine, primaryKey, timestamp) {
    // Look up the resolved row via the dynamic builder to avoid the legacy view
    const [rowToDelete] = engine.executeSync(internalQueryBuilder
        .selectFrom("lix_internal_state_vtable")
        .select([
        "entity_id",
        "schema_key",
        "file_id",
        "version_id",
        "plugin_key",
        "snapshot_content",
        "schema_version",
        "untracked",
        "inherited_from_version_id",
    ])
        .where("_pk", "=", primaryKey)
        .compile()).rows;
    if (!rowToDelete) {
        throw new Error(`Row not found for primary key: ${primaryKey}`);
    }
    const entity_id = rowToDelete.entity_id;
    const schema_key = rowToDelete.schema_key;
    const file_id = rowToDelete.file_id;
    const version_id = rowToDelete.version_id;
    const plugin_key = rowToDelete.plugin_key;
    const snapshot_content = rowToDelete.snapshot_content;
    const schema_version = rowToDelete.schema_version;
    const untracked = rowToDelete.untracked;
    if (rowToDelete.schema_key === "lix_file_descriptor") {
        deleteFilePathCacheEntry({
            engine,
            fileId: rowToDelete.entity_id,
            versionId: version_id,
        });
    }
    // If entity is untracked, handle differently based on its source (transaction/inherited/direct)
    if (untracked) {
        // Parse the primary key tag to determine where the row is coming from in the resolved view
        const parsed = parseStatePk(primaryKey);
        if (parsed.tag === "UI") {
            // Inherited untracked: create a tombstone to block inheritance
            insertTransactionState({
                engine: engine,
                timestamp,
                data: [
                    {
                        entity_id: String(entity_id),
                        schema_key: String(schema_key),
                        file_id: String(file_id),
                        plugin_key: String(plugin_key),
                        snapshot_content: null, // Deletion tombstone
                        schema_version: String(schema_version),
                        version_id: String(version_id),
                        untracked: true,
                    },
                ],
            });
            return;
        }
        if (parsed.tag === "T" || parsed.tag === "TI") {
            // The row is coming from the transaction stage (pending untracked insert/update).
            // Overwrite the pending transaction row with a deletion so the commit drops it
            // and nothing is persisted to the untracked table.
            insertTransactionState({
                engine: engine,
                timestamp,
                data: [
                    {
                        entity_id: String(entity_id),
                        schema_key: String(schema_key),
                        file_id: String(file_id),
                        plugin_key: String(plugin_key),
                        snapshot_content: null, // mark as delete in txn
                        schema_version: String(schema_version),
                        version_id: String(version_id),
                        untracked: true,
                    },
                ],
            });
            return;
        }
        // Direct untracked in this version (U tag) – delete from the untracked table immediately
        engine.executeSync(internalQueryBuilder
            .deleteFrom("lix_internal_state_all_untracked")
            .where("entity_id", "=", String(entity_id))
            .where("schema_key", "=", String(schema_key))
            .where("file_id", "=", String(file_id))
            .where("version_id", "=", String(version_id))
            .compile());
        return;
    }
    validateStateMutation({
        engine: engine,
        schema: String(schema_key) === LixStoredSchemaSchema["x-lix-key"]
            ? LixStoredSchemaSchema
            : null,
        schemaKey: String(schema_key),
        snapshot_content: JSON.parse(snapshot_content),
        operation: "delete",
        entity_id: String(entity_id),
        file_id: String(file_id),
        version_id: String(version_id),
        existingVersionsCache: withVersionCache(engine),
    });
    insertTransactionState({
        engine: engine,
        timestamp,
        data: [
            {
                entity_id: String(entity_id),
                schema_key: String(schema_key),
                file_id: String(file_id),
                plugin_key: String(plugin_key),
                snapshot_content: null, // No snapshot content for DELETE
                schema_version: String(schema_version),
                version_id: String(version_id),
                untracked: false, // tracked entity
            },
        ],
    });
}
function deleteFilePathCacheEntry(args) {
    args.engine.sqlite.exec({
        sql: `
			DELETE FROM lix_internal_file_path_cache
			WHERE file_id = ?
			  AND version_id = ?
		`,
        bind: [args.fileId, args.versionId],
        returnValue: "resultRows",
    });
}
function refreshFilePathCacheEntry(args) {
    const resolvedPath = resolveFileDescriptorPath({
        engine: args.engine,
        versionId: args.versionId,
        directoryId: args.directoryId,
        name: args.name,
        extension: args.extension,
    });
    if (!resolvedPath) {
        deleteFilePathCacheEntry({
            engine: args.engine,
            fileId: args.fileId,
            versionId: args.versionId,
        });
        return;
    }
    updateFilePathCache({
        engine: args.engine,
        fileId: args.fileId,
        versionId: args.versionId,
        directoryId: args.directoryId,
        name: args.name,
        extension: args.extension,
        path: resolvedPath,
    });
}
function normalizeFileDescriptorSnapshot(snapshot) {
    if (!snapshot || typeof snapshot !== "object") {
        return null;
    }
    const directoryIdRaw = snapshot.directory_id;
    const nameRaw = snapshot.name;
    const extensionRaw = snapshot.extension;
    if (typeof nameRaw !== "string" || nameRaw.length === 0) {
        return null;
    }
    const directoryId = typeof directoryIdRaw === "string" && directoryIdRaw.length > 0
        ? directoryIdRaw
        : null;
    const extension = typeof extensionRaw === "string" && extensionRaw.length > 0
        ? extensionRaw
        : null;
    return {
        directoryId,
        name: nameRaw,
        extension,
    };
}
function resolveFileDescriptorPath(args) {
    const directoryPath = args.directoryId
        ? (composeDirectoryPath({
            engine: args.engine,
            versionId: args.versionId,
            directoryId: args.directoryId,
        }) ?? undefined)
        : "/";
    if (!directoryPath) {
        return null;
    }
    const normalizedExtension = args.extension && args.extension.length > 0 ? args.extension : null;
    const suffix = normalizedExtension
        ? `${args.name}.${normalizedExtension}`
        : args.name;
    const basePath = directoryPath === "/" ? "/" : directoryPath;
    return `${basePath}${suffix}`;
}
function getColumnName(columnIndex) {
    return STATE_VTAB_COLUMN_NAMES[columnIndex] || "unknown";
}
function inferSourceTag(row) {
    if (!row) {
        return null;
    }
    if (typeof row.source_tag === "string") {
        return row.source_tag;
    }
    const pkValue = typeof row._pk === "string" ? row._pk : null;
    if (typeof pkValue === "string" && pkValue.length > 0) {
        const delimiterIndex = pkValue.indexOf("~");
        return delimiterIndex === -1 ? pkValue : pkValue.slice(0, delimiterIndex);
    }
    return null;
}
function withVersionCache(engine) {
    const result = withRuntimeCache(engine, VERSION_CACHE_QUERY);
    const rows = (result.rows ?? []);
    return new Set(rows.map((row) => String(row.id)));
}
//# sourceMappingURL=vtable.js.map