/**
 * Deterministic keyword-based tag matching for prompt selection.
 *
 * Used as the no-LLM fallback (and for read_prompts in hybrid mode).
 * Scores prompts by tag overlap * priority, then fills a byte budget.
 */

export interface PromptIndexEntry {
  name: string;
  priority: number;
  summary: string | null;
  chapters: string[] | null;
  content: string;
}

export interface TagMatchResult {
  /** Prompts with full content included (within byte budget) */
  fullContent: PromptIndexEntry[];
  /** Matched prompts beyond byte budget — name + summary only */
  indexOnly: PromptIndexEntry[];
  /** Non-matched prompts — listed for awareness */
  remaining: PromptIndexEntry[];
}

const DEFAULT_BYTE_BUDGET = 8192;

export class TagMatcher {
  constructor(private readonly byteBudget: number = DEFAULT_BYTE_BUDGET) {}

  match(tags: string[], prompts: PromptIndexEntry[]): TagMatchResult {
    const lowerTags = tags.map((t) => t.toLowerCase());

    // Score each prompt
    const scored = prompts.map((p) => ({
      prompt: p,
      score: this.computeScore(lowerTags, p),
      matched: this.computeScore(lowerTags, p) > 0,
    }));

    // Partition: matched (score > 0) vs non-matched
    const matched = scored.filter((s) => s.matched).sort((a, b) => b.score - a.score);
    const nonMatched = scored.filter((s) => !s.matched).map((s) => s.prompt);

    // Fill byte budget from matched prompts
    let budgetRemaining = this.byteBudget;
    const fullContent: PromptIndexEntry[] = [];
    const indexOnly: PromptIndexEntry[] = [];

    for (const { prompt } of matched) {
      const contentBytes = Buffer.byteLength(prompt.content, 'utf-8');
      if (budgetRemaining >= contentBytes) {
        fullContent.push(prompt);
        budgetRemaining -= contentBytes;
      } else {
        indexOnly.push(prompt);
      }
    }

    return { fullContent, indexOnly, remaining: nonMatched };
  }

  private computeScore(lowerTags: string[], prompt: PromptIndexEntry): number {
    // Priority 10 always included at the top
    if (prompt.priority === 10) return Infinity;

    // Baseline score = priority (so all prompts compete for the byte budget)
    // Tag matches boost the score further (matchCount * priority on top)
    let boost = 0;
    if (lowerTags.length > 0) {
      const searchText = [
        prompt.name,
        prompt.summary ?? '',
        ...(prompt.chapters ?? []),
      ].join(' ').toLowerCase();

      for (const tag of lowerTags) {
        if (searchText.includes(tag)) boost++;
      }
      boost *= prompt.priority;
    }

    return prompt.priority + boost;
  }
}

const STOP_WORDS = new Set([
  'the', 'a', 'an', 'is', 'to', 'for', 'of', 'and', 'or', 'in', 'on', 'at',
  'by', 'with', 'from', 'this', 'that', 'it', 'its', 'as', 'be', 'are', 'was',
  'were', 'been', 'has', 'have', 'had', 'do', 'does', 'did', 'but', 'not',
  'can', 'will', 'would', 'could', 'should', 'may', 'might', 'shall', 'must',
  'so', 'if', 'then', 'than', 'too', 'very', 'just', 'about', 'up', 'out',
  'no', 'yes', 'all', 'any', 'some', 'my', 'your', 'our', 'their', 'what',
  'which', 'who', 'how', 'when', 'where', 'why', 'want', 'need', 'get', 'set',
  'use', 'like', 'make', 'know', 'help', 'try',
]);

/**
 * Convert a natural-language description into keyword tags.
 * Splits on whitespace/punctuation, lowercases, filters stop words and short words, caps at 10.
 */
export function tokenizeDescription(description: string): string[] {
  const words = description
    .toLowerCase()
    .split(/[\s.,;:!?'"()\[\]{}<>|/\\@#$%^&*+=~`]+/)
    .map((w) => w.replace(/[^a-z0-9-]/g, ''))
    .filter((w) => w.length >= 3 && !STOP_WORDS.has(w));

  // Deduplicate while preserving order
  const seen = new Set<string>();
  const unique: string[] = [];
  for (const w of words) {
    if (!seen.has(w)) {
      seen.add(w);
      unique.push(w);
    }
  }
  return unique.slice(0, 10);
}

/**
 * Extract keywords from a tool call for the intercept fallback path.
 * Pulls words from the tool name and string argument values.
 */
export function extractKeywordsFromToolCall(
  toolName: string,
  args: Record<string, unknown>,
): string[] {
  const keywords = new Set<string>();

  // Tool name parts (split on / and -)
  for (const part of toolName.split(/[/-]/)) {
    if (part.length > 2) keywords.add(part.toLowerCase());
  }

  // String argument values — extract words
  for (const value of Object.values(args)) {
    if (typeof value === 'string' && value.length < 200) {
      for (const word of value.split(/\s+/)) {
        const clean = word.replace(/[^a-zA-Z0-9-]/g, '').toLowerCase();
        if (clean.length > 2) keywords.add(clean);
      }
    }
  }

  return [...keywords].slice(0, 10); // Cap at 10 keywords
}
