import crypto from "crypto";
import { mkdirSync, readFileSync } from "fs";
import fs from "fs/promises";
import path from "path";
import { clearSqliteCache, getSqliteDb } from "./sqlite_client.js";
import { config } from "../../config.js";
import { encryptColumn, decryptColumn, isEncryptedColumn } from "../../crypto/column_encryption.js";
import { deriveKeys, deriveKeysFromMnemonic, hexToKey } from "../../crypto/key_derivation.js";
/** Columns stored as 0/1 in SQLite that should be returned as boolean */
const BOOLEAN_COLUMNS = {
    schema_registry: new Set(["active", "test"]),
};
const JSON_COLUMNS = {
    sources: new Set(["provenance"]),
    interpretations: new Set(["interpretation_config", "provenance"]),
    observations: new Set(["fields", "provenance"]),
    timeline_events: new Set(["provenance"]),
    entity_snapshots: new Set(["snapshot", "provenance"]),
    entities: new Set(["aliases"]),
    relationship_observations: new Set(["metadata", "provenance"]),
    relationship_snapshots: new Set(["snapshot", "provenance"]),
    raw_fragments: new Set(["fragment_value", "fragment_envelope"]),
    schema_registry: new Set(["schema_definition", "reducer_config", "metadata"]),
    schema_recommendations: new Set([
        "fields_to_add",
        "fields_to_remove",
        "fields_to_modify",
        "converters_to_add",
    ]),
    auto_enhancement_queue: new Set(["payload"]),
};
/**
 * Columns that are encrypted at rest when NEOTOMA_ENCRYPTION_ENABLED=true.
 * Only content/payload columns; never IDs, timestamps, hashes, or signatures.
 * See plan Section 7 (blockchain compatibility).
 */
const ENCRYPTED_COLUMNS = {
    observations: new Set(["fields"]),
    entity_snapshots: new Set(["snapshot", "provenance"]),
    relationship_snapshots: new Set(["snapshot", "provenance"]),
    raw_fragments: new Set(["fragment_value", "fragment_envelope"]),
    schema_recommendations: new Set([
        "fields_to_add",
        "fields_to_remove",
        "fields_to_modify",
        "converters_to_add",
    ]),
    auto_enhancement_queue: new Set(["payload"]),
};
/** Cached data encryption key (lazy-loaded on first use) */
let cachedDataKey = null;
/**
 * Load the data encryption key from config (key file or mnemonic).
 * Returns null if encryption is not enabled.
 */
function getDataKey() {
    if (!config.encryption.enabled) {
        return null;
    }
    if (cachedDataKey) {
        return cachedDataKey;
    }
    // Priority: key file > mnemonic
    if (config.encryption.keyFilePath) {
        const raw = readFileSync(config.encryption.keyFilePath, "utf8").trim();
        const keyBytes = hexToKey(raw);
        const derived = deriveKeys(keyBytes);
        cachedDataKey = derived.dataKey;
        return cachedDataKey;
    }
    if (config.encryption.mnemonic) {
        const derived = deriveKeysFromMnemonic(config.encryption.mnemonic, config.encryption.mnemonicPassphrase);
        cachedDataKey = derived.dataKey;
        return cachedDataKey;
    }
    throw new Error("Encryption is enabled but no key source configured. " +
        "Set NEOTOMA_KEY_FILE_PATH or NEOTOMA_MNEMONIC.");
}
/**
 * Clear the cached data key. Call after key rotation or for testing.
 */
