import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, renameSync } from "node:fs";
import { dirname } from "node:path";
import Database from "better-sqlite3";
import { getDefaultMemoryPriority } from "./memory-schema.js";
const CURRENT_SCHEMA_VERSION = 1;
function parseJsonArray(value) {
    if (!value)
        return undefined;
    try {
        const parsed = JSON.parse(value);
        return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string") ? parsed : undefined;
    }
    catch {
        return undefined;
    }
}
function parseMetadata(value) {
    if (!value)
        return undefined;
    try {
        const parsed = JSON.parse(value);
        if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
            return undefined;
        const metadata = {};
        for (const [key, entry] of Object.entries(parsed)) {
            if (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean" || entry === null) {
                metadata[key] = entry;
            }
        }
        return metadata;
    }
    catch {
        return undefined;
    }
}
function rowToEvent(row) {
    return {
        id: row.id,
        projectId: row.project_id,
        sessionId: row.session_id,
        type: row.type,
        summary: row.summary,
        content: row.content ?? undefined,
        priority: row.priority,
        filePaths: parseJsonArray(row.file_paths),
        tags: parseJsonArray(row.tags),
        metadata: parseMetadata(row.metadata),
        createdAt: row.created_at,
    };
}
function toFts5Query(input) {
    const terms = input.match(/[\p{L}\p{N}_]+/gu) ?? [];
    const uniqueTerms = [...new Set(terms.map((term) => term.trim()).filter((term) => term.length > 0))];
    return uniqueTerms.map((term) => `"${term.replace(/"/g, '""')}"`).join(" OR ");
}
export class SqliteMemoryStore {
    kind = "sqlite";
    dbPath;
    db;
    session;
    writeQueue = Promise.resolve();
    constructor(options) {
        this.dbPath = options.dbPath;
    }
    async initialize(session) {
        this.session = session;
        this.open();
        this.ensureProject(session);
        this.ensureSession(session);
    }
    async recordEvent(session, event) {
        return this.enqueueWrite(() => {
            const db = this.open();
            this.ensureProject(session);
            this.ensureSession(session);
            const memoryEvent = {
                ...event,
                id: randomUUID(),
                projectId: session.projectId,
                sessionId: session.sessionId,
                priority: event.priority ?? getDefaultMemoryPriority(event.type),
                createdAt: event.createdAt ?? new Date().toISOString(),
            };
            db.prepare(`INSERT INTO memory_events (id, project_id, session_id, type, summary, content, priority, file_paths, tags, metadata, created_at)
				 VALUES (@id, @projectId, @sessionId, @type, @summary, @content, @priority, @filePaths, @tags, @metadata, @createdAt)`).run({
                ...memoryEvent,
                content: memoryEvent.content ?? null,
                filePaths: memoryEvent.filePaths ? JSON.stringify(memoryEvent.filePaths) : null,
                tags: memoryEvent.tags ? JSON.stringify(memoryEvent.tags) : null,
                metadata: memoryEvent.metadata ? JSON.stringify(memoryEvent.metadata) : null,
            });
            return memoryEvent;
        });
    }
    async search(request) {
        const db = this.open();
        const limit = request.limit ?? 10;
        const query = toFts5Query(request.query);
        if (!query) {
            const rows = db
                .prepare(`SELECT * FROM memory_events
					 WHERE (@projectId IS NULL OR project_id = @projectId)
					   AND (@sessionId IS NULL OR session_id = @sessionId)
					 ORDER BY priority DESC, created_at DESC
					 LIMIT @limit`)
                .all({ projectId: request.projectId ?? null, sessionId: request.sessionId ?? null, limit });
            return rows.map((row, index) => ({
                event: rowToEvent(row),
                score: limit - index,
                reason: "sqlite priority/recency match",
            }));
        }
        const rows = db
            .prepare(`SELECT memory_events.*, bm25(memory_events_fts) AS rank
				 FROM memory_events_fts
				 JOIN memory_events ON memory_events_fts.rowid = memory_events.rowid
				 WHERE memory_events_fts MATCH @query
				   AND (@projectId IS NULL OR memory_events.project_id = @projectId)
				   AND (@sessionId IS NULL OR memory_events.session_id = @sessionId)
				 ORDER BY rank ASC, memory_events.priority DESC, memory_events.created_at DESC
				 LIMIT @limit`)
            .all({ projectId: request.projectId ?? null, sessionId: request.sessionId ?? null, query, limit });
        return rows.map((row) => ({
            event: rowToEvent(row),
            score: -row.rank + row.priority,
            reason: "sqlite fts5 bm25 match",
        }));
    }
    async getRecent(projectId, limit) {
        const rows = this.open()
            .prepare("SELECT * FROM memory_events WHERE project_id = ? ORDER BY created_at DESC LIMIT ?")
            .all(projectId, limit);
        return rows.map(rowToEvent);
    }
    async deleteEvent(projectId, eventId) {
        return this.enqueueWrite(() => {
            const result = this.open()
                .prepare("DELETE FROM memory_events WHERE project_id = ? AND id = ?")
                .run(projectId, eventId);
            return result.changes > 0;
        });
    }
    async purgeProject(projectId) {
        return this.enqueueWrite(() => {
            const result = this.open().prepare("DELETE FROM memory_events WHERE project_id = ?").run(projectId);
            return result.changes;
        });
    }
    async getStatus() {
        const db = this.open();
        const row = db.prepare("SELECT COUNT(*) AS count FROM memory_events").get();
        return {
            enabled: true,
            projectId: this.session?.projectId,
            sessionId: this.session?.sessionId,
            eventCount: row.count,
            store: this.kind,
        };
    }
    async close() {
        this.db?.close();
        this.db = undefined;
    }
    open() {
        if (this.db)
            return this.db;
        mkdirSync(dirname(this.dbPath), { recursive: true });
        try {
            this.db = this.openDatabase();
            this.configureDatabase(this.db);
            return this.db;
        }
        catch (error) {
            this.db?.close();
            this.db = undefined;
            if (!existsSync(this.dbPath))
                throw error;
            const corruptPath = `${this.dbPath}.corrupt.${Date.now()}`;
            renameSync(this.dbPath, corruptPath);
            this.db = this.openDatabase();
            this.configureDatabase(this.db);
            return this.db;
        }
    }
    configureDatabase(db) {
        db.pragma("journal_mode = WAL");
        db.pragma("busy_timeout = 5000");
        this.runMigrations(db);
    }
    runMigrations(db) {
        db.exec(`
			CREATE TABLE IF NOT EXISTS memory_schema_version (
				version INTEGER NOT NULL
			);
		`);
        const versionRow = db.prepare("SELECT version FROM memory_schema_version LIMIT 1").get();
        const version = versionRow?.version ?? 0;
        if (version < 1) {
            db.exec(`
				CREATE TABLE IF NOT EXISTS memory_projects (
					project_id TEXT PRIMARY KEY,
					root_path TEXT NOT NULL,
					git_remote TEXT,
					current_branch TEXT,
					created_at TEXT NOT NULL,
					last_seen_at TEXT NOT NULL
				);

				CREATE TABLE IF NOT EXISTS memory_sessions (
					session_id TEXT PRIMARY KEY,
					project_id TEXT NOT NULL,
					cwd TEXT NOT NULL,
					continuity_session_id TEXT,
					model TEXT,
					provider TEXT,
					started_at TEXT NOT NULL,
					last_seen_at TEXT NOT NULL,
					ended_at TEXT,
					FOREIGN KEY(project_id) REFERENCES memory_projects(project_id)
				);

				CREATE TABLE IF NOT EXISTS memory_events (
					id TEXT PRIMARY KEY,
					project_id TEXT NOT NULL,
					session_id TEXT NOT NULL,
					type TEXT NOT NULL,
					summary TEXT NOT NULL,
					content TEXT,
					priority INTEGER NOT NULL,
					file_paths TEXT,
					tags TEXT,
					metadata TEXT,
					created_at TEXT NOT NULL,
					FOREIGN KEY(project_id) REFERENCES memory_projects(project_id),
					FOREIGN KEY(session_id) REFERENCES memory_sessions(session_id)
				);

				CREATE INDEX IF NOT EXISTS idx_memory_events_project_created ON memory_events(project_id, created_at DESC);
				CREATE INDEX IF NOT EXISTS idx_memory_events_session_created ON memory_events(session_id, created_at DESC);
				CREATE INDEX IF NOT EXISTS idx_memory_events_type_priority ON memory_events(type, priority DESC);

				CREATE VIRTUAL TABLE IF NOT EXISTS memory_events_fts USING fts5(
					summary,
					content,
					tags,
					content='memory_events',
					content_rowid='rowid'
				);

				CREATE TRIGGER IF NOT EXISTS memory_events_ai AFTER INSERT ON memory_events BEGIN
					INSERT INTO memory_events_fts(rowid, summary, content, tags)
					VALUES (new.rowid, new.summary, IFNULL(new.content, ''), IFNULL(new.tags, ''));
				END;

				CREATE TRIGGER IF NOT EXISTS memory_events_ad AFTER DELETE ON memory_events BEGIN
					INSERT INTO memory_events_fts(memory_events_fts, rowid, summary, content, tags)
					VALUES('delete', old.rowid, old.summary, IFNULL(old.content, ''), IFNULL(old.tags, ''));
				END;

				CREATE TRIGGER IF NOT EXISTS memory_events_au AFTER UPDATE ON memory_events BEGIN
					INSERT INTO memory_events_fts(memory_events_fts, rowid, summary, content, tags)
					VALUES('delete', old.rowid, old.summary, IFNULL(old.content, ''), IFNULL(old.tags, ''));
					INSERT INTO memory_events_fts(rowid, summary, content, tags)
					VALUES (new.rowid, new.summary, IFNULL(new.content, ''), IFNULL(new.tags, ''));
				END;
			`);
            db.prepare("DELETE FROM memory_schema_version").run();
            db.prepare("INSERT INTO memory_schema_version (version) VALUES (?)").run(CURRENT_SCHEMA_VERSION);
        }
    }
    openDatabase() {
        return new Database(this.dbPath);
    }
    async enqueueWrite(operation) {
        const pending = this.writeQueue.then(operation, operation);
        this.writeQueue = pending.then(() => undefined, () => undefined);
        return pending;
    }
    ensureProject(session) {
        const now = new Date().toISOString();
        this.open()
            .prepare(`INSERT INTO memory_projects (project_id, root_path, git_remote, current_branch, created_at, last_seen_at)
				 VALUES (@projectId, @rootPath, NULL, NULL, @now, @now)
				 ON CONFLICT(project_id) DO UPDATE SET last_seen_at = excluded.last_seen_at`)
            .run({ projectId: session.projectId, rootPath: session.cwd, now });
    }
    ensureSession(session) {
        const now = new Date().toISOString();
        this.open()
            .prepare(`INSERT INTO memory_sessions (session_id, project_id, cwd, continuity_session_id, model, provider, started_at, last_seen_at)
				 VALUES (@sessionId, @projectId, @cwd, @continuitySessionId, @model, @provider, @now, @now)
				 ON CONFLICT(session_id) DO UPDATE SET last_seen_at = excluded.last_seen_at`)
            .run({
            sessionId: session.sessionId,
            projectId: session.projectId,
            cwd: session.cwd,
            continuitySessionId: session.continuitySessionId ?? null,
            model: session.model ?? null,
            provider: session.provider ?? null,
            now,
        });
    }
}
//# sourceMappingURL=sqlite-memory-store.js.map