import { normalizeCode, normalizeText } from "./normalize.js";
import type { CpvEntry, SearchMatch, SynonymMap } from "./types.js";

const CODE_REGEX = /^\d/;
const MIN_SYNONYM_WORD_LEN = 4;

export function searchEntries(
  entries: CpvEntry[],
  query: string,
  limit: number,
  synonymMap: SynonymMap,
): SearchMatch[] {
  const trimmed = query.trim();
  if (!trimmed) return [];

  if (CODE_REGEX.test(trimmed)) {
    return searchByCode(entries, trimmed, limit);
  }

  return searchByText(entries, trimmed, limit, synonymMap);
}

function searchByCode(
  entries: CpvEntry[],
  query: string,
  limit: number,
): SearchMatch[] {
  const normalized = normalizeCode(query);
  if (!normalized) return [];

  const prefix = normalized.replace(/0+$/, "") || normalized;

  return entries
    .filter((entry) => entry.normalizedCode.startsWith(prefix))
    .map((entry) => ({
      code: entry.code,
      text: entry.text,
      score: entry.normalizedCode === normalized ? 1200 : 800,
      matchType: "code" as const,
    }))
    .sort((a, b) => b.score - a.score || a.code.localeCompare(b.code, "pl"))
    .slice(0, limit);
}

function searchByText(
  entries: CpvEntry[],
  query: string,
  limit: number,
  synonymMap: SynonymMap,
): SearchMatch[] {
  const normalizedQuery = normalizeText(query);
  const tokens = normalizedQuery.split(" ").filter(Boolean);

  const resultMap = new Map<string, SearchMatch>();

  // Direct text search
  for (const entry of entries) {
    const score = computeTextScore(entry, normalizedQuery, tokens);
    if (score > 0) {
      resultMap.set(entry.code, {
        code: entry.code,
        text: entry.text,
        score,
        matchType: "name",
      });
    }
  }

  // Synonym-expanded search
  const expandedTerms = expandQueryWithSynonyms(query, synonymMap);
  for (const term of expandedTerms) {
    const normalizedTerm = normalizeText(term);
    const termTokens = normalizedTerm.split(" ").filter(Boolean);

    for (const entry of entries) {
      if (resultMap.has(entry.code)) continue;

      const score = computeTextScore(entry, normalizedTerm, termTokens);
      if (score > 0) {
        resultMap.set(entry.code, {
          code: entry.code,
          text: entry.text,
          score: score * 0.9,
          matchType: "synonym",
          synonymTerm: term,
        });
      }
    }
  }

  return Array.from(resultMap.values())
    .sort((a, b) => b.score - a.score || a.code.localeCompare(b.code, "pl"))
    .slice(0, limit);
}

function computeTextScore(
  entry: CpvEntry,
  normalizedQuery: string,
  tokens: string[],
): number {
  let score = 0;

  if (entry.normalizedText === normalizedQuery) {
    score += 900;
  } else if (entry.normalizedText.startsWith(normalizedQuery)) {
    score += 550;
  } else if (entry.normalizedText.includes(normalizedQuery)) {
    score += 240;
  }

  if (tokens.length > 0) {
    const matchedTokens = tokens.filter((token) =>
      entry.normalizedSearch.includes(token),
    );

    if (matchedTokens.length === tokens.length) {
      score += 140 + matchedTokens.length * 20;
    } else if (matchedTokens.length > 0) {
      score += matchedTokens.length * 12;
    }
  }

  return score;
}

function expandQueryWithSynonyms(
  query: string,
  synonymMap: SynonymMap,
): string[] {
  const allWords = query.toLowerCase().split(/\s+/);
  const minLen = allWords.length >= 2 ? 5 : MIN_SYNONYM_WORD_LEN;
  const words = allWords.filter((w) => w.length >= minLen);
  const expanded = new Set<string>();

  for (const word of words) {
    const syns = synonymMap[word];
    if (!syns) continue;

    for (const s of syns) {
      const clean = s.replace(/\(.*?\)\s*/g, "").trim();
      if (clean.length >= minLen) {
        expanded.add(clean.toLowerCase());
      }
    }
  }

  return Array.from(expanded);
}
