import { createHash } from 'node:crypto';
import { dirname, join } from 'node:path';
import { mkdirSync } from 'node:fs';
import { Database } from 'bun:sqlite';
import { load as loadSqliteVec } from 'sqlite-vec';
import { MemoryEmbeddingProviderRegistry, embedMemoryText, normalizeMemoryEmbeddingVector, } from './memory-embeddings.js';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
/**
 * Resolves the path to the sqlite-vec native extension.
 *
 * When running inside a Bun bundled executable (import.meta.url path contains
 * "$bunfs"), the npm package's import.meta.resolve() cannot find the extension
 * because the virtual filesystem does not contain node_modules. In that case,
 * the extension must be co-located with the binary under
 * `<execDir>/lib/sqlite-vec-<os>-<arch>/vec0.<suffix>`.
 *
 * In development (bun run / node), the package's own getLoadablePath() is used
 * via the re-exported `load()` function.
 */
export function resolveSqliteVecPath() {
    const isBundled = import.meta.url.includes('$bunfs');
    if (isBundled) {
        const os = process.platform === 'win32' ? 'windows' : process.platform;
        const arch = process.arch;
        const suffix = process.platform === 'win32' ? 'dll' : process.platform === 'darwin' ? 'dylib' : 'so';
        return join(dirname(process.execPath), 'lib', `sqlite-vec-${os}-${arch}`, `vec0.${suffix}`);
    }
    // In dev mode, delegate to sqlite-vec's own resolver.
    return '';
}
/**
 * Loads the sqlite-vec extension into a Bun SQLite database.
 * Handles both bundled-binary and development execution contexts.
 */
