// Deterministic glossary extraction: surface candidate terms + definitions from
// a lecture transcript with NO model. Two complementary strategies:
//   (a) explicit definition patterns: "X is/are/means/refers to/is defined as Y"
//   (b) frequent Capitalized phrases / frequent content nouns as a fallback,
//       using their first containing sentence as a stand-in definition.
//
// IMPORTANT: relative imports only inside src/lib (vitest has no '@/*' alias).

import type { Segment } from '../types';
import { splitSentences, tokenizeWords, STOPWORDS } from './tokenize';

/** One glossary row: a term, a definition, and where it came from. */
export interface GlossaryEntry {
  term: string;
  definition: string;
  /** Start time (seconds) of the source segment. */
  start: number;
  /** Source segment id, when the segment had one. */
  segmentId?: string;
}

/** Internal accumulator while collecting candidates. */
interface Candidate extends GlossaryEntry {
  /** How "important"/frequent this candidate is, for ranking. */
  frequency: number;
  /** Whether it came from an explicit definition pattern (preferred). */
  explicit: boolean;
  /** Original discovery order, for stable tie-breaking. */
  order: number;
}

// "X is/are/means/refers to/is defined as Y" — capture the subject (term) and
// keep the whole sentence as the definition. The subject is the run of words
// before the connective; we cap its length so we don't swallow half a clause.
const DEFINITION_RE =
  /^(.{2,60}?)\s+(?:is|are|means|refers? to|is defined as)\s+(.+)$/i;

// A run of Capitalized words (proper-noun-ish phrase), e.g. "Hidden Markov Model".
const CAPITALIZED_PHRASE_RE = /\b([A-Z][a-z0-9]+(?:\s+[A-Z][a-z0-9]+)*)\b/g;

/**
 * Build a glossary of at most `max` entries from `segments`.
 *
 * Candidates are deduped by lowercased term (explicit definitions win over
 * fallbacks; otherwise the first/most-frequent occurrence is kept), then sorted
 * by frequency descending (explicit entries ranked ahead on ties), and capped.
 *
 * Empty input -> [].
 */
export function glossary(
  segments: Segment[],
  opts?: { max?: number },
): GlossaryEntry[] {
  const max = opts?.max ?? 12;
  if (segments.length === 0 || max <= 0) return [];

  // Document-wide content-word frequencies, used to rank fallback candidates.
  const wordFreq = new Map<string, number>();
  for (const seg of segments) {
    for (const tok of tokenizeWords(seg.text)) {
      wordFreq.set(tok, (wordFreq.get(tok) ?? 0) + 1);
    }
  }

  // Frequency of each Capitalized phrase (by lowercased key) across the doc.
  const phraseFreq = new Map<string, number>();
  for (const seg of segments) {
    for (const m of seg.text.matchAll(CAPITALIZED_PHRASE_RE)) {
      const phrase = m[1];
      if (!phrase) continue;
      const key = phrase.toLowerCase();
      phraseFreq.set(key, (phraseFreq.get(key) ?? 0) + 1);
    }
  }

  // Dedupe by lowercased term. Map preserves first-seen insertion order.
  const byTerm = new Map<string, Candidate>();
  let order = 0;

  const add = (cand: Omit<Candidate, 'order'>) => {
    const key = cand.term.toLowerCase().trim();
    if (key.length === 0) return;
    const existing = byTerm.get(key);
    if (!existing) {
      byTerm.set(key, { ...cand, order: order++ });
      return;
    }
    // An explicit definition supersedes a fallback for the same term. When both
    // are explicit, keep the more informative (longer) definition; this lets a
    // substantive "X is when ..." win over a throwaway "X is fun." mention.
    const upgrade =
      (cand.explicit && !existing.explicit) ||
      (cand.explicit === existing.explicit &&
        cand.definition.length > existing.definition.length);
    if (upgrade) {
      byTerm.set(key, { ...cand, order: existing.order });
    }
  };

  // Pass 1: explicit "X is Y" definitions (highest quality).
  for (const seg of segments) {
    for (const sentence of splitSentences(seg.text)) {
      const m = sentence.match(DEFINITION_RE);
      if (!m || !m[1]) continue;
      const term = m[1].trim();
      // Skip junk subjects (pure stopword / too short).
      const termWords = tokenizeWords(term);
      if (termWords.length === 0 && term.length < 3) continue;
      add({
        term,
        definition: sentence,
        start: seg.start,
        segmentId: seg.id,
        // Frequency from the phrase/word tables, boosted so explicit wins.
        frequency:
          (phraseFreq.get(term.toLowerCase()) ?? 0) +
          (wordFreq.get(term.toLowerCase()) ?? 0) +
          1,
        explicit: true,
      });
    }
  }

  // Pass 2a: frequent multi-word Capitalized phrases as fallback candidates.
  for (const seg of segments) {
    const sentences = splitSentences(seg.text);
    for (const [key, freq] of phraseFreq) {
      if (freq < 1) continue;
      // Only multi-word phrases here; single Capitalized words are noisy.
      if (!key.includes(' ')) continue;
      // First sentence in this segment that contains the phrase.
      const containing = sentences.find((s) => s.toLowerCase().includes(key));
      if (!containing) continue;
      // Reconstruct a Title-Cased display term from the key.
      const term = key.replace(/\b\w/g, (c) => c.toUpperCase());
      add({
        term,
        definition: containing,
        start: seg.start,
        segmentId: seg.id,
        frequency: freq,
        explicit: false,
      });
    }
  }

  // Pass 2b: frequent single content nouns as a last-resort fallback.
  for (const seg of segments) {
    const sentences = splitSentences(seg.text);
    for (const tok of tokenizeWords(seg.text)) {
      if (STOPWORDS.has(tok)) continue;
      const freq = wordFreq.get(tok) ?? 0;
      if (freq < 2) continue; // require some recurrence to qualify
      const containing = sentences.find((s) =>
        s.toLowerCase().includes(tok),
      );
      if (!containing) continue;
      add({
        term: tok,
        definition: containing,
        start: seg.start,
        segmentId: seg.id,
        frequency: freq,
        explicit: false,
      });
    }
  }

  // Rank: explicit first, then frequency desc, then discovery order (stable).
  const ranked = [...byTerm.values()].sort(
    (a, b) =>
      Number(b.explicit) - Number(a.explicit) ||
      b.frequency - a.frequency ||
      a.order - b.order,
  );

  return ranked.slice(0, max).map(({ term, definition, start, segmentId }) => ({
    term,
    definition,
    start,
    segmentId,
  }));
}
