/**
 * SQLite-backed vector storage for embedding chunks.
 * Uses better-sqlite3 for synchronous operations and stores
 * embeddings as Float32Array BLOBs.
 */

import Database from 'better-sqlite3';
import type { Chunk } from './chunker.js';

/**
 * A single row from the embeddings table.
 */
export interface EmbeddingRow {
  id: number;
  namespace: string;
  chunk_text: string;
  start_line: number;
  end_line: number;
  embedding: Buffer;
  created_at: string;
  content_hash: string;
}

/**
 * A search result from the vector store.
 */
export interface VectorSearchResult {
  namespace: string;
  chunkText: string;
  startLine: number;
  endLine: number;
  score: number;
}

/**
 * Compute cosine similarity between two vectors.
 * Returns a value between -1 and 1 (1 = identical direction).
 */
export function cosineSimilarity(a: number[] | Float32Array, b: number[] | Float32Array): number {
  if (a.length !== b.length) {
    throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
  }

  let dotProduct = 0;
  let normA = 0;
  let normB = 0;

  for (let i = 0; i < a.length; i++) {
    dotProduct += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }

  const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
  if (magnitude === 0) {return 0;}

  return dotProduct / magnitude;
}

/**
 * Simple content hash for change detection.
 * Uses a fast string hash rather than crypto for speed.
 */
export function contentHash(text: string): string {
  // FNV-1a 32-bit hash
  let hash = 0x811c9dc5;
  for (let i = 0; i < text.length; i++) {
    hash ^= text.charCodeAt(i);
    hash = Math.imul(hash, 0x01000193);
  }
  return (hash >>> 0).toString(16).padStart(8, '0');
}

/**
 * SQLite-backed vector store that persists embedding chunks.
 */
export class VectorStore {
  private _db: Database.Database;

  constructor(dbPath: string) {
    this._db = new Database(dbPath);
    this._db.pragma('journal_mode = WAL');
    this._initSchema();
  }

  // ---------------------------------------------------------------------------
  // Schema
  // ---------------------------------------------------------------------------

  private _initSchema(): void {
    this._db.exec(`
      CREATE TABLE IF NOT EXISTS embeddings (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        namespace TEXT NOT NULL,
        chunk_text TEXT NOT NULL,
        start_line INTEGER NOT NULL,
        end_line INTEGER NOT NULL,
        embedding BLOB NOT NULL,
        created_at TEXT NOT NULL DEFAULT (datetime('now')),
        content_hash TEXT NOT NULL
      );

      CREATE INDEX IF NOT EXISTS idx_embeddings_namespace
        ON embeddings (namespace);

      CREATE INDEX IF NOT EXISTS idx_embeddings_content_hash
        ON embeddings (namespace, content_hash);
    `);
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  /**
   * Upsert chunks for a namespace.
   * Deletes all existing embeddings for the namespace, then inserts new ones.
   *
   * @param chunks - The text chunks with metadata.
   * @param embeddings - Corresponding embedding vectors (one per chunk).
   * @param hash - Content hash for the entire namespace content.
   */
  upsertChunks(
    chunks: Chunk[],
    embeddings: number[][],
    hash: string,
  ): void {
    if (chunks.length !== embeddings.length) {
      throw new Error(`Chunks/embeddings length mismatch: ${chunks.length} vs ${embeddings.length}`);
    }

    if (chunks.length === 0) {return;}

    const namespace = chunks[0].namespace;

    const insertStmt = this._db.prepare(`
      INSERT INTO embeddings (namespace, chunk_text, start_line, end_line, embedding, content_hash)
      VALUES (?, ?, ?, ?, ?, ?)
    `);

    const deleteStmt = this._db.prepare(`
      DELETE FROM embeddings WHERE namespace = ?
    `);

    const transaction = this._db.transaction(() => {
      deleteStmt.run(namespace);
      for (let i = 0; i < chunks.length; i++) {
        const chunk = chunks[i];
        const embeddingBuffer = Buffer.from(new Float32Array(embeddings[i]).buffer);
        insertStmt.run(
          chunk.namespace,
          chunk.text,
          chunk.startLine,
          chunk.endLine,
          embeddingBuffer,
          hash,
        );
      }
    });

    transaction();
  }

  /**
   * Delete all embeddings for a namespace.
   */
  deleteNamespace(namespace: string): void {
    this._db.prepare('DELETE FROM embeddings WHERE namespace = ?').run(namespace);
  }

  /**
   * Check if a namespace already has embeddings with the given content hash.
   * Used to skip re-embedding unchanged content.
   */
  hasContentHash(namespace: string, hash: string): boolean {
    const row = this._db.prepare(
      'SELECT 1 FROM embeddings WHERE namespace = ? AND content_hash = ? LIMIT 1',
    ).get(namespace, hash) as { '1': number } | undefined;
    return row !== undefined;
  }

  /**
   * Search for the nearest vectors to the query embedding.
   * Uses brute-force cosine similarity (suitable for moderate dataset sizes).
   *
   * @param queryEmbedding - The query vector.
   * @param topK - Maximum number of results to return.
   * @returns Ranked results sorted by descending similarity score.
   */
  search(queryEmbedding: number[], topK: number): VectorSearchResult[] {
    const rows = this._db.prepare(
      'SELECT namespace, chunk_text, start_line, end_line, embedding FROM embeddings',
    ).all() as Pick<EmbeddingRow, 'namespace' | 'chunk_text' | 'start_line' | 'end_line' | 'embedding'>[];

    const queryArray = new Float32Array(queryEmbedding);

    const scored: VectorSearchResult[] = rows.map((row) => {
      const stored = new Float32Array(
        row.embedding.buffer,
        row.embedding.byteOffset,
        row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT,
      );

      return {
        namespace: row.namespace,
        chunkText: row.chunk_text,
        startLine: row.start_line,
        endLine: row.end_line,
        score: cosineSimilarity(queryArray, stored),
      };
    });

    // Sort by descending score and return top K
    scored.sort((a, b) => b.score - a.score);
    return scored.slice(0, topK);
  }

  /**
   * Get total number of stored embeddings.
   */
  count(): number {
    const row = this._db.prepare('SELECT COUNT(*) as cnt FROM embeddings').get() as { cnt: number };
    return row.cnt;
  }

  /**
   * Close the database connection.
   */
  close(): void {
    this._db.close();
  }
}
