/**
 * Simple Reranker
 *
 * Pure logic-based reranking without ML dependencies.
 * Uses text matching, graph metrics, session data, and longevity.
 */

import type { ContextItem, ScoreBreakdown, ScoredContextItem } from './types.js';
import { getLongevityScore, recordRerankedItems } from './longevity.js';
import type { KuzuConnection } from '../graph/connection.js';

/**
 * Reranker weights for score components
 */
const WEIGHTS = {
  base: 0.15,           // Tool-based relevance (already computed)
  text: 0.35,           // Query-entity text matching
  graph: 0.10,          // PageRank/centrality
  session: 0.10,        // Recency and touch frequency
  neighbor: 0.05,       // Neighbor match boost
  longevity: 0.15,      // Persistence signal
  fileCooccurrence: 0.10, // Multiple entities from same file
};

/**
 * Tokenize text for matching
 * Handles camelCase, snake_case, and kebab-case
 */
function tokenize(text: string): Set<string> {
  const tokens = new Set<string>();

  // Split on common separators
  const parts = text.split(/[\s._\-/\\:]+/);

  for (const part of parts) {
    if (!part) continue;

    // Add the part as-is (lowercase)
    tokens.add(part.toLowerCase());

    // Split camelCase
    const camelParts = part.split(/(?=[A-Z])/);
    for (const cp of camelParts) {
      if (cp) tokens.add(cp.toLowerCase());
    }
  }

  return tokens;
}

/**
 * Calculate text match score between query and item
 */
function textMatchScore(queryTokens: Set<string>, item: ContextItem): number {
  let score = 0;

  // Tokenize entity name
  const nameTokens = tokenize(item.qualifiedName);

  // Name matches (50% of text score)
  let nameMatches = 0;
  for (const qt of queryTokens) {
    for (const nt of nameTokens) {
      if (nt.includes(qt) || qt.includes(nt)) {
        nameMatches++;
        break;
      }
    }
  }
  const nameScore = Math.min(nameMatches / Math.max(queryTokens.size, 1), 1) * 0.5;
  score += nameScore;

  // Exact substring matches in qualified name (+10% per match, capped at 30%)
  let substringMatches = 0;
  const lowerQualifiedName = item.qualifiedName.toLowerCase();
  for (const qt of queryTokens) {
    if (lowerQualifiedName.includes(qt)) {
      substringMatches++;
    }
  }
  score += Math.min(substringMatches * 0.1, 0.3);

  // Path matches (15% of text score)
  if (item.filePath) {
    const pathTokens = tokenize(item.filePath);
    let pathMatches = 0;
    for (const qt of queryTokens) {
      for (const pt of pathTokens) {
        if (pt.includes(qt) || qt.includes(pt)) {
          pathMatches++;
          break;
        }
      }
    }
    score += Math.min(pathMatches / Math.max(queryTokens.size, 1), 1) * 0.15;
  }

  // Description matches (5% of text score)
  if (item.description) {
    const descTokens = tokenize(item.description);
    let descMatches = 0;
    for (const qt of queryTokens) {
      for (const dt of descTokens) {
        if (dt.includes(qt) || qt.includes(dt)) {
          descMatches++;
          break;
        }
      }
    }
    score += Math.min(descMatches / Math.max(queryTokens.size, 1), 1) * 0.05;
  }

  return Math.min(score, 1);
}

/**
 * Calculate session boost based on recency and frequency
 */
function sessionBoostScore(item: ContextItem): number {
  // Recency: exponential decay (half-life = 48 hours)
  const hoursSinceTouch = (Date.now() - item.lastTouched.getTime()) / (1000 * 60 * 60);
  const halfLife = 48;
  const recencyScore = Math.exp(-0.693 * hoursSinceTouch / halfLife);

  // Frequency: logarithmic (touch_count=10 → ~0.7)
  const frequencyScore = Math.min(Math.log(item.touchCount + 1) / Math.log(15), 1);

  // Combine: recency * 0.3 + frequency * 0.7
  return recencyScore * 0.3 + frequencyScore * 0.7;
}

/**
 * Calculate neighbor match boost
 */
function neighborBoostScore(queryTokens: Set<string>, item: ContextItem): number {
  if (!item.neighbors || item.neighbors.length === 0) {
    return 0;
  }

  let matches = 0;
  for (const neighbor of item.neighbors) {
    const neighborTokens = tokenize(neighbor);
    for (const qt of queryTokens) {
      for (const nt of neighborTokens) {
        if (nt.includes(qt) || qt.includes(nt)) {
          matches++;
          break;
        }
      }
      if (matches >= 3) break; // Cap at 3 matches
    }
    if (matches >= 3) break;
  }

  return matches * 0.1; // 0.1 per match, max 0.3
}