export function clearCachedDataKey() {
    cachedDataKey = null;
}
const TABLES_WITH_ID = new Set([
    "sources",
    "interpretations",
    "observations",
    "entities",
    "timeline_events",
    "relationship_observations",
    "raw_fragments",
    "entity_merges",
    "entity_splits",
    "schema_registry",
    "schema_recommendations",
    "auto_enhancement_queue",
    "field_blacklist",
    "source_entity_edges",
    "source_event_edges",
    "mcp_oauth_state",
    "mcp_oauth_connections",
    "mcp_oauth_client_state",
    "auth_users",
    "auth_sessions",
    "local_auth_users",
]);
const DETERMINISTIC_ID_TABLES = new Set([
    "sources",
    "observations",
    "entities",
    "timeline_events",
    "relationship_observations",
    "interpretations",
]);
/** SQLite3 only accepts numbers, strings, bigints, buffers, and null. Normalize for bind. */
function toBindValue(value) {
    if (value === undefined || value === null) {
        return null;
    }
    if (typeof value === "boolean") {
        return value ? 1 : 0;
    }
    return value;
}
function toDbValue(table, column, value) {
    if (value === undefined) {
        return null;
    }
    if (typeof value === "boolean") {
        return value ? 1 : 0;
    }
    if (JSON_COLUMNS[table]?.has(column)) {
        const jsonStr = value === null ? null : JSON.stringify(value);
        // Encrypt if this column is in the encrypted set and encryption is enabled
        if (jsonStr !== null && ENCRYPTED_COLUMNS[table]?.has(column)) {
            const key = getDataKey();
            if (key) {
                return encryptColumn(jsonStr, key);
            }
        }
        return jsonStr;
    }
    return value;
}
/** Map SQLite constraint errors to PostgreSQL error codes for compatibility */
function mapSqliteErrorToPostgres(error) {
    const code = error.code || "";
    const msg = (error.message || "").toLowerCase();
    const errno = error.errno;
    if (errno === 2067 || code === "SQLITE_CONSTRAINT_UNIQUE" || msg.includes("unique constraint")) {
        return "23505";
    }
    if (errno === 787 || code === "SQLITE_CONSTRAINT_FOREIGNKEY" || msg.includes("foreign key")) {
        return "23503";
    }
    if (code === "SQLITE_CONSTRAINT" && msg.includes("unique"))
        return "23505";
    if (code === "SQLITE_CONSTRAINT" && msg.includes("foreign key"))
        return "23503";
    return undefined;
}
export function isRecoverableSqliteConnectionError(error) {
    if (!error || typeof error !== "object")
        return false;
    const e = error;
    const code = (e.code || "").toUpperCase();
    const message = (e.message || "").toLowerCase();
    return (code.startsWith("SQLITE_IOERR") ||
        code === "SQLITE_CORRUPT" ||
        message.includes("disk i/o error") ||
        message.includes("database disk image is malformed") ||
        message.includes("btreeinitpage"));
}
/**
 * Returns true for transient lock/busy errors that warrant a retry with backoff.
 * better-sqlite3 surfaces these as SQLITE_BUSY or SQLITE_LOCKED error codes, or
 * via error messages that include "database is locked" / "database table is locked".
 */
