#!/usr/bin/env node

/**
 * match-star.mjs — Zero-LLM, zero-browser ATS behavioural question matcher.
 *
 * Parses interview-prep/story-bank.md, scores each STAR story against the
 * question text (and optionally a JD file), and returns the top match(es)
 * formatted to ATS paste length (250–500 words).
 *
 * Usage:
 *   node match-star.mjs "Tell me about a time you led a project under pressure"
 *   node match-star.mjs "Describe a conflict you resolved" --jd jds/acme.md
 *   node match-star.mjs "Give an example of handling ambiguity" --top 2
 *   node match-star.mjs --list    # list all stories with their tags
 */

import { readFileSync, existsSync } from 'fs';

// ── Config ──────────────────────────────────────────────────────────

const STORY_BANK_PATH = 'interview-prep/story-bank.md';

const args       = process.argv.slice(2);
const LIST_MODE  = args.includes('--list');
const jdFlag     = args.indexOf('--jd');
const jdPath     = jdFlag !== -1 ? args[jdFlag + 1] : null;
const topFlag    = args.indexOf('--top');
const topRaw     = topFlag !== -1 ? parseInt(args[topFlag + 1], 10) : NaN;
const TOP_N      = Number.isInteger(topRaw) && topRaw > 0 ? topRaw : 1;
// Exclude flag operands by index position, not by value, to preserve repeated text in the question
const excludeIdx = new Set([
  ...(jdFlag  !== -1 ? [jdFlag + 1]  : []),
  ...(topFlag !== -1 ? [topFlag + 1] : []),
]);
const question = args
  .filter((a, i) => !a.startsWith('--') && !excludeIdx.has(i))
  .join(' ').trim();

// ── Parser ───────────────────────────────────────────────────────────

/**
 * Parse story-bank.md into structured story objects.
 * @param {string} content
 * @returns {Array<{title, theme, source, situation, task, action, result, reflection, tags}>}
 */
function parseStories(content) {
  const stories = [];
  // Split on story headings: ### [Theme] Title
  const blocks = content.split(/^### /m).slice(1);

  for (const block of blocks) {
    const lines  = block.trim().split('\n');
    const header = lines[0].trim();

    // Extract theme from [Theme] prefix if present
    const themeMatch = header.match(/^\[([^\]]+)\]\s*(.+)/);
    const theme = themeMatch ? themeMatch[1].trim() : '';
    const title = themeMatch ? themeMatch[2].trim() : header;

    const get = (label) => {
      const re  = new RegExp(`\\*\\*${label}:\\*\\*\\s*(.+)`);
      const hit = block.match(re);
      return hit ? hit[1].trim() : '';
    };

    const tagsRaw = get('Best for questions about');
    const tags    = tagsRaw
      ? tagsRaw.split(/[,;]/).map(t => t.trim().toLowerCase()).filter(Boolean)
      : [];

    if (!title || (!get('A \\(Action\\)') && !get('Action'))) continue; // skip template/empty blocks

    stories.push({
      title,
      theme,
      source:     get('Source'),
      situation:  get('S \\(Situation\\)') || get('Situation'),
      task:       get('T \\(Task\\)') || get('Task'),
      action:     get('A \\(Action\\)') || get('Action'),
      result:     get('R \\(Result\\)') || get('Result'),
      reflection: get('Reflection'),
      tags,
    });
  }

  return stories;
}

// ── Scoring ──────────────────────────────────────────────────────────

/**
 * Tokenize text into lowercase words, stripping punctuation.
 * @param {string} text
 * @returns {string[]}
 */
function tokenize(text) {
  return text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(Boolean);
}

const STOPWORDS = new Set([
  'a','an','the','and','or','but','in','on','at','to','for','of','with',
  'you','me','my','your','i','we','they','it','is','was','were','are',
  'be','been','have','had','has','do','did','does','tell','about','time',
  'when','how','give','example','describe','situation','where','what',
]);

/**
 * Score a story against a question + optional JD text.
 * Higher = better match.
 * @param {object} story
 * @param {string[]} queryTokens
 * @param {string[]} jdTokens
 * @returns {number}
 */