/**
 * Calculate file co-occurrence boost
 */
function fileCooccurrenceBoost(
  item: ContextItem,
  fileEntityCounts: Map<string, number>
): number {
  if (!item.filePath) return 0;

  const count = fileEntityCounts.get(item.filePath) ?? 1;

  // 1 entity = 0.0, 2 = 0.3, 3 = 0.5, 5+ = 0.7
  if (count <= 1) return 0;
  if (count === 2) return 0.3;
  if (count <= 4) return 0.5;
  return 0.7;
}

/**
 * Calculate graph boost score (simplified without actual graph queries)
 *
 * In a full implementation, this would query Kuzu for pagerank/centrality.
 * For now, we use heuristics based on entity type and name patterns.
 */
function graphBoostScore(item: ContextItem, _conn?: KuzuConnection): number {
  // Heuristic: main/index/app files are likely important
  const importantPatterns = ['main', 'index', 'app', 'server', 'client', 'core'];
  const lowerName = item.qualifiedName.toLowerCase();

  for (const pattern of importantPatterns) {
    if (lowerName.includes(pattern)) {
      return 0.5;
    }
  }

  // Classes slightly more important than functions on average
  if (item.entityType === 'Class') {
    return 0.3;
  }

  // Default
  return 0.1;
}

/**
 * Score a single context item
 */
function scoreItem(
  item: ContextItem,
  queryTokens: Set<string>,
  fileEntityCounts: Map<string, number>,
  conn?: KuzuConnection
): ScoreBreakdown {
  const base = item.score;
  const text = textMatchScore(queryTokens, item);
  const graph = graphBoostScore(item, conn);
  const session = sessionBoostScore(item);
  const neighbor = neighborBoostScore(queryTokens, item);
  const longevity = getLongevityScore(item.qualifiedName);
  const fileCooccurrence = fileCooccurrenceBoost(item, fileEntityCounts);

  const total =
    base * WEIGHTS.base +
    text * WEIGHTS.text +
    graph * WEIGHTS.graph +
    session * WEIGHTS.session +
    neighbor * WEIGHTS.neighbor +
    longevity * WEIGHTS.longevity +
    fileCooccurrence * WEIGHTS.fileCooccurrence;

  return {
    base,
    text,
    graph,
    session,
    neighbor,
    longevity,
    fileCooccurrence,
    total,
  };
}

/**
 * Build file entity count map
 */
function buildFileEntityCounts(items: ContextItem[]): Map<string, number> {
  const counts = new Map<string, number>();
  for (const item of items) {
    if (item.filePath) {
      counts.set(item.filePath, (counts.get(item.filePath) ?? 0) + 1);
    }
  }
  return counts;
}

/**
 * Rerank context items by query relevance
 */
export function rerankContextItems(
  items: ContextItem[],
  query: string,
  topK: number = 20,
  conn?: KuzuConnection
): ContextItem[] {
  if (items.length === 0) return [];
  if (!query.trim()) return items.slice(0, topK);

  const queryTokens = tokenize(query);
  const fileEntityCounts = buildFileEntityCounts(items);

  // Score all items
  const scored: ScoredContextItem[] = items.map((item) => ({
    item,
    breakdown: scoreItem(item, queryTokens, fileEntityCounts, conn),
  }));

  // Sort by total score descending
  scored.sort((a, b) => b.breakdown.total - a.breakdown.total);

  // Take top K
  const result = scored.slice(0, topK).map((s) => s.item);

  // Record in longevity tracker
  recordRerankedItems(result.map((r) => r.qualifiedName));

  return result;
}

/**
 * Get detailed rerank scores for debugging
 */
export function getRerankScores(
  items: ContextItem[],
  query: string,
  conn?: KuzuConnection
): ScoredContextItem[] {
  if (items.length === 0) return [];

  const queryTokens = tokenize(query);
  const fileEntityCounts = buildFileEntityCounts(items);

  const scored: ScoredContextItem[] = items.map((item) => ({
    item,
    breakdown: scoreItem(item, queryTokens, fileEntityCounts, conn),
  }));

  scored.sort((a, b) => b.breakdown.total - a.breakdown.total);

  return scored;
}

/**
 * Rerank options
 */
export interface RerankOptions {
  /** Query to rank by */
  query: string;
  /** Number of top items to return */
  topK?: number;
  /** Kuzu connection for graph metrics */
  conn?: KuzuConnection;
}

/**
 * Convenience function for reranking
 */
export function rerank(items: ContextItem[], options: RerankOptions): ContextItem[] {
  return rerankContextItems(items, options.query, options.topK ?? 20, options.conn);
}