export function isSqliteLockError(error) {
    if (!error || typeof error !== "object")
        return false;
    const e = error;
    const code = (e.code || "").toUpperCase();
    const message = (e.message || "").toLowerCase();
    return (code === "SQLITE_BUSY" ||
        code === "SQLITE_LOCKED" ||
        code.startsWith("SQLITE_BUSY_") ||
        code.startsWith("SQLITE_LOCKED_") ||
        message.includes("database is locked") ||
        message.includes("database table is locked"));
}
/** Maximum number of lock-retry attempts before giving up. */
const SQLITE_LOCK_MAX_RETRIES = 5;
/** Base delay in ms for exponential backoff on lock errors. */
const SQLITE_LOCK_BASE_DELAY_MS = 50;
/**
 * Awaitable sleep helper. Using a promise-based sleep avoids blocking the
 * Node.js event loop while still keeping the retry logic straightforward.
 */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function fromDbRow(table, row) {
    const result = { ...row };
    const booleanColumns = BOOLEAN_COLUMNS[table];
    if (booleanColumns) {
        for (const column of booleanColumns) {
            if (result[column] === 1 || result[column] === "1") {
                result[column] = true;
            }
            else if (result[column] === 0 || result[column] === "0") {
                result[column] = false;
            }
        }
    }
    const jsonColumns = JSON_COLUMNS[table];
    if (!jsonColumns) {
        return result;
    }
    for (const column of jsonColumns) {
        if (result[column] === null || result[column] === undefined) {
            continue;
        }
        if (typeof result[column] === "string") {
            let raw = result[column];
            // Decrypt if this column is in the encrypted set and value looks encrypted
            if (ENCRYPTED_COLUMNS[table]?.has(column) && isEncryptedColumn(raw)) {
                const key = getDataKey();
                if (key) {
                    try {
                        raw = decryptColumn(raw, key);
                    }
                    catch {
                        // If decryption fails, leave as-is (wrong key or plaintext data from before encryption)
                    }
                }
            }
            try {
                result[column] = JSON.parse(raw);
            }
            catch {
                // Leave as string if parse fails
                result[column] = raw;
            }
        }
    }
    // Compatibility aliases (match Postgres naming used in tests).
    if (table === "entities") {
        if (!("merged_into" in result) && "merged_to_entity_id" in result) {
            result["merged_into"] = result["merged_to_entity_id"];
        }
    }
    return result;
}
function normalizeColumnName(table, column) {
    // Compatibility aliases used by tests and older codepaths.
    if (table === "entities" && column === "merged_into")
        return "merged_to_entity_id";
    if (table === "observations" && column === "priority")
        return "source_priority";
    // PostgREST-style JSON path projection used in query code:
    //   snapshot->>published_date
    // Convert to SQLite-compatible json_extract(snapshot, '$.published_date').
    const jsonPathMatch = column.match(/^([A-Za-z_][A-Za-z0-9_]*)->>([A-Za-z_][A-Za-z0-9_]*)$/);
    if (jsonPathMatch) {
        const [, jsonColumn, jsonField] = jsonPathMatch;
        const jsonPath = `$.${jsonField}`;
        const extracted = `json_extract(${jsonColumn}, '${jsonPath}')`;
        // Emulate PostgREST text projection semantics for ->>:
        // booleans become "true"/"false"; scalars become text.
        return `CASE json_type(${jsonColumn}, '${jsonPath}') WHEN 'true' THEN 'true' WHEN 'false' THEN 'false' ELSE CAST(${extracted} AS TEXT) END`;
    }
    return column;
}
function deriveCanonicalName(value) {
    if (typeof value === "string" && value.trim().length > 0)
        return value.trim();
    return null;
}
function deriveCanonicalNameFromObject(value) {
    if (!value || typeof value !== "object")
        return null;
    const v = value;
    return (deriveCanonicalName(v["canonical_name"]) ??
        deriveCanonicalName(v["canonicalName"]) ??
        deriveCanonicalName(v["title"]) ??
        deriveCanonicalName(v["name"]) ??
        null);
}
async function ensureEntityRowForObservation(db, obs) {
    const entityId = typeof obs["entity_id"] === "string" ? obs["entity_id"] : null;
    const entityType = typeof obs["entity_type"] === "string" ? obs["entity_type"] : null;
    if (!entityId || !entityType)
        return;
    const existing = db.prepare("SELECT id FROM entities WHERE id = ?").get(entityId);
    if (existing)
        return;
    const fields = obs["fields"];
    const canonicalName = deriveCanonicalNameFromObject(fields) ??
        // last resort so INSERT doesn't violate NOT NULL
        entityId;
    const now = new Date().toISOString();
    const userId = typeof obs["user_id"] === "string" || obs["user_id"] === null ? obs["user_id"] : null;
    db.prepare("INSERT INTO entities (id, entity_type, canonical_name, created_at, updated_at, user_id) VALUES (?, ?, ?, ?, ?, ?)").run(entityId, entityType, canonicalName, now, now, userId);
}
async function ensureEntityRowForSnapshot(db, snapshotRow) {
    const entityId = typeof snapshotRow["entity_id"] === "string" ? snapshotRow["entity_id"] : null;
    const entityType = typeof snapshotRow["entity_type"] === "string" ? snapshotRow["entity_type"] : null;
    if (!entityId || !entityType)
        return;
    const existing = db.prepare("SELECT id FROM entities WHERE id = ?").get(entityId);
    if (existing)
        return;
    const canonicalName = deriveCanonicalName(snapshotRow["canonical_name"]) ??
        deriveCanonicalNameFromObject(snapshotRow["snapshot"]) ??
        entityId;
    const now = new Date().toISOString();
    const userId = typeof snapshotRow["user_id"] === "string" || snapshotRow["user_id"] === null
        ? snapshotRow["user_id"]
        : null;
    db.prepare("INSERT INTO entities (id, entity_type, canonical_name, created_at, updated_at, user_id) VALUES (?, ?, ?, ?, ?, ?)").run(entityId, entityType, canonicalName, now, now, userId);
}
async function recomputeEntitySnapshot(db, entityId) {
    const { ObservationReducer } = await import("../../reducers/observation_reducer.js");
    const reducer = new ObservationReducer();
    const rows = db.prepare("SELECT * FROM observations WHERE entity_id = ?").all(entityId);
    const observations = rows.map((r) => fromDbRow("observations", r));
    if (observations.length === 0) {
        db.prepare("DELETE FROM entity_snapshots WHERE entity_id = ?").run(entityId);
        return;
    }
    const snapshot = await reducer.computeSnapshot(entityId, observations);
    if (!snapshot) {
        db.prepare("DELETE FROM entity_snapshots WHERE entity_id = ?").run(entityId);
        return;
    }
    const canonicalName = deriveCanonicalNameFromObject(snapshot.snapshot);
    const payload = {
        ...snapshot,
        canonical_name: canonicalName,
    };
    const columns = Object.keys(payload);
    const values = columns.map((column) => toDbValue("entity_snapshots", column, payload[column]));
    const placeholders = columns.map(() => "?").join(", ");
    db.prepare(`INSERT OR REPLACE INTO entity_snapshots (${columns.join(", ")}) VALUES (${placeholders})`).run(values);
}
async function recomputeRelationshipSnapshot(db, relationshipKey) {
    const { RelationshipReducer } = await import("../../reducers/relationship_reducer.js");
    const reducer = new RelationshipReducer();
    const rows = db
        .prepare("SELECT * FROM relationship_observations WHERE relationship_key = ?")
        .all(relationshipKey);
    const observations = rows.map((r) => fromDbRow("relationship_observations", r));
    if (observations.length === 0) {
        db.prepare("DELETE FROM relationship_snapshots WHERE relationship_key = ?").run(relationshipKey);
        return;
    }
    const snapshot = await reducer.computeSnapshot(relationshipKey, observations);
    const payload = { ...snapshot };
    const columns = Object.keys(payload);
    const values = columns.map((column) => toDbValue("relationship_snapshots", column, payload[column]));
    const placeholders = columns.map(() => "?").join(", ");
    db.prepare(`INSERT OR REPLACE INTO relationship_snapshots (${columns.join(", ")}) VALUES (${placeholders})`).run(values);
}
class LocalQueryBuilder {
    table;
    operation = "select";
    selectColumns = "*";
    filters = [];
    filterValues = [];
    orderBy = [];
    limitValue = null;
    offsetValue = null;
    insertPayload = null;
    updatePayload = null;
    upsertPayload = null;
    countExact = false;
    countHead = false;
    expectSingle = false;
    allowNullSingle = false;
    orConditions = [];
    constructor(table) {
        this.table = table;
    }
    select(columns, options) {
        this.selectColumns = columns || "*";
        this.countExact = options?.count === "exact";
        this.countHead = options?.head === true;
        if (this.operation !== "insert" && this.operation !== "upsert") {
            this.operation = "select";
        }
        return this;
    }
    insert(payload) {
        this.operation = "insert";
        this.insertPayload = Array.isArray(payload) ? payload : [payload];
        return this;
    }
    update(payload) {
        this.operation = "update";
        this.updatePayload = payload;
        return this;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    upsert(payload, _options) {
        this.operation = "upsert";
        this.upsertPayload = Array.isArray(payload) ? payload : [payload];
        return this;
    }
    delete() {
        this.operation = "delete";
        return this;
    }
    eq(column, value) {
        const col = normalizeColumnName(this.table, column);
        if (value === null) {
            this.filters.push(`${col} IS NULL`);
        }
        else {
            this.filters.push(`${col} = ?`);
            this.filterValues.push(value);
        }
        return this;
    }
    /** Postgrest-compatible NULL check: .is(column, null) => column IS NULL */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    is(column, value) {
        const col = normalizeColumnName(this.table, column);
        this.filters.push(`${col} IS NULL`);
        return this;
    }
    in(column, values) {
        const col = normalizeColumnName(this.table, column);
        if (values.length === 0) {
            this.filters.push("1 = 0");
            return this;
        }
        const placeholders = values.map(() => "?").join(", ");
        this.filters.push(`${col} IN (${placeholders})`);
        this.filterValues.push(...values);
        return this;
    }
    not(column, operator, value) {
        const col = normalizeColumnName(this.table, column);
        if (operator === "is" && value === null) {
            this.filters.push(`${col} IS NOT NULL`);
            return this;
        }
        this.filters.push(`${col} != ?`);
        this.filterValues.push(value);
        return this;
    }
    ilike(column, pattern) {
        const col = normalizeColumnName(this.table, column);
        this.filters.push(`${col} LIKE ? COLLATE NOCASE`);
        this.filterValues.push(pattern);
        return this;
    }
    gte(column, value) {
        const col = normalizeColumnName(this.table, column);
        this.filters.push(`${col} >= ?`);
        this.filterValues.push(value);
        return this;
    }
    gt(column, value) {
        const col = normalizeColumnName(this.table, column);
        this.filters.push(`${col} > ?`);
        this.filterValues.push(value);
        return this;
    }
    lte(column, value) {
        const col = normalizeColumnName(this.table, column);
        this.filters.push(`${col} <= ?`);
        this.filterValues.push(value);
        return this;
    }
    lt(column, value) {
        const col = normalizeColumnName(this.table, column);
        this.filters.push(`${col} < ?`);
        this.filterValues.push(value);
        return this;
    }
    order(column, options) {
        const col = normalizeColumnName(this.table, column);
        this.orderBy.push({ column: col, ascending: options?.ascending !== false });
        return this;
    }
    range(from, to) {
        this.offsetValue = from;
        this.limitValue = Math.max(0, to - from + 1);
        return this;
    }
    limit(limit) {
        this.limitValue = limit;
        return this;
    }
    single() {
        this.expectSingle = true;
        this.allowNullSingle = false;
        return this;
    }
    maybeSingle() {
        this.expectSingle = true;
        this.allowNullSingle = true;
        return this;
    }
    or(conditions) {
        const parts = conditions
            .split(",")
            .map((p) => p.trim())
            .filter(Boolean);
        const clauses = [];
        const values = [];
        // SECURITY: allowlist column identifiers; values are always bound as `?`.
        // Without this check, any caller that interpolates user-controlled strings
        // into the `or(...)` clause (e.g. `mime_type.ilike.${userNeedle}`) could
        // inject arbitrary SQL into the identifier position. See docs/reports/
        // security_audit_2026_04_22.md S-3.
        const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*(->>[A-Za-z_][A-Za-z0-9_]*)?$/;
        for (const part of parts) {
            const dotIdx = part.indexOf(".");
            if (dotIdx < 0)
                continue;
            const rawLeft = part.slice(0, dotIdx);
            const rest = part.slice(dotIdx + 1);
            const dotIdx2 = rest.indexOf(".");
            const op = dotIdx2 >= 0 ? rest.slice(0, dotIdx2) : rest;
            const value = dotIdx2 >= 0 ? rest.slice(dotIdx2 + 1) : "";
            if (!rawLeft || !op)
                continue;
            if (!IDENT_RE.test(rawLeft))
                continue;
            const left = normalizeColumnName(this.table, rawLeft);
            if (op === "eq") {
                clauses.push(`${left} = ?`);
                values.push(value);
            }
            else if (op === "is" && value === "null") {
                clauses.push(`${left} IS NULL`);
            }
            else if (op === "ilike") {
                clauses.push(`${left} LIKE ? COLLATE NOCASE`);
                values.push(value);
            }
            else if (op === "like") {
                clauses.push(`${left} LIKE ?`);
                values.push(value);
            }
            else if (op === "neq") {
                clauses.push(`${left} != ?`);
                values.push(value);
            }
            else if (op === "cs") {
                // PostgREST cs (contains) - for JSON arrays, do a LIKE check on the TEXT column
                clauses.push(`${left} LIKE ?`);
                values.push(`%${value.replace(/^\["|"\]$/, "").replace(/^"|"$/g, "")}%`);
            }
        }
        if (clauses.length > 0) {
            this.orConditions.push({ clause: `(${clauses.join(" OR ")})`, values });
        }
        return this;
    }
    async execute() {
        const whereClauses = [];
        const values = [];
        if (this.filters.length > 0) {
            whereClauses.push(...this.filters);
            values.push(...this.filterValues.map(toBindValue));
        }
        if (this.orConditions.length > 0) {
            const orClause = this.orConditions.map((o) => o.clause).join(" AND ");
            whereClauses.push(orClause);
            for (const condition of this.orConditions) {
                values.push(...condition.values.map(toBindValue));
            }
        }
        const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
        // Capture the last SQLite lock error so a retry-exhausted envelope can
        // preserve its `code`/`errno` instead of returning a string-only message.
        let lastLockError = null;
        for (let lockRetry = 0; lockRetry < SQLITE_LOCK_MAX_RETRIES; lockRetry += 1) {
            for (let attempt = 0; attempt < 2; attempt += 1) {
                const db = getSqliteDb();
                try {
                    if (this.operation === "select") {
                        const countRow = this.countExact
                            ? db
                                .prepare(`SELECT COUNT(*) as count FROM ${this.table} ${whereSql}`)
                                .get(values)
                            : undefined;
                        const count = countRow?.count;
                        if (this.countHead) {
                            return { data: null, error: null, count };
                        }
                        const orderSql = this.orderBy.length > 0
                            ? `ORDER BY ${this.orderBy
                                .map((o) => `${o.column} ${o.ascending ? "ASC" : "DESC"}`)
                                .join(", ")}`
                            : "";
                        const limitSql = this.limitValue !== null ? `LIMIT ${this.limitValue}` : "";
                        const offsetSql = this.offsetValue !== null ? `OFFSET ${this.offsetValue}` : "";
                        const sql = `SELECT ${this.selectColumns || "*"} FROM ${this.table} ${whereSql} ${orderSql} ${limitSql} ${offsetSql}`.trim();
                        const rows = db
                            .prepare(sql)
                            .all(values)
                            .map((row) => fromDbRow(this.table, row));
                        if (this.expectSingle) {
                            const row = rows[0] || null;
                            if (!row && !this.allowNullSingle) {
                                const detail = whereSql || "no filter applied";
                                return {
                                    data: null,
                                    error: {
                                        message: `SELECT on table '${this.table}' returned no rows (expected exactly one). Query had: ${detail}.`,
                                        code: "PGRST116",
                                    },
                                };
                            }
                            return { data: row, error: null, count };
                        }
                        return { data: rows, error: null, count };
                    }
                    if (this.operation === "insert" && this.insertPayload) {
                        const inserted = [];
                        for (const item of this.insertPayload) {
                            const payload = { ...item };
                            // Compatibility: observations.priority -> observations.source_priority
                            if (this.table === "observations" &&
                                "priority" in payload &&
                                !("source_priority" in payload)) {
                                payload["source_priority"] = payload["priority"];
                                delete payload["priority"];
                            }
                            // Parity with Postgres: supply required observation columns when omitted by tests/helpers.
                            if (this.table === "observations") {
                                // Compatibility: some tests use observations.id as entity identifier.
                                if (!("entity_id" in payload) &&
                                    typeof payload["id"] === "string" &&
                                    /^ent_/i.test(payload["id"])) {
                                    payload["entity_id"] = payload["id"];
                                }
                                if (!("schema_version" in payload))
                                    payload["schema_version"] = "1.0";
                                if (!("observed_at" in payload))
                                    payload["observed_at"] = new Date().toISOString();
                            }
                            // Parity with Postgres: derive relationship_key when omitted.
                            if (this.table === "relationship_observations") {
                                const hasKey = typeof payload["relationship_key"] === "string" && payload["relationship_key"];
                                if (!hasKey) {
                                    const rt = typeof payload["relationship_type"] === "string"
                                        ? payload["relationship_type"]
                                        : "";
                                    const se = typeof payload["source_entity_id"] === "string"
                                        ? payload["source_entity_id"]
                                        : "";
                                    const te = typeof payload["target_entity_id"] === "string"
                                        ? payload["target_entity_id"]
                                        : "";
                                    if (rt && se && te) {
                                        payload["relationship_key"] = `${rt}:${se}:${te}`;
                                    }
                                }
                                if (!("observed_at" in payload))
                                    payload["observed_at"] = new Date().toISOString();
                                if (!("source_priority" in payload))
                                    payload["source_priority"] = 0;
                                if (!("metadata" in payload))
                                    payload["metadata"] = {};
                            }
                            // Parity: entity_snapshots.canonical_name column for filtering.
                            if (this.table === "entity_snapshots" && !("canonical_name" in payload)) {
                                const derived = deriveCanonicalNameFromObject(payload["snapshot"]);
                                if (derived)
                                    payload["canonical_name"] = derived;
                            }
                            if (TABLES_WITH_ID.has(this.table) && !payload.id) {
                                if (DETERMINISTIC_ID_TABLES.has(this.table)) {
                                    console.warn(`[DETERMINISM] Missing ID for ${this.table} insert; falling back to randomUUID. Callers should provide deterministic IDs for domain tables.`);
                                }
                                payload.id = crypto.randomUUID();
                            }
                            // Only add created_at for tables that have this column
                            // entity_snapshots and relationship_snapshots don't have created_at
                            const TABLES_WITHOUT_CREATED_AT = new Set([
                                "entity_snapshots",
                                "relationship_snapshots",
                            ]);
                            if ("created_at" in payload === false && !TABLES_WITHOUT_CREATED_AT.has(this.table)) {
                                payload.created_at = new Date().toISOString();
                            }
                            const columns = Object.keys(payload);
                            const values = columns.map((column) => toDbValue(this.table, column, payload[column]));
                            const placeholders = columns.map(() => "?").join(", ");
                            db.prepare(`INSERT INTO ${this.table} (${columns.join(", ")}) VALUES (${placeholders})`).run(values);
                            inserted.push(payload);
                            // Local backend parity: inserts can materialize related state (entities + snapshots).
                            if (this.table === "observations") {
                                await ensureEntityRowForObservation(db, payload);
                                if (typeof payload["entity_id"] === "string") {
                                    await recomputeEntitySnapshot(db, payload["entity_id"]);
                                }
                            }
                            if (this.table === "entity_snapshots") {
                                await ensureEntityRowForSnapshot(db, payload);
                            }
                            if (this.table === "relationship_observations") {
                                const rk = payload["relationship_key"];
                                if (typeof rk === "string" && rk) {
                                    await recomputeRelationshipSnapshot(db, rk);
                                }
                            }
                        }
                        if (this.selectColumns) {
                            return { data: this.expectSingle ? inserted[0] : inserted, error: null };
                        }
                        return { data: null, error: null };
                    }
                    if (this.operation === "update" && this.updatePayload) {
                        const payload = { ...this.updatePayload };
                        const columns = Object.keys(payload);
                        const updateValues = columns.map((column) => toDbValue(this.table, column, payload[column]));
                        const updateSql = columns.map((column) => `${column} = ?`).join(", ");
                        db.prepare(`UPDATE ${this.table} SET ${updateSql} ${whereSql}`).run([
                            ...updateValues,
                            ...values,
                        ]);
                        if (this.selectColumns) {
                            const rows = db
                                .prepare(`SELECT ${this.selectColumns} FROM ${this.table} ${whereSql}`)
                                .all(values)
                                .map((row) => fromDbRow(this.table, row));
                            if (this.expectSingle) {
                                const row = rows[0] || null;
                                if (!row && !this.allowNullSingle) {
                                    const detail = whereSql || "no filter applied";
                                    return {
                                        data: null,
                                        error: {
                                            message: `UPDATE on table '${this.table}' then SELECT returned no rows (expected exactly one). Query had: ${detail}.`,
                                            code: "PGRST116",
                                        },
                                    };
                                }
                                return { data: row, error: null };
                            }
                            return { data: rows, error: null };
                        }
                        return { data: null, error: null };
                    }
                    if (this.operation === "upsert" && this.upsertPayload) {
                        const inserted = [];
                        for (const item of this.upsertPayload) {
                            const payload = { ...item };
                            if (this.table === "entity_snapshots" && !("canonical_name" in payload)) {
                                const derived = deriveCanonicalNameFromObject(payload["snapshot"]);
                                if (derived)
                                    payload["canonical_name"] = derived;
                            }
                            if (TABLES_WITH_ID.has(this.table) && !payload.id) {
                                if (DETERMINISTIC_ID_TABLES.has(this.table)) {
                                    console.warn(`[DETERMINISM] Missing ID for ${this.table} upsert; falling back to randomUUID. Callers should provide deterministic IDs for domain tables.`);
                                }
                                payload.id = crypto.randomUUID();
                            }
                            const columns = Object.keys(payload);
                            const values = columns.map((column) => toDbValue(this.table, column, payload[column]));
                            const placeholders = columns.map(() => "?").join(", ");
                            db.prepare(`INSERT OR REPLACE INTO ${this.table} (${columns.join(", ")}) VALUES (${placeholders})`).run(values);
                            inserted.push(payload);
                            if (this.table === "entity_snapshots") {
                                await ensureEntityRowForSnapshot(db, payload);
                            }
                        }
                        if (this.selectColumns) {
                            return { data: this.expectSingle ? inserted[0] : inserted, error: null };
                        }
                        return { data: null, error: null };
                    }
                    if (this.operation === "delete") {
                        db.prepare(`DELETE FROM ${this.table} ${whereSql}`).run(values);
                        return { data: null, error: null };
                    }
                    return { data: null, error: { message: "Unsupported operation" } };
                }
                catch (error) {
                    if (attempt === 0 && isRecoverableSqliteConnectionError(error)) {
                        clearSqliteCache();
                        continue;
                    }
                    if (isSqliteLockError(error)) {
                        // Capture for the retry-exhausted envelope, then break out of the
                        // connection-retry inner loop; the outer lock-retry loop will sleep
                        // and try again.
                        lastLockError = {
                            code: error?.code,
                            errno: error?.errno,
                            message: error?.message || String(error),
                        };
                        break;
                    }
                    const msg = error.message || String(error);
                    const code = mapSqliteErrorToPostgres(error);
                    return { data: null, error: { message: msg, ...(code ? { code } : {}) } };
                }
            }
            // If we reach here inside the lock-retry loop, a lock error occurred.
            // Sleep with exponential backoff before the next attempt.
            if (lockRetry < SQLITE_LOCK_MAX_RETRIES - 1) {
                const delay = SQLITE_LOCK_BASE_DELAY_MS * Math.pow(2, lockRetry);
                await sleep(delay);
            }
        } // end lock-retry loop
        // Retry exhausted: surface a DB_CONNECTION_FAILED envelope so callers can
        // distinguish lock-exhaustion from arbitrary adapter failures and so the
        // originating SQLite `code`/`errno` is preserved for logging. Per
        // `docs/reference/error_codes.md`, this is HTTP 503 / Retry? Yes.
        return {
            data: null,
            error: {
                message: lastLockError?.message
                    ? `SQLite database is locked: maximum retries exceeded (${lastLockError.message})`
                    : "SQLite database is locked: maximum retries exceeded",
                code: "DB_CONNECTION_FAILED",
                ...(lastLockError?.code ? { sqlite_code: lastLockError.code } : {}),
                ...(lastLockError?.errno !== undefined ? { sqlite_errno: lastLockError.errno } : {}),
            },
        };
    }
    then(onfulfilled, onrejected) {
        return this.execute().then(onfulfilled, onrejected);
    }
}
class LocalStorageBucket {
    bucket;
    constructor(bucket) {
        this.bucket = bucket;
    }
    resolvePath(objectPath) {
        const root = this.bucket === "sources"
            ? path.resolve(config.rawStorageDir)
            : path.resolve(config.dataDir, "storage", this.bucket);
        // Reject absolute paths, URL schemes, and null bytes outright so a later
        // `path.resolve` cannot escape the storage root.
        if (typeof objectPath !== "string" ||
            objectPath.length === 0 ||
            objectPath.includes("\0") ||
            /^[a-z][a-z0-9+.-]*:\/\//i.test(objectPath) ||
            path.isAbsolute(objectPath)) {
            throw new Error(`STORAGE_PATH_REJECTED: ${objectPath}`);
        }
        const resolved = path.resolve(root, objectPath);
        const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
        if (resolved !== root && !resolved.startsWith(rootWithSep)) {
            throw new Error(`STORAGE_PATH_TRAVERSAL_BLOCKED: ${objectPath}`);
        }
        return resolved;
    }
    async upload(objectPath, data, options) {
        const targetPath = this.resolvePath(objectPath);
        const dir = path.dirname(targetPath);
        mkdirSync(dir, { recursive: true });
        try {
            if (!options?.upsert) {
                try {
                    await fs.access(targetPath);
                    return {
                        data: null,
                        error: { message: "File already exists", code: "STORAGE_FILE_EXISTS" },
                    };
                }
                catch {
                    // continue
                }
            }
            await fs.writeFile(targetPath, data);
            return { data: null, error: null };
        }
        catch (error) {
            return { data: null, error: { message: error.message || String(error) } };
        }
    }
    async download(objectPath) {
        const targetPath = this.resolvePath(objectPath);
        try {
            const file = await fs.readFile(targetPath);
            return {
                data: {
                    arrayBuffer: async () => file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength),
                },
                error: null,
            };
        }
        catch (error) {
            return { data: null, error: { message: error.message || String(error) } };
        }
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async createSignedUrl(objectPath, _expiresIn) {
        const targetPath = this.resolvePath(objectPath);
        const signedUrl = `file://${targetPath}`;
        return { data: { signedUrl }, error: null };
    }
}
class LocalStorageClient {
    from(bucket) {
        return new LocalStorageBucket(bucket);
    }
}
class LocalAuthAdmin {
    async getUserById(userId) {
        const db = getSqliteDb();
        const user = db.prepare("SELECT id, email FROM auth_users WHERE id = ?").get(userId);
        if (!user) {
            return { data: null, error: { message: "User not found", code: "PGRST116" } };
        }
        return { data: { user }, error: null };
    }
    async createUser(payload) {
        const db = getSqliteDb();
        const id = payload.id || crypto.randomUUID();
        const email = payload.email || null;
        db.prepare("INSERT INTO auth_users (id, email, created_at) VALUES (?, ?, ?)").run(id, email, new Date().toISOString());
        return { data: { user: { id, email } }, error: null };
    }
    async listUsers() {
        const db = getSqliteDb();
        const users = db.prepare("SELECT id, email FROM auth_users").all();
        return { data: { users: users }, error: null };
    }
    async generateLink(payload) {
        const db = getSqliteDb();
        const user = db
            .prepare("SELECT id, email FROM auth_users WHERE email = ?")
            .get(payload.email);
        if (!user) {
            return { data: null, error: { message: "User not found", code: "PGRST116" } };
        }
        const accessToken = `local_${crypto.randomUUID()}`;
        const refreshToken = `local_refresh_${crypto.randomUUID()}`;
        const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
        const sessionId = crypto.randomUUID();
        db.prepare("INSERT INTO auth_sessions (id, user_id, access_token, refresh_token, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, user.id, accessToken, refreshToken, expiresAt, new Date().toISOString());
        const redirectTo = payload.options?.redirectTo || "http://localhost:5173";
        const actionLink = `${redirectTo}#access_token=${accessToken}&refresh_token=${refreshToken}`;
        return { data: { properties: { action_link: actionLink } }, error: null };
    }
}
class LocalAuthClient {
    admin = new LocalAuthAdmin();
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async refreshSession(_opts) {
        return {
            data: { session: null },
            error: { message: "Token refresh not supported in local backend", code: "AUTH_LOCAL" },
        };
    }
    async getUser(token) {
        const db = getSqliteDb();
        const session = db
            .prepare("SELECT user_id, access_token, expires_at FROM auth_sessions WHERE access_token = ?")
            .get(token);
        if (!session) {
            return { data: null, error: { message: "Invalid token", code: "AUTH_INVALID" } };
        }
        if (session.expires_at && new Date(session.expires_at).getTime() < Date.now()) {
            return { data: null, error: { message: "Token expired", code: "AUTH_EXPIRED" } };
        }
        const user = db
            .prepare("SELECT id, email FROM auth_users WHERE id = ?")
            .get(session.user_id);
        if (!user) {
            return { data: null, error: { message: "User not found", code: "PGRST116" } };
        }
        return { data: { user: user }, error: null };
    }
}
export class LocalDbClient {
    storage = new LocalStorageClient();
    auth = new LocalAuthClient();
    from(table) {
        return new LocalQueryBuilder(table);
    }
}
export function createLocalDbClient() {
    return new LocalDbClient();
}
//# sourceMappingURL=local_db_adapter.js.map