function score(story, queryTokens, jdTokens) {
  const signal = queryTokens.filter(t => !STOPWORDS.has(t));
  let s = 0;

  // Tag match: highest weight (tags are explicit "best for" labels)
  const tagText = story.tags.join(' ');
  for (const token of signal) {
    if (tagText.includes(token)) s += 3;
  }

  // Title/theme match
  const titleTokens = tokenize(story.title + ' ' + story.theme);
  for (const token of signal) {
    if (titleTokens.includes(token)) s += 2;
  }

  // Action + result match (the most substantive parts)
  const bodyTokens = tokenize(story.action + ' ' + story.result);
  for (const token of signal) {
    if (bodyTokens.includes(token)) s += 1;
  }

  // JD boost: stopword-filtered JD tokens matched against tokenized tags (exact token overlap)
  if (jdTokens.length > 0) {
    const jdSignal = new Set(jdTokens.filter(t => !STOPWORDS.has(t)));
    for (const tag of story.tags) {
      const tagTokens = tokenize(tag);
      if (tagTokens.some(t => jdSignal.has(t))) s += 2;
    }
  }

  return s;
}

// ── Formatter ────────────────────────────────────────────────────────

/**
 * Format a STAR story as ATS-ready prose (250–500 words).
 * @param {object} story
 * @param {string} question
 * @returns {string}
 */
function formatAts(story, question) {
  const parts = [];

  if (story.situation) parts.push(story.situation);
  if (story.task)      parts.push(story.task);
  if (story.action)    parts.push(story.action);
  if (story.result)    parts.push(story.result);
  if (story.reflection) parts.push(story.reflection);

  const wordArr = parts.filter(Boolean).join(' ').split(/\s+/).filter(Boolean);
  // Enforce 500-word ceiling; prose below 250 words gets a warning
  const prose   = wordArr.slice(0, 500).join(' ');
  const words   = Math.min(wordArr.length, 500);
  const notice  = wordArr.length < 250
    ? '\n   ⚠️  Under 250 words — consider expanding this story in story-bank.md.'
    : '';

  return [
    `— ${story.title}${story.theme ? ` [${story.theme}]` : ''}`,
    story.source ? `   Source: ${story.source}` : '',
    story.tags.length ? `   Tags: ${story.tags.join(', ')}` : '',
    '',
    prose,
    '',
    `   (~${words} words)${notice}`,
  ].filter(s => s !== null).join('\n');
}

// ── Main ─────────────────────────────────────────────────────────────

if (!existsSync(STORY_BANK_PATH)) {
  console.error(`Error: ${STORY_BANK_PATH} not found.`);
  console.error('Run /career-ops interview-prep on a role first to populate your story bank.');
  process.exit(1);
}

const content = readFileSync(STORY_BANK_PATH, 'utf-8');
const stories = parseStories(content);

if (stories.length === 0) {
  console.error('No stories found in story-bank.md yet.');
  console.error('Run /career-ops interview-prep on a role to add your first stories.');
  process.exit(1);
}

if (LIST_MODE) {
  console.log(`\nStory Bank — ${stories.length} stories\n${'─'.repeat(40)}`);
  stories.forEach((s, i) => {
    console.log(`${i + 1}. ${s.title}${s.theme ? ` [${s.theme}]` : ''}`);
    if (s.tags.length) console.log(`   Tags: ${s.tags.join(', ')}`);
    if (s.source)      console.log(`   ${s.source}`);
    console.log('');
  });
  process.exit(0);
}

if (!question) {
  console.error('Usage: node match-star.mjs "<behavioural question>" [--jd <file>] [--top <n>]');
  console.error('       node match-star.mjs --list');
  process.exit(1);
}

const queryTokens = tokenize(question);
let jdTokens = [];
if (jdPath) {
  if (!existsSync(jdPath)) {
    console.error(`Error: JD file not found at ${jdPath}`);
    process.exit(1);
  }
  jdTokens = tokenize(readFileSync(jdPath, 'utf-8'));
}

// Score and rank
const ranked = stories
  .map(s => ({ story: s, score: score(s, queryTokens, jdTokens) }))
  .sort((a, b) => b.score - a.score)
  .slice(0, TOP_N);

console.log(`\nATS Behavioural Question Matcher`);
console.log('─'.repeat(40));
console.log(`Question: "${question}"`);
if (jdPath) console.log(`JD:       ${jdPath}`);
console.log(`Stories:  ${stories.length} in bank\n`);

for (let i = 0; i < ranked.length; i++) {
  const { story, score: s } = ranked[i];
  console.log(`${'─'.repeat(40)}`);
  console.log(`Match ${i + 1} of ${ranked.length} (score: ${s})\n`);
  console.log(formatAts(story, question));
  console.log('');
}

if (ranked.length > 0 && ranked[0].score === 0) {
  console.log('⚠️  No strong match found. Consider adding a story to story-bank.md that covers this competency.');
}
