/**
 * Memory FTS5 Search Adapter — SQLite FTS5 backend for MemoryCore Facade.
 * 
 * PR-3.5: FTS5 Adapter Alignment
 * Architecture Freeze v2 (PR-1): Adapter layer
 * 
 * Queries the same SQLite FTS5 index used by legacy memory_search.
 * READ-ONLY — never writes to the database.
 * Falls back to file-search adapter when FTS5 is unavailable.
 * 
 * Usage:
 *   import { createFTS5SearchAdapter } from "./memory-fts5-search-adapter.mjs";
 *   const adapter = createFTS5SearchAdapter(dbPath, fallbackAdapter);
 *   const result = await adapter.search("query", { maxResults: 10 });
 */

import { existsSync } from "node:fs";
import { createRequire } from "node:module";

// ─── Lazy-load better-sqlite3 (CJS native module) ─────

let _Database = null;
let _loadAttempted = false;

function loadDatabase() {
  if (_loadAttempted) return _Database;
  _loadAttempted = true;
  
  try {
    const require = createRequire(import.meta.url);
    _Database = require("better-sqlite3");
  } catch {
    _Database = null;
  }
  return _Database;
}

// ─── FTS5 query helpers ──────────────────────────────

/**
 * Escape a string for FTS5 MATCH syntax.
 * Strips special chars, wraps terms in double quotes.
 */
function escapeFTS5(query) {
  const cleaned = query.replace(/["'*()^~:@&|!\-\\\[\]{}<>]/g, " ");
  const terms = cleaned.split(/\s+/).filter(t => t.length > 0);
  if (terms.length === 0) return "";
  return terms.map(t => `"${t}"`).join(" AND ");
}

// ─── Main factory ────────────────────────────────────

/**
 * @param {string} memoryDbPath - Path to main.sqlite
 * @param {object} [fallbackAdapter] - Optional file-search fallback
 * @returns {object} { search, isAvailable }
 */
export function createFTS5SearchAdapter(memoryDbPath, fallbackAdapter) {

  /**
   * Open read-only connection. Returns null if unavailable.
   */
  function openDb() {
    const DB = loadDatabase();
    if (!DB) return null;
    if (!existsSync(memoryDbPath)) return null;
    try {
      return new DB(memoryDbPath, { readonly: true });
    } catch {
      return null;
    }
  }

  /**
   * Execute FTS5 search against chunks_fts table.
   */
  async function search(query, opts = {}) {
    const startTime = Date.now();
    const { maxResults = 10 } = opts;
    const elapsed = () => Date.now() - startTime;

    // Reject empty queries — don't replicate legacy match-all quirk
    if (!query || typeof query !== "string" || query.trim().length === 0) {
      return {
        ok: false,
        results: [],
        source: "fts5-search",
        latencyMs: elapsed(),
        error: { code: "INVALID_QUERY", message: "Query must be a non-empty string" },
      };
    }

    const db = openDb();

    // FTS5 unavailable → fallback to file-search
    if (!db) {
      if (fallbackAdapter) {
        const fb = await fallbackAdapter.search(query, opts);
        return { ...fb, source: "file-search-fallback" };
      }
      return {
        ok: false,
        results: [],
        source: "file-search-fallback",
        latencyMs: elapsed(),
        error: { code: "FTS5_UNAVAILABLE", message: "FTS5 database not available and no fallback" },
      };
    }

    try {
      const ftsQuery = escapeFTS5(query);

      if (!ftsQuery) {
        db.close();
        return {
          ok: false,
          results: [],
          source: "fts5-search",
          latencyMs: elapsed(),
          error: { code: "NO_INDEXABLE_TERMS", message: "Query contained no indexable terms" },
        };
      }

      // Primary: FTS5 MATCH
      let rows;
      try {
        const stmt = db.prepare(`
          SELECT
            chunks.id,
            chunks.path,
            chunks.source,
            chunks.start_line,
            chunks.end_line,
            snippet(chunks_fts, 0, '<mark>', '</mark>', '...', 50) as snippet,
            rank
          FROM chunks_fts
          JOIN chunks ON chunks_fts.rowid = chunks.rowid
          WHERE chunks_fts MATCH ?
          ORDER BY rank
          LIMIT ?
        `);
        rows = stmt.all(ftsQuery, maxResults);
      } catch {
        // FTS5 syntax error after escaping — fall back to LIKE
        const safeTerm = query.replace(/[%_]/g, " ").trim().split(/\s+/)[0] || query;
        if (safeTerm) {
          const likeStmt = db.prepare(`
            SELECT id, path, source, start_line, end_line,
                   substr(text, 1, 200) as snippet,
                   0.5 as rank
            FROM chunks
            WHERE text LIKE ?
            LIMIT ?
          `);
          rows = likeStmt.all(`%${safeTerm}%`, maxResults);
        } else {
          rows = [];
        }
      }

      // Map to unified facade contract
      const results = (rows || []).map(r => ({
        id: String(r.id),
        path: r.path || "",
        snippet: (r.snippet || "").replace(/<mark>/g, "").replace(/<\/mark>/g, ""),
        score: r.rank != null ? Math.min(1, Math.max(0, 1 - Math.abs(r.rank) / 10)) : 0.5,
        source: r.source || "memory",
      }));

      db.close();

      return {
        ok: true,
        results,
        source: "fts5-search",
        latencyMs: elapsed(),
      };
    } catch (e) {
      try { db.close(); } catch {}

      // Fallback on error
      if (fallbackAdapter) {
        const fb = await fallbackAdapter.search(query, opts);
        return { ...fb, source: "file-search-fallback" };
      }

      return {
        ok: false,
        results: [],
        source: "fts5-search",
        latencyMs: elapsed(),
        error: { code: "SEARCH_ERROR", message: e.message },
      };
    }
  }

  /**
   * Check if FTS5 backend is reachable.
   */
  async function isAvailable() {
    const db = openDb();
    if (db) { db.close(); return true; }
    return false;
  }

  return { search, isAvailable };
}

export default createFTS5SearchAdapter;
