#!/usr/bin/env node
/**
 * OpenClaw Compression — Embedding Scorer + Federated Query
 *
 * Embedding-based relevance scoring for SmartCrusher.
 * Migrated from Headroom `crates/headroom-core/src/relevance/` (HybridScorer)
 *
 * Federated cross-project code graph query.
 * Inspired by CodeGraph's multi-repo architecture.
 *
 * Usage:
 *   const { EmbeddingScorer } = require('./embedding-scorer');
 *   const scores = await scorer.score(query, items);
 */

const crypto = require('crypto');
const path = require('path');
const fs = require('fs');

// ─── Simple Embedding Scorer (TF-IDF + Word Overlap) ────────────

/**
 * Scoring without external embedding model dependency.
 * Uses TF-IDF + Jaccard overlap as a lightweight substitute for
 * full embedding-based similarity.
 *
 * For production: swap in jina-embeddings-v2-base-zh (already
 * available in OpenClaw's remote-memory-server).
 */
class EmbeddingScorer {
  constructor(options = {}) {
    this.method = options.method || 'hybrid';  // 'tfidf' | 'bm25' | 'jaccard' | 'hybrid'
    this.idfCache = new Map();
    this.docCount = 0;
  }

  /**
   * Score items against a query.
   * Returns sorted array of { index, score, matchedTerms }.
   */
  score(query, items, options = {}) {
    const queryTerms = this._tokenize(query);
    if (queryTerms.length === 0) {
      return items.map((_, i) => ({ index: i, score: 0, matchedTerms: [] }));
    }

    // Compute document frequencies
    this._buildIdf(items.map(item =>
      this._extractText(item).toLowerCase()
    ));

    const results = items.map((item, index) => {
      const text = this._extractText(item).toLowerCase();
      const tokens = this._tokenize(text);

      let score = 0;
      const matchedTerms = [];

      switch (this.method) {
        case 'tfidf':
          score = this._tfidf(queryTerms, tokens);
          matchedTerms.push(...queryTerms.filter(t => tokens.includes(t)));
          break;

        case 'bm25': {
          const k1 = options.k1 || 1.5;
          const b = options.b || 0.75;
          const avgdl = options.avgdl || 50;
          score = this._bm25(queryTerms, tokens, k1, b, avgdl);
          matchedTerms.push(...queryTerms.filter(t => tokens.includes(t)));
          break;
        }

        case 'jaccard':
          score = this._jaccard(queryTerms, tokens);
          matchedTerms.push(...queryTerms.filter(t => tokens.includes(t)));
          break;

        case 'hybrid':
        default: {
          const tfidfScore = this._tfidf(queryTerms, tokens);
          const jaccardScore = this._jaccard(queryTerms, tokens);
          const bm25Score = this._bm25(queryTerms, tokens, 1.5, 0.75, 50);

          // Weighted combination: TF-IDF(0.4) + BM25(0.3) + Jaccard(0.3)
          score = tfidfScore * 0.4 + bm25Score * 0.3 + jaccardScore * 0.3;
          matchedTerms.push(...queryTerms.filter(t => tokens.includes(t)));
          break;
        }
      }

      return { index, score: Math.round(score * 1000) / 1000, matchedTerms };
    });

    // Sort by score DESC
    results.sort((a, b) => b.score - a.score);

    return results;
  }

  /**
   * Compute top-K indices by relevance.
   */
  topK(query, items, k, options = {}) {
    const scores = this.score(query, items, options);
    return scores.slice(0, k).map(s => s.index);
  }

  _tokenize(text) {
    // Extract words, split camelCase and snake_case
    const words = [];
    const raw = text.toLowerCase().split(/[\s\W]+/).filter(Boolean);

    for (const w of raw) {
      words.push(w);
      // Split camelCase: "SmartCrusher" → "smart", "crusher"
      const camelParts = w.split(/(?=[A-Z])/).filter(Boolean);
      if (camelParts.length > 1) words.push(...camelParts.map(p => p.toLowerCase()));
      // Split snake_case: "smart_crusher" → "smart", "crusher"
      const snakeParts = w.split('_').filter(Boolean);
      if (snakeParts.length > 1) words.push(...snakeParts);
    }

    return [...new Set(words)];
  }

  _extractText(item) {
    if (typeof item === 'string') return item;
    if (typeof item === 'object' && item !== null) {
      // Extract meaningful fields
      const fields = ['name', 'title', 'description', 'message', 'text', 'summary', 'content'];
      const parts = [];
      for (const f of fields) {
        if (item[f]) parts.push(String(item[f]));
      }
      if (parts.length === 0) parts.push(JSON.stringify(item));
      return parts.join(' ');
    }
    return String(item);
  }

