/**
 * FTS5 Index Service
 *
 * Manages a SQLite FTS5 full-text search index for the Wiki-First
 * Knowledge System. Provides upsert, remove, search, and rebuild
 * operations over a `knowledge_fts` virtual table using Porter stemming
 * and Unicode61 tokenization.
 *
 * The database file is stored at `<knowledgePath>/index.fts5.db`.
 *
 * @module services/knowledge/fts5-index.service
 */
import * as path from 'path';
import { existsSync, mkdirSync } from 'fs';
import { createBareModuleRequire } from '../../utils/node-require.utils.js';
import { LoggerService } from '../core/logger.service.js';
import { toFts5Phrase } from './fts5-query-sanitizer.js';
// ---------------------------------------------------------------------------
// Lazy module loading
// ---------------------------------------------------------------------------
/**
 * CJS-style `require` scoped to this module. `better-sqlite3` is a native
 * addon and must be loaded via CJS `require`, but this file compiles to
 * ESM where the bare `require` global is undefined.
 *
 * Anchor: see `backend/src/utils/node-require.utils.ts` — the helper
 * hides `import.meta.url` behind direct `eval` so ts-jest's CommonJS
 * transpile does not see the meta-property token. The createRequire
 * anchor is THIS file's URL, so Node's resolver walks up from here
 * to find `node_modules` (and to resolve any future relative paths).
 */
const nodeRequire = createBareModuleRequire(typeof require === 'function' ? require : null);
/** Lazy-loaded better-sqlite3 module reference. */
let _BetterSqlite3 = null;
/**
 * Load better-sqlite3 on demand. Throws a clear error if the native module
 * is not available.
 */
