import type { GraphDataset } from "../../types/medical";
import { normalizeSearchText, tokenizeSearchText } from "../../utils/text.ts";

const boundedLevenshtein = (a: string, b: string, maxDistance: number): number => {
  if (a === b) return 0;
  const aLen = a.length;
  const bLen = b.length;

  if (Math.abs(aLen - bLen) > maxDistance) {
    return maxDistance + 1;
  }

  const prev = new Array(bLen + 1);
  const curr = new Array(bLen + 1);

  for (let j = 0; j <= bLen; j += 1) prev[j] = j;

  for (let i = 1; i <= aLen; i += 1) {
    curr[0] = i;
    let rowMin = curr[0];

    for (let j = 1; j <= bLen; j += 1) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      curr[j] = Math.min(
        prev[j] + 1,
        curr[j - 1] + 1,
        prev[j - 1] + cost
      );
      rowMin = Math.min(rowMin, curr[j]);
    }

    if (rowMin > maxDistance) {
      return maxDistance + 1;
    }

    for (let j = 0; j <= bLen; j += 1) {
      prev[j] = curr[j];
    }
  }

  return prev[bLen];
};

export const searchDatasetsByName = (
  datasets: GraphDataset[],
  query: string
): GraphDataset[] => {
  const trimmedQuery = query.trim();
  if (!trimmedQuery) return [];

  const queryTokens = tokenizeSearchText(trimmedQuery);
  const normalizedQuery = normalizeSearchText(trimmedQuery);

  const scored = datasets
    .map((dataset) => {
      const normalizedName = normalizeSearchText(dataset.name);
      const nameTokens = tokenizeSearchText(dataset.name);

      let score = 0;

      if (normalizedName === normalizedQuery) score += 120;
      if (normalizedName.startsWith(normalizedQuery)) score += 80;
      if (normalizedName.includes(normalizedQuery)) score += 50;

      queryTokens.forEach((token) => {
        if (token.length < 2) return;
        if (nameTokens.some((nameToken) => nameToken.startsWith(token))) score += 30;
        if (nameTokens.some((nameToken) => nameToken.includes(token))) score += 18;

        if (token.length >= 4) {
          const maxDistance = token.length <= 6 ? 1 : 2;
          const nearestDistance = nameTokens.reduce((best, nameToken) => {
            if (Math.abs(nameToken.length - token.length) > maxDistance) return best;
            const distance = boundedLevenshtein(token, nameToken, maxDistance);
            return Math.min(best, distance);
          }, maxDistance + 1);

          if (nearestDistance <= maxDistance) {
            score += nearestDistance === 0 ? 0 : nearestDistance === 1 ? 16 : 10;
          }
        }
      });

      return { dataset, score };
    })
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score || a.dataset.name.localeCompare(b.dataset.name));

  return scored.map((item) => item.dataset);
};