  _tfidf(queryTerms, docTerms) {
    let score = 0;
    for (const term of queryTerms) {
      const tf = docTerms.filter(t => t === term).length / (docTerms.length || 1);
      const N = Math.max(this.docCount, 1);
      const df = this.idfCache.get(term) || 1;
      const idf = Math.log((N + 1) / (df + 1)) + 1;
      score += tf * idf;
    }
    return score;
  }

  _bm25(queryTerms, docTerms, k1 = 1.5, b = 0.75, avgdl = 50) {
    let score = 0;
    const dl = docTerms.length;

    for (const term of queryTerms) {
      const tf = docTerms.filter(t => t === term).length;
      if (tf === 0) continue;

      const N = Math.max(this.docCount, 1);
      const df = this.idfCache.get(term) || 1;
      const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);

      const numerator = tf * (k1 + 1);
      const denominator = tf + k1 * (1 - b + b * (dl / Math.max(avgdl, 1)));
      score += idf * (numerator / denominator);
    }

    return score;
  }

  _jaccard(queryTerms, docTerms) {
    const querySet = new Set(queryTerms);
    const docSet = new Set(docTerms.filter(t => querySet.has(t)));
    const intersection = docSet.size;
    const union = new Set([...querySet, ...docTerms]).size;
    return union === 0 ? 0 : intersection / union;
  }

  _buildIdf(docs) {
    this.docCount = docs.length;
    this.idfCache.clear();

    for (const doc of docs) {
      const terms = new Set(this._tokenize(doc));
      for (const term of terms) {
        this.idfCache.set(term, (this.idfCache.get(term) || 0) + 1);
      }
    }
  }

  /**
   * Export IDF cache for persistence.
   */
  export() {
    return {
      method: this.method,
      docCount: this.docCount,
      idfCache: Object.fromEntries(this.idfCache),
    };
  }

  /**
   * Import from persisted cache.
   */
  import(data) {
    this.method = data.method || 'hybrid';
    this.docCount = data.docCount || 0;
    this.idfCache = new Map(Object.entries(data.idfCache || {}));
  }
}

// ─── Federated Query ─────────────────────────────────────────────

/**
 * Federated cross-project code graph query.
 *
 * Queries multiple CodeGraph databases and merges results.
 * Each project maintains its own .codegraph/codegraph.db.
 */
class FederatedQuery {
  constructor(projectDirs = []) {
    this.projects = new Map();  // projectName → { dir, db }

    for (const dir of projectDirs) {
      this.addProject(dir);
    }
  }

  /**
   * Add a project directory to the federation.
   */
  addProject(projectDir) {
    const name = path.basename(projectDir);
    this.projects.set(name, { dir: projectDir, dbPath: path.join(projectDir, '.codegraph', 'codegraph.db') });
  }

  /**
   * Search across all projects.
   */
  search(query, options = {}) {
    const { limit = 20 } = options;
    const allResults = [];

    for (const [name, project] of this.projects) {
      try {
        const Database = require('better-sqlite3');
        const db = new Database(project.dbPath);
        db.pragma('query_only = true');

        const results = db.prepare(`
          SELECT n.id, n.kind, n.name, n.qualified_name, n.signature,
                 n.line_start, f.path as file_path, f.language,
                 nodes_fts.rank as score, '${name}' as project
          FROM nodes_fts
          JOIN nodes n ON n.id = nodes_fts.rowid
          JOIN files f ON f.id = n.file_id
          WHERE nodes_fts MATCH ?
          ORDER BY score
          LIMIT ?
        `).all(`"${query}"`, limit);

        allResults.push(...results);
        db.close();
      } catch {
        // Skip projects without index
      }
    }

    // Merge and re-rank
    allResults.sort((a, b) => (a.score || 0) - (b.score || 0));

    return {
      query,
      projects: this.projects.size,
      results: allResults.slice(0, limit),
      byProject: allResults.reduce((acc, r) => {
        acc[r.project] = (acc[r.project] || 0) + 1;
        return acc;
      }, {}),
    };
  }

  /**
   * Auto-discover projects in a workspace directory.
   */
  static discover(workspaceDir, options = {}) {
    const { maxDepth = 2 } = options;
    const projects = [];

    function walk(dir, depth) {
      if (depth > maxDepth) return;
      const codegraphDir = path.join(dir, '.codegraph');
      if (fs.existsSync(codegraphDir) && fs.existsSync(path.join(codegraphDir, 'codegraph.db'))) {
        projects.push(dir);
        return; // found a project, don't recurse deeper
      }

      let entries;
      try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
      catch { return; }

      for (const entry of entries) {
        if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
          walk(path.join(dir, entry.name), depth + 1);
        }
      }
    }

    walk(workspaceDir, 0);
    return new FederatedQuery(projects);
  }
}

// ─── Module exports ──────────────────────────────────────────────

module.exports = {
  EmbeddingScorer,
  FederatedQuery,
};