function getBetterSqlite3() {
    if (!_BetterSqlite3) {
        try {
            _BetterSqlite3 = nodeRequire('better-sqlite3');
        }
        catch (err) {
            throw new Error('better-sqlite3 native module failed to load. Run `npm rebuild better-sqlite3` to fix. ' +
                `Original error: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    return _BetterSqlite3;
}
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
/**
 * Fts5IndexService manages a SQLite FTS5 full-text search database.
 *
 * It lazily initializes the database and FTS5 virtual table on first use,
 * and provides methods to upsert, remove, search, and rebuild the index.
 *
 * @example
 * ```typescript
 * const fts5 = new Fts5IndexService('/path/to/knowledge');
 * fts5.upsertDocument({ id: 'doc-1', title: 'Setup', tags: 'devops', content: '...', category: 'SOPs' });
 * const results = fts5.search('setup devops');
 * ```
 */
export class Fts5IndexService {
    db = null;
    dbPath;
    logger;
    /**
     * Create an Fts5IndexService.
     *
     * @param knowledgePath - Directory where the FTS5 database will be stored
     */
    constructor(knowledgePath) {
        this.dbPath = path.join(knowledgePath, 'index.fts5.db');
        this.logger = LoggerService.getInstance().createComponentLogger('Fts5IndexService');
    }
    /**
     * Get or lazily initialize the SQLite database connection.
     * Creates the parent directory and FTS5 virtual table if they do not exist.
     *
     * @returns The better-sqlite3 Database instance
     */
    getDb() {
        if (this.db) {
            return this.db;
        }
        // Ensure parent directory exists
        const dir = path.dirname(this.dbPath);
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true });
        }
        const Database = getBetterSqlite3();
        this.db = new Database(this.dbPath);
        // Performance pragmas
        this.db.pragma('journal_mode = WAL');
        this.db.pragma('synchronous = NORMAL');
        this.initSchema();
        this.logger.info('FTS5 index database opened', { path: this.dbPath });
        return this.db;
    }
    /**
     * Create the FTS5 virtual table if it does not already exist.
     * Uses Porter stemming and Unicode61 tokenization for broad language support.
     */
    initSchema() {
        const db = this.db;
        db.exec(`
      CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
        id UNINDEXED,
        title,
        tags,
        content,
        category UNINDEXED,
        tokenize='porter unicode61'
      );
    `);
    }
    /**
     * Upsert a document into the FTS5 index.
     * Removes any existing row with the same ID before inserting.
     *
     * @param doc - The document to upsert
     */
    upsertDocument(doc) {
        const db = this.getDb();
        // FTS5 tables do not support ON CONFLICT, so delete-then-insert
        const deleteStmt = db.prepare('DELETE FROM knowledge_fts WHERE id = ?');
        const insertStmt = db.prepare('INSERT INTO knowledge_fts (id, title, tags, content, category) VALUES (?, ?, ?, ?, ?)');
        const txn = db.transaction(() => {
            deleteStmt.run(doc.id);
            insertStmt.run(doc.id, doc.title, doc.tags, doc.content, doc.category);
        });
        txn();
    }
    /**
     * Remove a document from the FTS5 index by its ID.
     *
     * @param id - The document identifier to remove
     */
    removeDocument(id) {
        const db = this.getDb();
        db.prepare('DELETE FROM knowledge_fts WHERE id = ?').run(id);
    }
    /**
     * Search the FTS5 index using a MATCH query with BM25 ranking.
     *
     * @param query - The search query string (FTS5 MATCH syntax)
     * @param options - Optional search parameters
     * @param options.category - Filter results by category
     * @param options.limit - Maximum number of results (default 10)
     * @returns Array of matching documents sorted by relevance
     */
    search(query, options) {
        if (!query || query.trim().length === 0) {
            return [];
        }
        const db = this.getDb();
        const limit = options?.limit ?? 10;
        // Sanitize query via phrase-quote tokenization. See
        // {@link toFts5Phrase} for the full design rationale — short version:
        // the previous regex-strip sanitizer left commas, hyphens, colons, and
        // FTS5 keywords intact, causing 44 silent agent-recall failures per day
        // (F-FTS5 / WI-E triage). Phrase-quoting neutralizes ALL operator
        // characters because FTS5 does not parse operators inside `"..."`
        // phrases. Each token round-trips through unicode61 at index AND
        // query time, so recall is preserved for hyphenated identifiers like
        // `team-leader`.
        const sanitized = toFts5Phrase(query);
        if (sanitized.length === 0) {
            return [];
        }
        try {
            if (options?.category) {
                const stmt = db.prepare(`
          SELECT id, title, tags, content, category, rank
          FROM knowledge_fts
          WHERE knowledge_fts MATCH ?
            AND category = ?
          ORDER BY rank
          LIMIT ?
        `);
                return stmt.all(sanitized, options.category, limit);
            }
            const stmt = db.prepare(`
        SELECT id, title, tags, content, category, rank
        FROM knowledge_fts
        WHERE knowledge_fts MATCH ?
        ORDER BY rank
        LIMIT ?
      `);
            return stmt.all(sanitized, limit);
        }
        catch (err) {
            this.logger.warn('FTS5 search query failed, returning empty results', {
                query: sanitized,
                error: err instanceof Error ? err.message : String(err),
            });
            return [];
        }
    }
    /**
     * Rebuild the entire FTS5 index from scratch.
     * Drops all existing rows and reinserts the provided documents in a single transaction.
     *
     * @param documents - The complete set of documents to index
     */
    rebuildIndex(documents) {
        const db = this.getDb();
        const deleteAll = db.prepare('DELETE FROM knowledge_fts');
        const insertStmt = db.prepare('INSERT INTO knowledge_fts (id, title, tags, content, category) VALUES (?, ?, ?, ?, ?)');
        const txn = db.transaction(() => {
            deleteAll.run();
            for (const doc of documents) {
                insertStmt.run(doc.id, doc.title, doc.tags, doc.content, doc.category);
            }
        });
        txn();
        this.logger.info('FTS5 index rebuilt', { documentCount: documents.length });
    }
    /**
     * Close the database connection and release resources.
     */
    close() {
        if (this.db) {
            try {
                this.db.close();
                this.logger.debug('FTS5 index database closed', { path: this.dbPath });
            }
            catch (err) {
                this.logger.warn('Error closing FTS5 database', {
                    error: err instanceof Error ? err.message : String(err),
                });
            }
            this.db = null;
        }
    }
}
//# sourceMappingURL=fts5-index.service.js.map