// src/lib/mcp/spec-search.ts — recherche full-text déterministe sur la spec
// clonée, pour le tool MCP `search_spec_topics` (parité avec le `search` du MCP
// officiel de specification.website). Index en mémoire construit paresseusement
// une fois par process depuis CHECKLIST + les pages clonées (~156 × ~6 Ko) —
// aucune DB, aucun accès réseau, résultats stables pour une même requête.
import "server-only";
import { CHECKLIST } from "@/lib/spec/checklist-data";
import { getSpecPage } from "@/lib/spec/spec-content";
import type { SpecCategoryId, SpecStatus } from "@/lib/spec/types";

export type SpecSearchHit = {
  /** `${category}/${slug}` */
  id: string;
  title: string;
  status: SpecStatus;
  category: SpecCategoryId;
  summary: string;
  /** URL canonique upstream (référence). */
  url: string;
  /** Route interne de la page clonée. */
  internalPath: string;
  score: number;
  /** Extraits du corps autour des premières occurrences (≤ 2). */
  excerpts: string[];
};

type IndexedTopic = {
  hit: Omit<SpecSearchHit, "score" | "excerpts">;
  /** Champs pré-lowercasés pour le scoring. */
  title: string;
  slug: string;
  summary: string;
  body: string;
  /** Corps original (casse préservée) pour les extraits. */
  rawBody: string;
};

const DEFAULT_LIMIT = 5;
const MAX_LIMIT = 25;
const EXCERPT_RADIUS = 45; // ~90 chars par fenêtre
const MAX_EXCERPTS = 2;

// Pondérations (parité upstream : titre > slug > résumé > corps).
const W_TITLE = 8;
const W_SLUG = 6;
const W_SUMMARY = 4;
const W_BODY = 1;

let INDEX: IndexedTopic[] | null = null;

function buildIndex(): IndexedTopic[] {
  const out: IndexedTopic[] = [];
  for (const cat of CHECKLIST) {
    for (const item of cat.items) {
      const slug = item.id.slice(item.id.indexOf("/") + 1);
      const page = getSpecPage(cat.id, slug);
      const summary = page?.frontmatter.summary ?? item.description;
      const rawBody = page?.body ?? "";
      out.push({
        hit: {
          id: item.id,
          title: item.title,
          status: item.status,
          category: cat.id,
          summary,
          url: item.url,
          internalPath: `/flight-checker/spec/${item.id}`,
        },
        title: item.title.toLowerCase(),
        slug: slug.toLowerCase(),
        summary: summary.toLowerCase(),
        body: rawBody.toLowerCase(),
        rawBody,
      });
    }
  }
  return out;
}

/**
 * Tokenise comme l'upstream : lowercase, split sur l'espace et toute
 * ponctuation sauf `/ _ . -`, tokens de moins de 2 caractères ignorés.
 */
export function tokenize(query: string): string[] {
  return query
    .toLowerCase()
    .split(/[^\p{L}\p{N}/_.-]+/u)
    .filter((t) => t.length >= 2);
}

/** Jusqu'à MAX_EXCERPTS fenêtres de corps autour des premières occurrences. */
function excerptsFor(topic: IndexedTopic, tokens: string[]): string[] {
  const spans: { start: number; end: number }[] = [];
  for (const token of tokens) {
    const at = topic.body.indexOf(token);
    if (at === -1) continue;
    const start = Math.max(0, at - EXCERPT_RADIUS);
    const end = Math.min(
      topic.rawBody.length,
      at + token.length + EXCERPT_RADIUS,
    );
    if (spans.some((s) => start < s.end && end > s.start)) continue; // overlap
    spans.push({ start, end });
    if (spans.length >= MAX_EXCERPTS) break;
  }
  return spans.map(({ start, end }) => {
    const prefix = start > 0 ? "…" : "";
    const suffix = end < topic.rawBody.length ? "…" : "";
    return `${prefix}${topic.rawBody.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
  });
}

/**
 * Recherche pondérée par substring : pour chaque token, titre +8, slug +6,
 * résumé +4, corps +1 ; score = somme sur les tokens. Tri score décroissant
 * puis ordre canonique de la checklist (déterministe). `limit` clampé 1–25.
 */
export function searchSpecTopics(
  query: string,
  limit: number = DEFAULT_LIMIT,
): SpecSearchHit[] {
  const tokens = tokenize(query);
  if (tokens.length === 0) return [];
  INDEX ??= buildIndex();
  const max = Math.min(Math.max(Math.trunc(limit), 1), MAX_LIMIT);

  const scored: { topic: IndexedTopic; score: number; order: number }[] = [];
  for (let order = 0; order < INDEX.length; order++) {
    const topic = INDEX[order];
    let score = 0;
    for (const token of tokens) {
      if (topic.title.includes(token)) score += W_TITLE;
      if (topic.slug.includes(token)) score += W_SLUG;
      if (topic.summary.includes(token)) score += W_SUMMARY;
      if (topic.body.includes(token)) score += W_BODY;
    }
    if (score > 0) scored.push({ topic, score, order });
  }

  scored.sort((a, b) => b.score - a.score || a.order - b.order);
  return scored.slice(0, max).map(({ topic, score }) => ({
    ...topic.hit,
    score,
    excerpts: excerptsFor(topic, tokens),
  }));
}
