/**
 * Recall scoring — candidate collection and fixed scoring.
 *
 * This module collects candidates from the four retrieval paths, merges them
 * by URI, and applies the single supported raw_plus_lex_damp scoring algorithm.
 */

import { buildCandidateKey, extractCueTerms, getViewPrior } from '../view/memoryViewQueries';
import { scoreRawPlusLexDamp } from './scoringStrategies';

export {
  DEFAULT_STRATEGY,
  type StrategyName,
} from './scoringStrategies';

// ─── Types ──────────────────────────────────────────────────────────

export interface ScoringCandidate {
  uri: string;
  priority: number;
  disclosure: string;
  exact_score: number;
  glossary_semantic_score: number;
  dense_score: number;
  lexical_score: number;
  view_bonus: number;
  matched_on: Set<string>;
  cues: Set<string>;
  view_types: Set<string>;
  semantic_views: Set<string>;
  lexical_views: Set<string>;
  updated_at: Date | null;
}

export interface ScoredResult {
  uri: string;
  score: number;
  exact_score: number;
  glossary_semantic_score: number;
  dense_score: number;
  lexical_score: number;
  score_breakdown: Record<string, number | string>;
  cues: string[];
  matched_on: string[];
  priority: number;
  memory_age_days?: number;
  recency_bonus?: number;
}

export interface ScoringConfig {
  strategy?: string;
  w_exact: number;
  w_glossary_semantic: number;
  w_dense: number;
  w_lexical: number;
  priority_base: number;
  priority_step: number;
  multi_view_step: number;
  multi_view_cap: number;
  query_tokens?: number;
  recency_enabled?: boolean;
  recency_half_life_days?: number;
  recency_max_bonus?: number;
  recency_priority_exempt?: number;
  view_priors?: Record<string, number> | null;
  [key: string]: unknown;
}

export interface CandidateRows {
  exactRows: Record<string, unknown>[];
  glossarySemanticRows: Record<string, unknown>[];
  denseRows: Record<string, unknown>[];
  lexicalRows: Record<string, unknown>[];
}

// ─── Candidate collection ───────────────────────────────────────────

export function collectCandidates(
  { exactRows, glossarySemanticRows, denseRows, lexicalRows }: CandidateRows,
  { viewPriors = null }: { viewPriors?: Record<string, number> | null } = {},
): Map<string, ScoringCandidate> {
  const byUri = new Map<string, ScoringCandidate>();
  const viewPriorOf = (vt: string): number => {
    if (viewPriors) return Number(viewPriors[vt] || 0);
    return getViewPrior(vt);
  };

  function getOrCreate(row: Record<string, unknown>): ScoringCandidate | null {
    const key = buildCandidateKey(row);
    if (!key) return null;
    const existing = byUri.get(key);
    if (existing) return existing;
    const created: ScoringCandidate = {
      uri: row.uri as string,
      priority: Number(row.priority || 0),
      disclosure: (row.disclosure as string) || '',
      exact_score: 0,
      glossary_semantic_score: 0,
      dense_score: 0,
      lexical_score: 0,
      view_bonus: 0,
      matched_on: new Set(),
      cues: new Set(),
      view_types: new Set(),
      semantic_views: new Set(),
      lexical_views: new Set(),
      updated_at: null,
    };
    byUri.set(key, created);
    return created;
  }

  function mergeTimestamp(item: ScoringCandidate, row: Record<string, unknown>): void {
    if (row.updated_at) {
      const ts = new Date(row.updated_at as string);
      if (!item.updated_at || ts > item.updated_at) item.updated_at = ts;
    }
  }

  for (const row of exactRows) {
    const item = getOrCreate(row);
    if (!item) continue;
    const s = Number(row.exact_score || 0) * Number(row.weight || 1);
    item.exact_score = Math.max(item.exact_score, s);
    item.matched_on.add('exact');
    if (row.glossary_exact_hit) item.matched_on.add('glossary');
    if (row.glossary_text_hit) item.matched_on.add('glossary_text');
    if (row.query_contains_glossary_hit) item.matched_on.add('query_contains_glossary');
    if (row.glossary_fts_hit) item.matched_on.add('glossary_fts');
    if (row.path_exact_hit) item.matched_on.add('path');
    for (const cue of extractCueTerms(row)) item.cues.add(cue);
    mergeTimestamp(item, row);
  }
  for (const row of glossarySemanticRows) {
    const item = getOrCreate(row);
    if (!item) continue;
    item.glossary_semantic_score = Math.max(item.glossary_semantic_score, Number(row.glossary_semantic_score || 0));
    item.matched_on.add('glossary_semantic');
    if (row.keyword) item.cues.add(row.keyword as string);
    mergeTimestamp(item, row);
  }
  for (const row of denseRows) {
    const item = getOrCreate(row);
    if (!item) continue;
    const s = Number(row.semantic_score || 0) * Number(row.weight || 1);
    item.dense_score = Math.max(item.dense_score, s);
    item.view_bonus = Math.max(item.view_bonus, viewPriorOf(row.view_type as string));
    item.matched_on.add('dense');
    item.view_types.add(row.view_type as string);
    item.semantic_views.add(row.view_type as string);
    for (const cue of extractCueTerms(row)) item.cues.add(cue);
    mergeTimestamp(item, row);
  }
  for (const row of lexicalRows) {
    const item = getOrCreate(row);
    if (!item) continue;
    const s = Number(row.lexical_score || 0) * Number(row.weight || 1);
    item.lexical_score = Math.max(item.lexical_score, s);
    item.view_bonus = Math.max(item.view_bonus, viewPriorOf(row.view_type as string));
    item.matched_on.add('lexical');
    item.view_types.add(row.view_type as string);
    item.lexical_views.add(row.view_type as string);
    if (row.fts_hit) item.matched_on.add('fts');
    if (row.text_hit) item.matched_on.add('text');
    if (row.uri_hit) item.matched_on.add('uri');
    for (const cue of extractCueTerms(row)) item.cues.add(cue);
    mergeTimestamp(item, row);
  }

  return byUri;
}

// ─── Router ──────────────────────────────────────────────────────────

export function runStrategy(
  byUri: Map<string, ScoringCandidate>,
  config: ScoringConfig,
): ScoredResult[] {
  return scoreRawPlusLexDamp(byUri, config);
}
