// createMemoryVault — the integration layer. Composes path resolution, DB lifecycle, validator
// gate, boundary redaction, prepared-SQL adapters, and the optional onMemoryEvent callback into
// a single MemoryVaultStore port. Every public method follows the same order:
//
//   1. validate (throws MemoryStorageValidationError before any SQL touches)
//   2. redact at the boundary (applies the factory redactString to free-text fields)
//   3. execute the prepared SQL (transactional when the method writes more than one row)
//   4. fire onMemoryEvent (AFTER commit, never on rollback)
//
// Steps 1-3 are pure-functional in TS-space until the SQL prepare() runs, so a thrown validator
// or redact exception leaves the database untouched. Step 4 sees only the post-commit truth.
import { DatabaseSync } from "node:sqlite";
import { randomUUID } from "node:crypto";
import { openMemoryDatabase } from "./db.js";
import { resolveMemoryDir, resolveMemoryDbPath } from "./paths.js";
import { createMemoryContentCipher, resolveVaultKey } from "./cipher.js";
import { scopeCoordinateOf, scopeKindOf } from "./scope-key.js";
import { deleteMemoryRow, getMemoryRow, insertMemoryRow, listMemoriesRows, listMemoriesByScopeRows, updateMemoryRow, } from "./memories.js";
import { deleteEdgeRow, insertEdgeRow, listIncomingEdgeRows, listOutgoingEdgeRows, } from "./edges.js";
import { getEmbeddingRow, upsertEmbeddingRow } from "./embeddings.js";
import { getAccessStatsRows, recordAccessRows } from "./access.js";
import { insertTombstoneRow, listTombstonesByScopeRows } from "./tombstones.js";
import { gateDeleteOptions, gateEmbeddingInput, gateMemoryEdge, gateMemoryRecord, gateMemoryScope, } from "./validate.js";
import { redactMemoryEdge, redactMemoryRecord, redactTombstone } from "./redact-record.js";
import { MemoryStorageError } from "./errors.js";
const IDENTITY = (s) => s;
const NOOP_EMIT = () => undefined;
// Single named entry point for the default environment read so the literal `process.env`
// reference does not appear inline in business logic (audit AC19).
function defaultEnv() {
    return process.env;
}
function resolveOptions(opts, cipher) {
    return {
        now: opts?.now ?? (() => Date.now()),
        newTombstoneId: opts?.newTombstoneId ?? (() => randomUUID()),
        redactString: opts?.redactString ?? IDENTITY,
        emit: opts?.onMemoryEvent ?? NOOP_EMIT,
        cipher,
    };
}
// Resolve the content cipher. Precedence: an explicitly injected cipher (tests), then an injected
// raw key (tests/CI), then the real tiered resolver (KEIKO_MEMORY_KEY > keychain > keyfile). The
// public factory never requires any of these — production callers get the tiered resolver.
function resolveCipher(opts, env) {
    if (opts?.cipher !== undefined)
        return opts.cipher;
    if (opts?.vaultKey !== undefined)
        return createMemoryContentCipher(opts.vaultKey);
    const memoryDir = resolveMemoryDir(opts?.memoryDir, env);
    return createMemoryContentCipher(resolveVaultKey(env, memoryDir).key);
}
// Validate-then-redact for inserts. The validator runs on the CALLER-SUPPLIED record so a
// validation failure carries the caller's exact failure list. Redaction runs SECOND so the SQL
// row is the redacted form — a secret-shaped string that slipped past the capture-layer gate
// in #207 is scrubbed before persistence.
function preparedForWrite(record, opts) {
    const validated = gateMemoryRecord(record);
    return redactMemoryRecord(validated, opts.redactString);
}
function preparedEdgeForWrite(edge, opts) {
    const validated = gateMemoryEdge(edge);
    return redactMemoryEdge(validated, opts.redactString);
}
function mergePatch(existing, patch, nowMs) {
    // updatedAt is owned by the vault, not the patch, so the caller cannot regress it. createdAt
    // and the scope coordinate are immutable on update (scope changes require supersession +
    // re-insert by design — moving a record across scopes is an audit event, not a field write).
    const next = {
        ...existing,
        ...patch,
        id: existing.id,
        schemaVersion: existing.schemaVersion,
        scope: existing.scope,
        createdAt: existing.createdAt,
        updatedAt: nowMs,
    };
    return next;
}
function existingMemoryOrThrow(db, id, cipher) {
    const existing = getMemoryRow(db, id, cipher);
    if (existing === undefined) {
        throw new MemoryStorageError("not-found", "Memory not found.");
    }
    return existing;
}
function buildTombstone(record, options, opts) {
    const base = {
        id: opts.newTombstoneId(),
        memoryId: record.id,
        scopeKind: scopeKindOf(record.scope),
        scopeCoordinate: scopeCoordinateOf(record.scope),
        type: record.type,
        forgottenAt: options.nowMs,
        forgetterSurface: options.forgetterSurface,
    };
    return options.reason === undefined ? base : { ...base, reason: options.reason };
}
function runDelete(db, id, options, opts) {
    const existing = getMemoryRow(db, id, opts.cipher);
    if (existing === undefined) {
        throw new MemoryStorageError("not-found", "Memory not found.");
    }
    const tombstone = options.tombstone
        ? redactTombstone(buildTombstone(existing, options, opts), opts.redactString)
        : undefined;
    db.exec("BEGIN");
    try {
        deleteMemoryRow(db, id);
        if (tombstone !== undefined) {
            insertTombstoneRow(db, tombstone, opts.cipher);
        }
        db.exec("COMMIT");
    }
    catch (error) {
        db.exec("ROLLBACK");
        throw error;
    }
    return { tombstone };
}
function buildMemoryMutators(db, opts) {
    return {
        insertMemory: (record) => {
            const ready = preparedForWrite(record, opts);
            insertMemoryRow(db, ready, opts.cipher);
            opts.emit({ kind: "memory:inserted", record: ready });
            return ready;
        },
        updateMemory: (id, patch, nowMs) => {
            const existing = existingMemoryOrThrow(db, id, opts.cipher);
            const merged = mergePatch(existing, patch, nowMs);
            const ready = preparedForWrite(merged, opts);
            updateMemoryRow(db, ready, opts.cipher);
            opts.emit({ kind: "memory:updated", record: ready });
            return ready;
        },
        deleteMemory: (id, options) => {
            gateDeleteOptions(options);
            const existing = existingMemoryOrThrow(db, id, opts.cipher);
            const { tombstone } = runDelete(db, id, options, opts);
            opts.emit({
                kind: "memory:deleted",
                memoryId: id,
                scope: existing.scope,
                tombstoned: tombstone !== undefined,
            });
            if (tombstone !== undefined) {
                opts.emit({ kind: "memory:tombstoned", tombstone });
            }
        },
        getMemory: (id) => getMemoryRow(db, id, opts.cipher),
        listMemories: (options) => {
            const effective = options ?? {};
            const nowMs = effective.nowMs ?? opts.now();
            return listMemoriesRows(db, effective, nowMs, opts.cipher);
        },
        listMemoriesByScope: (scope, options) => {
            gateMemoryScope(scope);
            const effective = options ?? {};
            const nowMs = effective.nowMs ?? opts.now();
            return listMemoriesByScopeRows(db, scope, effective, nowMs, opts.cipher);
        },
    };
}
function buildEdgeAndEmbeddingOps(db, opts) {
    return {
        insertEdge: (edge) => {
            const ready = preparedEdgeForWrite(edge, opts);
            insertEdgeRow(db, ready, opts.cipher);
            opts.emit({ kind: "edge:inserted", edge: ready });
            return ready;
        },
        listOutgoingEdges: (memoryId) => listOutgoingEdgeRows(db, memoryId, opts.cipher),
        listIncomingEdges: (memoryId) => listIncomingEdgeRows(db, memoryId, opts.cipher),
        deleteEdge: (edgeId) => {
            const removed = deleteEdgeRow(db, edgeId);
            if (!removed) {
                throw new MemoryStorageError("not-found", "Edge not found.");
            }
            opts.emit({ kind: "edge:deleted", edgeId });
        },
        upsertEmbedding: (memoryId, embedding) => {
            gateEmbeddingInput(embedding);
            if (getMemoryRow(db, memoryId, opts.cipher) === undefined) {
                throw new MemoryStorageError("not-found", "Memory not found.");
            }
            upsertEmbeddingRow(db, memoryId, embedding, opts.now(), opts.cipher);
            opts.emit({
                kind: "embedding:upserted",
                memoryId,
                provider: embedding.provider,
                modelId: embedding.modelId,
            });
        },
        getEmbedding: (memoryId) => getEmbeddingRow(db, memoryId, opts.cipher),
        listTombstonesByScope: (scope) => {
            gateMemoryScope(scope);
            return listTombstonesByScopeRows(db, scope, opts.cipher);
        },
        recordAccess: (ids, nowMs) => {
            recordAccessRows(db, ids, nowMs);
        },
        getAccessStats: (ids) => getAccessStatsRows(db, ids),
    };
}
function buildStore(db, opts) {
    return {
        ...buildMemoryMutators(db, opts),
        ...buildEdgeAndEmbeddingOps(db, opts),
        close: () => {
            db.close();
        },
    };
}
export function createMemoryVault(options) {
    const env = options?.env ?? defaultEnv();
    const dbPath = resolveMemoryDbPath(options?.memoryDir, env);
    const cipher = resolveCipher(options, env);
    const db = openMemoryDatabase(dbPath, cipher);
    return buildStore(db, resolveOptions(options, cipher));
}
