/**
 * GFS RAG Vector Store
 * 
 * In-memory vector store using BM25-style term matching + cosine similarity.
 * Supports document ingestion, chunking, and retrieval.
 * 
 * Designed to be upgraded to a persistent vector DB (Pinecone, pgvector)
 * when scale demands it — the interface is the same.
 */

import { RAGChunk, RAGSearchResult, RAGConfig, DEFAULT_RAG_CONFIG, SENSITIVE_CATEGORIES } from './types';

class VectorStore {
  private chunks: RAGChunk[] = [];
  private initialized = false;

  /**
   * Add chunks to the store.
   */
  addChunks(chunks: RAGChunk[]): void {
    for (const chunk of chunks) {
      // Deduplicate by ID
      const existingIndex = this.chunks.findIndex(c => c.id === chunk.id);
      if (existingIndex >= 0) {
        this.chunks[existingIndex] = chunk;
      } else {
        this.chunks.push(chunk);
      }
    }
    this.initialized = true;
  }

  /**
   * Search for relevant chunks using BM25-style term matching.
   * 
   * Safety: Filters out chunks that could enable data deletion
   * or expose raw PII (Standing Order 5.2).
   */
  search(
    query: string,
    config: Partial<RAGConfig> = {}
  ): RAGSearchResult[] {
    const cfg = { ...DEFAULT_RAG_CONFIG, ...config };

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

    // Build query term vector
    const queryTerms = query
      .toLowerCase()
      .replace(/[^a-z0-9\s-]/g, ' ')
      .split(/\s+/)
      .filter(w => w.length > 2);

    if (queryTerms.length === 0) return [];

    // Score each chunk
    const scored: RAGSearchResult[] = [];

    for (const chunk of this.chunks) {
      // Skip excluded categories
      if (cfg.excludeCategories?.includes(chunk.category)) continue;

      // === SAFETY GUARDRAIL ===
      // Never return chunks that contain deletion instructions or raw PII patterns
      if (containsDangerousContent(chunk.content)) continue;

      let score = 0;
      const matchedTerms: string[] = [];

      // BM25-style scoring
      for (const term of queryTerms) {
        const tf = chunk.termVector[term] || 0;
        if (tf > 0) {
          // IDF approximation: rarer terms score higher
          const df = this.countDocumentFrequency(term);
          const idf = Math.log(1 + (this.chunks.length - df + 0.5) / (df + 0.5));
          // BM25 with k1=1.2, b=0.75
          const docLen = Object.values(chunk.termVector).reduce((a, b) => a + b, 0);
          const avgLen = this.getAverageDocLength();
          const bm25 = idf * ((tf * 2.2) / (tf + 1.2 * (0.25 + 0.75 * docLen / avgLen)));
          score += bm25;
          matchedTerms.push(term);
        }
      }

      // Boost for tag matches
      for (const term of queryTerms) {
        if (chunk.tags.some(t => t.toLowerCase().includes(term))) {
          score *= 1.3;
        }
      }

      // Boost for priority categories
      if (cfg.boostCategories?.includes(chunk.category)) {
        score *= 1.5;
      }

      // Normalize score to 0-1 range (approximate)
      const normalizedScore = Math.min(score / (queryTerms.length * 3), 1);

      if (normalizedScore >= cfg.minScore && matchedTerms.length > 0) {
        scored.push({
          chunk,
          score: normalizedScore,
          matchReason: `Matched terms: ${matchedTerms.join(', ')}`,
        });
      }
    }

    // Sort by score descending, take top N
    return scored
      .sort((a, b) => b.score - a.score)
      .slice(0, cfg.maxChunks);
  }

  /**
   * Get store statistics.
   */
  getStats(): {
    totalChunks: number;
    initialized: boolean;
    categories: Record<string, number>;
    documents: number;
  } {
    const categories: Record<string, number> = {};
    const docIds = new Set<string>();

    for (const chunk of this.chunks) {
      categories[chunk.category] = (categories[chunk.category] || 0) + 1;
      docIds.add(chunk.documentId);
    }

    return {
      totalChunks: this.chunks.length,
      initialized: this.initialized,
      categories,
      documents: docIds.size,
    };
  }

  /**
   * Clear the store (for testing).
   */
  clear(): void {
    this.chunks = [];
    this.initialized = false;
  }

  // === Private Helpers ===

  private countDocumentFrequency(term: string): number {
    return this.chunks.filter(c => (c.termVector[term] || 0) > 0).length;
  }

  private getAverageDocLength(): number {
    if (this.chunks.length === 0) return 1;
    const total = this.chunks.reduce(
      (sum, c) => sum + Object.values(c.termVector).reduce((a, b) => a + b, 0),
      0
    );
    return total / this.chunks.length;
  }
}

/**
 * Safety guardrail: detect dangerous content that should never be
 * returned to an agent (Standing Order 5.1, 5.2).
 */
function containsDangerousContent(content: string): boolean {
  const lower = content.toLowerCase();
  const dangerousPatterns = [
    /drop\s+table/i,
    /delete\s+from\s+.*where\s+1\s*=\s*1/i,
    /truncate\s+table/i,
    /rm\s+-rf/i,
    /password["']?\s*[:=]\s*["'][^"']{4,}/i,
    /\b(ssn|social.security)\s*[:=]\s*\d/i,
  ];

  return dangerousPatterns.some(p => p.test(lower));
}

// Singleton instance
export const vectorStore = new VectorStore();