function loadSqliteVecExtension(db) {
    const bundledPath = resolveSqliteVecPath();
    if (bundledPath) {
        db.loadExtension(bundledPath);
    }
    else {
        loadSqliteVec(db);
    }
}
// Keep this in sync with DEFAULT_MEMORY_EMBEDDING_DIMS in memory-embeddings.ts.
// Duplicating the literal here avoids an initialization cycle when state/index.ts
// re-exports both modules during targeted test imports.
export const MEMORY_VECTOR_DIMS = 384;
export { embedMemoryText } from './memory-embeddings.js';
export function resolveMemoryVectorDbPath(dbPath) {
    if (!dbPath || dbPath === ':memory:')
        return ':memory:';
    if (dbPath.endsWith('.sqlite'))
        return dbPath.slice(0, -'.sqlite'.length) + '.vec.sqlite';
    if (dbPath.endsWith('.db'))
        return dbPath.slice(0, -'.db'.length) + '.vec.db';
    return `${dbPath}.vec.sqlite`;
}
export function memoryVectorSourceHash(record) {
    const source = JSON.stringify({
        id: record.id,
        scope: record.scope,
        cls: record.cls,
        summary: record.summary,
        detail: record.detail ?? '',
        tags: record.tags,
        provenance: record.provenance,
        reviewState: record.reviewState,
        confidence: record.confidence,
        createdAt: record.createdAt,
        updatedAt: record.updatedAt,
    });
    return createHash('sha256').update(source).digest('hex');
}
export function buildMemoryEmbeddingText(record) {
    return [
        record.cls,
        record.scope,
        record.summary,
        record.detail ?? '',
        record.tags.join(' '),
        record.provenance.map((entry) => `${entry.kind}:${entry.ref} ${entry.label ?? ''}`).join(' '),
    ].filter(Boolean).join('\n');
}
export class SqliteVecMemoryIndex {
    dbPath;
    dimensions;
    embeddingRegistry;
    db = null;
    enabled = false;
    available = false;
    error;
    static rebuildBatchSize = 25;
    constructor(dbPath, dimensions = MEMORY_VECTOR_DIMS, embeddingRegistry) {
        this.dbPath = dbPath;
        this.dimensions = dimensions;
        this.embeddingRegistry = embeddingRegistry;
    }
    init() {
        if (this.db)
            return;
        try {
            if (this.dbPath !== ':memory:') {
                mkdirSync(dirname(this.dbPath), { recursive: true });
            }
            this.db = new Database(this.dbPath);
            loadSqliteVecExtension(this.db);
            this.available = true;
            this.enabled = true;
            this.createSchema();
        }
        catch (err) {
            this.close();
            this.available = false;
            this.enabled = false;
            this.error = summarizeError(err);
            logger.warn('Memory vector index unavailable', { backend: 'sqlite-vec', error: this.error });
        }
    }
    stats() {
        const provider = this.embeddingRegistry.getDefaultProviderOrNull();
        return {
            backend: 'sqlite-vec',
            enabled: this.enabled,
            available: this.available,
            path: this.dbPath,
            dimensions: this.dimensions,
            indexedRecords: this.countIndexedRecords(),
            embeddingProviderId: provider?.id ?? this.embeddingRegistry.getDefaultProviderId(),
            embeddingProviderLabel: provider?.label ?? `Unregistered (${this.embeddingRegistry.getDefaultProviderId()})`,
            ...(this.error ?? !provider
                ? { error: this.error ?? `Active memory embedding provider '${this.embeddingRegistry.getDefaultProviderId()}' is not registered.` }
                : {}),
        };
    }
    upsert(record) {
        if (!this.db || !this.enabled)
            return;
        try {
            const sourceHash = memoryVectorSourceHash(record);
            const embedding = this.embedText(buildMemoryEmbeddingText(record), 'record', record.id);
            this.writeRecord(record, sourceHash, embedding);
        }
        catch (err) {
            this.markEmbeddingUnavailable(err);
        }
    }
    async upsertAsync(record) {
        if (!this.db || !this.enabled)
            return;
        const sourceHash = memoryVectorSourceHash(record);
        const result = await this.embeddingRegistry.embedAsync({
            text: buildMemoryEmbeddingText(record),
            dimensions: this.dimensions,
            usage: 'record',
            recordId: record.id,
        });
        const embedding = normalizeMemoryEmbeddingVector(result.vector, this.dimensions);
        this.writeRecord(record, sourceHash, embedding);
    }
    delete(recordId) {
        if (!this.db || !this.enabled)
            return;
        const row = this.db.query('SELECT rowid FROM memory_vector_ids WHERE record_id = ? LIMIT 1').get(recordId);
        if (!row)
            return;
        this.db.query('DELETE FROM memory_vectors WHERE rowid = ?').run(row.rowid);
        this.db.query('DELETE FROM memory_vector_ids WHERE rowid = ?').run(row.rowid);
    }
    sync(records) {
        if (!this.db || !this.enabled)
            return;
        const seen = new Set();
        const tx = this.db.transaction((entries) => {
            for (const record of entries) {
                seen.add(record.id);
                const row = this.db?.query('SELECT source_hash FROM memory_vector_ids WHERE record_id = ? LIMIT 1').get(record.id);
                const sourceHash = memoryVectorSourceHash(record);
                if (row?.source_hash === sourceHash)
                    continue;
                this.upsert(record);
            }
            const staleRows = this.db?.query('SELECT record_id FROM memory_vector_ids').all() ?? [];
            for (const stale of staleRows) {
                if (!seen.has(stale.record_id))
                    this.delete(stale.record_id);
            }
        });
        try {
            tx(records);
        }
        catch (err) {
            this.markEmbeddingUnavailable(err);
        }
    }
    async syncAsync(records, options = {}) {
        if (!this.db || !this.enabled)
            return;
        const pending = [];
        for (const record of records) {
            const row = this.db.query('SELECT source_hash FROM memory_vector_ids WHERE record_id = ? LIMIT 1').get(record.id);
            const sourceHash = memoryVectorSourceHash(record);
            if (!options.force && row?.source_hash === sourceHash) {
                continue;
            }
            const result = await this.embeddingRegistry.embedAsync({
                text: buildMemoryEmbeddingText(record),
                dimensions: this.dimensions,
                usage: 'record',
                recordId: record.id,
            });
            pending.push({
                record,
                sourceHash,
                embedding: normalizeMemoryEmbeddingVector(result.vector, this.dimensions),
            });
            if (pending.length >= SqliteVecMemoryIndex.rebuildBatchSize) {
                this.writePendingRebuildBatch(pending.splice(0, pending.length));
                await yieldToEventLoop();
            }
        }
        const staleRows = this.db.query('SELECT record_id FROM memory_vector_ids').all();
        const staleIds = new Set(staleRows.map((row) => row.record_id));
        for (const record of records) {
            staleIds.delete(record.id);
        }
        if (pending.length > 0) {
            this.writePendingRebuildBatch(pending);
        }
        for (const recordId of staleIds) {
            this.delete(recordId);
        }
    }
    search(query, filter = {}) {
        if (!this.db || !this.enabled)
            return [];
        const trimmed = query.trim();
        if (!trimmed)
            return [];
        const requestedLimit = normalizeLimit(filter.limit, 10);
        const vectorLimit = Math.max(requestedLimit, 1);
        let embedding;
        try {
            embedding = this.embedText(trimmed, 'query');
        }
        catch (err) {
            this.markEmbeddingUnavailable(err);
            return [];
        }
        const where = ['embedding MATCH ?', 'k = ?'];
        const params = [embedding, vectorLimit];
        if (filter.scope) {
            where.push('scope = ?');
            params.push(filter.scope);
        }
        if (filter.cls) {
            where.push('cls = ?');
            params.push(filter.cls);
        }
        if (filter.reviewState) {
            const states = Array.isArray(filter.reviewState) ? filter.reviewState : [filter.reviewState];
            if (states.length === 1) {
                where.push('review_state = ?');
                params.push(states[0]);
            }
        }
        if (filter.minConfidence !== undefined) {
            where.push('confidence >= ?');
            params.push(filter.minConfidence);
        }
        if (filter.staleOnly) {
            where.push("(review_state = 'stale' OR review_state = 'contradicted' OR confidence < 70)");
        }
        else {
            where.push("review_state != 'contradicted'");
        }
        const rows = this.db.query(`SELECT rowid, record_id, distance
         FROM memory_vectors
        WHERE ${where.join(' AND ')}
        ORDER BY distance`).all(...params);
        return rows.map((row) => ({
            id: row.record_id,
            distance: Number(row.distance),
            similarity: distanceToSimilarity(Number(row.distance)),
        }));
    }
    close() {
        if (!this.db)
            return;
        this.db.close();
        this.db = null;
        this.enabled = false;
    }
    createSchema() {
        if (!this.db)
            return;
        this.db.run(`
      CREATE TABLE IF NOT EXISTS memory_vector_ids (
        rowid       INTEGER PRIMARY KEY AUTOINCREMENT,
        record_id   TEXT NOT NULL UNIQUE,
        source_hash TEXT NOT NULL DEFAULT '',
        updated_at  INTEGER NOT NULL DEFAULT 0,
        indexed_at  INTEGER NOT NULL DEFAULT 0
      )
    `);
        this.db.run(`
      CREATE VIRTUAL TABLE IF NOT EXISTS memory_vectors USING vec0(
        embedding float[${this.dimensions}],
        scope text,
        cls text,
        review_state text,
        confidence integer,
        +record_id text,
        +source_hash text,
        +updated_at integer,
        +created_at integer
      )
    `);
    }
    ensureRowId(recordId) {
        if (!this.db)
            throw new Error('Memory vector index is not initialized');
        this.db.query('INSERT OR IGNORE INTO memory_vector_ids (record_id) VALUES (?)').run(recordId);
        const row = this.db.query('SELECT rowid FROM memory_vector_ids WHERE record_id = ? LIMIT 1').get(recordId);
        if (!row)
            throw new Error(`Memory vector row id not found for ${recordId}`);
        return Number(row.rowid);
    }
    countIndexedRecords() {
        if (!this.db || !this.enabled)
            return 0;
        const row = this.db.query('SELECT count(*) AS count FROM memory_vector_ids').get();
        return Number(row?.count ?? 0);
    }
    embedText(text, usage, recordId) {
        const result = this.embeddingRegistry.embedSync({
            text,
            dimensions: this.dimensions,
            usage,
            recordId,
        });
        return normalizeMemoryEmbeddingVector(result.vector, this.dimensions);
    }
    markEmbeddingUnavailable(error) {
        this.error = summarizeError(error);
        logger.warn('Memory vector index skipped a synchronous operation because the active embedding provider cannot run synchronously', {
            providerId: this.embeddingRegistry.getDefaultProviderId(),
            error: this.error,
        });
    }
    writePendingRebuildBatch(entries) {
        if (!this.db || !this.enabled || entries.length === 0)
            return;
        const tx = this.db.transaction((batch) => {
            for (const entry of batch) {
                this.writeRecord(entry.record, entry.sourceHash, entry.embedding);
            }
        });
        tx(entries);
    }
    writeRecord(record, sourceHash, embedding) {
        if (!this.db)
            return;
        const rowid = this.ensureRowId(record.id);
        this.db.query('DELETE FROM memory_vectors WHERE rowid = ?').run(rowid);
        this.db.query(`INSERT INTO memory_vectors
         (rowid, embedding, scope, cls, review_state, confidence, record_id, source_hash, updated_at, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(rowid, embedding, record.scope, record.cls, record.reviewState, record.confidence, record.id, sourceHash, record.updatedAt, record.createdAt);
        this.db.query(`UPDATE memory_vector_ids
         SET source_hash = ?, updated_at = ?, indexed_at = ?
       WHERE rowid = ?`).run(sourceHash, record.updatedAt, Date.now(), rowid);
        this.error = undefined;
    }
}
function normalizeLimit(value, fallback) {
    if (!Number.isFinite(value))
        return fallback;
    return Math.max(1, Math.min(500, Math.round(value)));
}
function distanceToSimilarity(distance) {
    if (!Number.isFinite(distance))
        return 0;
    return Math.max(0, Math.min(1, 1 - (distance / 2)));
}
function yieldToEventLoop() {
    return new Promise((resolve) => {
        const timer = setTimeout(resolve, 0);
        timer.unref?.();
    });
}
