/**
 * MemoryStore — project memory substrate.
 *
 * Durable, provenance-rich memory for decisions, constraints, incidents, and
 * patterns. Backed by SQLite via SQLiteStore. Survives process restarts and is
 * queryable by runtime/panel/context enrichment consumers.
 *
 * Provenance links can reference: session, turn, task, event, or file.
 */
import { randomUUID } from 'node:crypto';
import { SQLiteStore } from './sqlite-store.js';
import { SqliteVecMemoryIndex, resolveMemoryVectorDbPath, } from './memory-vector-store.js';
import { HASHED_MEMORY_EMBEDDING_PROVIDER, MemoryEmbeddingProviderRegistry, } from './memory-embeddings.js';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
import { clampConfidence, createSchema, isReviewCandidate, isReviewFlagged, normalizeReviewState, normalizeScope, recordMatchesPostSqlFilter, reviewQueueScore, rowToRecord, scoreRecord, } from './memory-store-helpers.js';
export { MemoryRegistry } from './memory-registry.js';
// ── MemoryStore ───────────────────────────────────────────────────────────────
export class MemoryStore {
    sqlite;
    vectorIndex;
    ready = false;
    rebuildVectorIndexPromise = null;
    embeddingRegistry;
    constructor(dbPath, options) {
        this.sqlite = new SQLiteStore(dbPath);
        this.embeddingRegistry = options.embeddingRegistry;
        this.vectorIndex = options.enableVectorIndex === false
            ? null
            : new SqliteVecMemoryIndex(options.vectorDbPath ?? resolveMemoryVectorDbPath(dbPath), undefined, this.embeddingRegistry);
    }
    async init() {
        if (this.ready)
            return;
        await this.sqlite.init(createSchema);
        this.ready = true;
        this.vectorIndex?.init();
        this.rebuildVectorIndex();
        logger.info('MemoryStore: initialized', { ready: true });
    }
    get isReady() {
        return this.ready;
    }
    /** Add a new memory record. Returns the created record. */
    async add(opts) {
        if (!this.ready)
            throw new Error('MemoryStore: not initialized');
        const now = Date.now();
        const id = `mem_${now.toString(36)}_${randomUUID().replace(/-/g, '').slice(0, 8)}`;
        const record = {
            id,
            scope: opts.scope ?? 'project',
            cls: opts.cls,
            summary: opts.summary,
            detail: opts.detail,
            tags: opts.tags ?? [],
            provenance: opts.provenance ?? [],
            reviewState: opts.review?.state ?? 'fresh',
            confidence: clampConfidence(opts.review?.confidence ?? 60),
            reviewedAt: opts.review?.reviewedAt,
            reviewedBy: opts.review?.reviewedBy,
            staleReason: opts.review?.staleReason,
            createdAt: now,
            updatedAt: now,
        };
        this.sqlite.run(`INSERT INTO memory_records
         (id, scope, cls, summary, detail, tags, provenance, review_state, confidence, reviewed_at, reviewed_by, stale_reason, created_at, updated_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
            record.id,
            record.scope,
            record.cls,
            record.summary,
            record.detail ?? null,
            JSON.stringify(record.tags),
            JSON.stringify(record.provenance),
            record.reviewState,
            record.confidence,
            record.reviewedAt ?? null,
            record.reviewedBy ?? null,
            record.staleReason ?? null,
            record.createdAt,
            record.updatedAt,
        ]);
        logger.info('MemoryStore: record added', { id, cls: opts.cls });
        this.vectorIndex?.upsert(record);
        this.persist();
        return record;
    }
    /** Retrieve a single record by ID. */
    get(id) {
        if (!this.ready)
            return null;
        const rows = this.sqlite.exec(`SELECT id, scope, cls, summary, detail, tags, provenance, review_state, confidence, reviewed_at, reviewed_by, stale_reason, created_at, updated_at
         FROM memory_records WHERE id = ? LIMIT 1`, [id]);
        if (!rows.length || !(rows[0]?.values.length))
            return null;
        return rowToRecord(rows[0]?.columns ?? [], rows[0]?.values[0] ?? []);
    }
    /** Search records with an optional filter. */
    search(filter = {}) {
        if (!this.ready)
            return [];
        if (filter.semantic) {
            return this.searchSemantic(filter).map((entry) => entry.record);
        }
        const conditions = [];
        const params = [];
        if (filter.scope) {
            conditions.push('scope = ?');
            params.push(filter.scope);
        }
        if (filter.cls) {
            conditions.push('cls = ?');
            params.push(filter.cls);
        }
        if (filter.since) {
            conditions.push('created_at >= ?');
            params.push(filter.since);
        }
        if (filter.reviewState) {
            if (Array.isArray(filter.reviewState)) {
                conditions.push(`review_state IN (${filter.reviewState.map(() => '?').join(', ')})`);
                params.push(...filter.reviewState);
            }
            else {
                conditions.push('review_state = ?');
                params.push(filter.reviewState);
            }
        }
        if (filter.query) {
            const escaped = filter.query.replace(/%/g, '\\%').replace(/_/g, '\\_');
            conditions.push("(summary LIKE ? ESCAPE '\\' OR detail LIKE ? ESCAPE '\\')");
            params.push(`%${escaped}%`, `%${escaped}%`);
        }
        if (filter.tags?.length) {
            for (const tag of filter.tags) {
                const escapedTag = tag.replace(/%/g, '\\%').replace(/_/g, '\\_');
                conditions.push("tags LIKE ? ESCAPE '\\'");
                params.push(`%"${escapedTag}"%`);
            }
        }
        const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
        const rows = this.sqlite.exec(`SELECT id, scope, cls, summary, detail, tags, provenance, review_state, confidence, reviewed_at, reviewed_by, stale_reason, created_at, updated_at
         FROM memory_records ${where}
         ORDER BY updated_at DESC, created_at DESC`, params);
        if (!rows.length)
            return [];
        let records = (rows[0]?.values ?? []).map(v => rowToRecord(rows[0]?.columns ?? [], v));
        if (filter.minConfidence !== undefined) {
            records = records.filter((record) => record.confidence >= filter.minConfidence);
        }
        if (filter.provenanceKinds?.length) {
            const allowed = new Set(filter.provenanceKinds);
            records = records.filter((record) => record.provenance.some((link) => allowed.has(link.kind)));
        }
        if (filter.staleOnly) {
            records = records.filter((record) => isReviewFlagged(record));
        }
        records = records.sort((a, b) => scoreRecord(b, filter) - scoreRecord(a, filter) || b.updatedAt - a.updatedAt || b.createdAt - a.createdAt);
        if (filter.limit !== undefined) {
            records = records.slice(0, filter.limit);
        }
        return records;
    }
    searchSemantic(filter = {}) {
        if (!this.ready)
            return [];
        const query = filter.query?.trim();
        if (!query) {
            return this.search({ ...filter, semantic: false }).map((record) => ({
                record,
                distance: Number.POSITIVE_INFINITY,
                similarity: 0,
                score: scoreRecord(record, { ...filter, semantic: false }),
            }));
        }
        const requestedLimit = Math.max(1, filter.limit ?? 10);
        const vectorFilter = {
            ...filter,
            limit: Math.max(requestedLimit * 8, 50),
        };
        const candidates = this.vectorIndex?.search(query, vectorFilter) ?? [];
        if (candidates.length === 0) {
            return this.search({ ...filter, semantic: false }).map((record) => ({
                record,
                distance: Number.POSITIVE_INFINITY,
                similarity: 0,
                score: scoreRecord(record, { ...filter, semantic: false }),
            }));
        }
        const results = [];
        for (const candidate of candidates) {
            const record = this.get(candidate.id);
            if (!record)
                continue;
            if (!recordMatchesPostSqlFilter(record, filter))
                continue;
            const lexicalScore = scoreRecord(record, { ...filter, query: undefined, semantic: false });
            results.push({
                record,
                distance: candidate.distance,
                similarity: candidate.similarity,
                score: candidate.similarity * 100 + lexicalScore * 0.25,
            });
        }
        return results
            .sort((a, b) => b.score - a.score || a.distance - b.distance || b.record.updatedAt - a.record.updatedAt)
            .slice(0, requestedLimit);
    }
    reviewQueue(limit = 10) {
        const records = this.search({ limit: Math.max(limit * 4, 25) });
        const candidates = records.filter((record) => isReviewCandidate(record));
        return candidates
            .sort((a, b) => reviewQueueScore(b) - reviewQueueScore(a) || b.updatedAt - a.updatedAt || b.createdAt - a.createdAt)
            .slice(0, limit);
    }
    exportBundle(filter = {}) {
        const records = this.search(filter);
        const recordIds = new Set(records.map((record) => record.id));
        const links = [];
        for (const record of records) {
            for (const link of this.linksFor(record.id)) {
                if (!recordIds.has(link.fromId) || !recordIds.has(link.toId))
                    continue;
                const duplicate = links.some((existing) => (existing.fromId === link.fromId
                    && existing.toId === link.toId
                    && existing.relation === link.relation));
                if (!duplicate)
                    links.push(link);
            }
        }
        return {
            schemaVersion: 'v1',
            exportedAt: Date.now(),
            scope: filter.scope ?? 'all',
            recordCount: records.length,
            linkCount: links.length,
            records,
            links,
        };
    }
    async importBundle(bundle) {
        if (!this.ready)
            throw new Error('MemoryStore: not initialized');
        let importedRecords = 0;
        let skippedRecords = 0;
        let importedLinks = 0;
        for (const record of bundle.records) {
            if (this.get(record.id)) {
                skippedRecords++;
                continue;
            }
            this.sqlite.run(`INSERT INTO memory_records
           (id, scope, cls, summary, detail, tags, provenance, review_state, confidence, reviewed_at, reviewed_by, stale_reason, created_at, updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
                record.id,
                normalizeScope(record.scope),
                record.cls,
                record.summary,
                record.detail ?? null,
                JSON.stringify(record.tags),
                JSON.stringify(record.provenance),
                normalizeReviewState(record.reviewState),
                clampConfidence(record.confidence),
                record.reviewedAt ?? null,
                record.reviewedBy ?? null,
                record.staleReason ?? null,
                record.createdAt,
                record.updatedAt,
            ]);
            importedRecords++;
            this.vectorIndex?.upsert(record);
        }
        for (const link of bundle.links) {
            if (!this.get(link.fromId) || !this.get(link.toId))
                continue;
            this.sqlite.run(`INSERT OR REPLACE INTO memory_links (from_id, to_id, relation, created_at)
         VALUES (?, ?, ?, ?)`, [link.fromId, link.toId, link.relation, link.createdAt]);
            importedLinks++;
        }
        logger.info('MemoryStore: bundle imported', { importedRecords, skippedRecords, importedLinks });
        this.persist();
        return { importedRecords, skippedRecords, importedLinks };
    }
    /** Create a directed link between two records. */
    async link(fromId, toId, relation) {
        if (!this.ready)
            return null;
        const from = this.get(fromId);
        const to = this.get(toId);
        if (!from) {
            logger.warn('MemoryStore: link source not found', { fromId });
            return null;
        }
        if (!to) {
            logger.warn('MemoryStore: link target not found', { toId });
            return null;
        }
        const now = Date.now();
        try {
            this.sqlite.run(`INSERT OR REPLACE INTO memory_links (from_id, to_id, relation, created_at)
         VALUES (?, ?, ?, ?)`, [fromId, toId, relation, now]);
        }
        catch (err) {
            logger.error('MemoryStore: link insert failed', {
                error: summarizeError(err),
            });
            return null;
        }
        this.persist();
        return { fromId, toId, relation, createdAt: now };
    }
    /** Get all links where this record is either source or target. */
    linksFor(id) {
        if (!this.ready)
            return [];
        const rows = this.sqlite.exec(`SELECT from_id, to_id, relation, created_at
         FROM memory_links
         WHERE from_id = ? OR to_id = ?
         ORDER BY created_at DESC`, [id, id]);
        if (!rows.length)
            return [];
        return (rows[0]?.values ?? []).map(v => {
            const col = rows[0]?.columns ?? [];
            return {
                fromId: String(v[col.indexOf('from_id')]),
                toId: String(v[col.indexOf('to_id')]),
                relation: String(v[col.indexOf('relation')]),
                createdAt: Number(v[col.indexOf('created_at')]),
            };
        });
    }
    /** Update mutable fields of an existing record. */
    update(id, patch) {
        if (!this.ready)
            return null;
        const existing = this.get(id);
        if (!existing) {
            logger.warn('MemoryStore: update target not found', { id });
            return null;
        }
        const now = Date.now();
        const newScope = patch.scope ?? existing.scope;
        const newSummary = patch.summary ?? existing.summary;
        const newDetail = patch.detail !== undefined ? patch.detail : existing.detail;
        const newTags = patch.tags ?? existing.tags;
        this.sqlite.run(`UPDATE memory_records
         SET scope = ?, summary = ?, detail = ?, tags = ?, updated_at = ?
         WHERE id = ?`, [newScope, newSummary, newDetail ?? null, JSON.stringify(newTags), now, id]);
        logger.info('MemoryStore: record updated', { id });
        const updated = { ...existing, scope: newScope, summary: newSummary, detail: newDetail, tags: newTags, updatedAt: now };
        this.vectorIndex?.upsert(updated);
        this.persist();
        return updated;
    }
    review(id, patch) {
        if (!this.ready)
            return null;
        const existing = this.get(id);
        if (!existing) {
            logger.warn('MemoryStore: review target not found', { id });
            return null;
        }
        const now = Date.now();
        const reviewState = patch.state ?? existing.reviewState;
        const confidence = clampConfidence(patch.confidence ?? existing.confidence);
        const reviewedAt = now;
        const reviewedBy = patch.reviewedBy ?? existing.reviewedBy;
        const staleReason = reviewState === 'stale' || reviewState === 'contradicted'
            ? (patch.staleReason ?? existing.staleReason ?? 'marked by operator')
            : undefined;
        this.sqlite.run(`UPDATE memory_records
         SET review_state = ?, confidence = ?, reviewed_at = ?, reviewed_by = ?, stale_reason = ?, updated_at = ?
         WHERE id = ?`, [
            reviewState,
            confidence,
            reviewedAt,
            reviewedBy ?? null,
            staleReason ?? null,
            now,
            id,
        ]);
        logger.info('MemoryStore: record reviewed', { id, reviewState, confidence });
        const reviewed = { ...existing, reviewState, confidence, reviewedAt, reviewedBy, staleReason, updatedAt: now };
        this.vectorIndex?.upsert(reviewed);
        this.persist();
        return reviewed;
    }
    /** Delete a record and all its links. */
    delete(id) {
        if (!this.ready)
            return false;
        const existing = this.get(id);
        if (!existing)
            return false;
        // Delete links explicitly as well as via FK cascade; sql.js may load older
        // stores with FK enforcement disabled for a connection.
        this.sqlite.run('DELETE FROM memory_links WHERE from_id = ? OR to_id = ?', [id, id]);
        this.sqlite.run('DELETE FROM memory_records WHERE id = ?', [id]);
        this.vectorIndex?.delete(id);
        logger.info('MemoryStore: record deleted', { id });
        this.persist();
        return true;
    }
    rebuildVectorIndex() {
        if (!this.ready)
            return this.vectorStats();
        const rows = this.sqlite.exec(`SELECT id, scope, cls, summary, detail, tags, provenance, review_state, confidence, reviewed_at, reviewed_by, stale_reason, created_at, updated_at
         FROM memory_records
         ORDER BY updated_at DESC, created_at DESC`);
        const records = rows.length ? (rows[0]?.values ?? []).map((value) => rowToRecord(rows[0]?.columns ?? [], value)) : [];
        this.vectorIndex?.sync(records);
        return this.vectorStats();
    }
    async rebuildVectorIndexAsync() {
        if (!this.ready)
            return this.vectorStats();
        if (!this.rebuildVectorIndexPromise) {
            this.rebuildVectorIndexPromise = (async () => {
                try {
                    const rows = this.sqlite.exec(`SELECT id, scope, cls, summary, detail, tags, provenance, review_state, confidence, reviewed_at, reviewed_by, stale_reason, created_at, updated_at
               FROM memory_records
               ORDER BY updated_at DESC, created_at DESC`);
                    const records = rows.length ? (rows[0]?.values ?? []).map((value) => rowToRecord(rows[0]?.columns ?? [], value)) : [];
                    if (this.vectorIndex?.syncAsync) {
                        await this.vectorIndex.syncAsync(records, { force: true });
                    }
                    else {
                        this.vectorIndex?.sync(records);
                    }
                    return this.vectorStats();
                }
                finally {
                    this.rebuildVectorIndexPromise = null;
                }
            })();
        }
        return this.rebuildVectorIndexPromise;
    }
    vectorStats() {
        return this.vectorIndex?.stats() ?? {
            backend: 'sqlite-vec',
            enabled: false,
            available: false,
            path: '',
            dimensions: 0,
            indexedRecords: 0,
            embeddingProviderId: HASHED_MEMORY_EMBEDDING_PROVIDER.id,
            embeddingProviderLabel: HASHED_MEMORY_EMBEDDING_PROVIDER.label,
            error: 'memory vector index disabled',
        };
    }
    async doctor() {
        return {
            vector: this.vectorStats(),
            embeddings: await this.embeddingRegistry.doctor(),
            checkedAt: Date.now(),
        };
    }
    async save() {
        return this.sqlite.save();
    }
    close() {
        this.sqlite.close();
        this.vectorIndex?.close();
        this.ready = false;
    }
    /**
     * Re-root the store to a new SQLite database path.
     *
     * Closes the existing SQLite handle and vector index, then re-opens both at
     * the new path. The caller is responsible for ensuring no writes are in-flight
     * when this is called (WorkspaceSwapManager enforces this via the busy-session
     * guard).
     *
     * @throws if the new SQLite store or vector index cannot be opened.
     */
    async reroot(newDbPath) {
        // Step 1: close existing handles
        this.sqlite.close();
        this.vectorIndex?.close();
        this.ready = false;
        this.rebuildVectorIndexPromise = null;
        // Step 2: open at new path
        this.sqlite = new SQLiteStore(newDbPath);
        this.vectorIndex = new SqliteVecMemoryIndex(resolveMemoryVectorDbPath(newDbPath), undefined, this.embeddingRegistry);
        // Step 3: initialise
        await this.init();
    }
    // ── Private helpers ──────────────────────────────────────────────────────────
    persist() {
        void this.save().catch((err) => {
            logger.warn('MemoryStore: autosave failed', { error: summarizeError(err) });
        });
    }
}
