// Lightweight fuzzy search helpers without external deps
// Provides diacritics-insensitive normalization and Levenshtein distance scoring

function normalizeText(text: string): string {
  return text
    .toLowerCase()
    .normalize("NFD")
    .replace(/\p{Diacritic}+/gu, "")
    .trim();
}

// Basic Levenshtein distance (iterative with two rows)
function levenshtein(a: string, b: string): number {
  if (a === b) return 0;
  const aLen = a.length;
  const bLen = b.length;
  if (aLen === 0) return bLen;
  if (bLen === 0) return aLen;

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

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

  for (let i = 1; i <= aLen; i++) {
    curr[0] = i;
    const aChar = a.charCodeAt(i - 1);
    for (let j = 1; j <= bLen; j++) {
      const cost = aChar === b.charCodeAt(j - 1) ? 0 : 1;
      curr[j] = Math.min(
        prev[j] + 1, // deletion
        curr[j - 1] + 1, // insertion
        prev[j - 1] + cost, // substitution
      );
    }
    for (let j = 0; j <= bLen; j++) prev[j] = curr[j];
  }
  return curr[bLen];
}

// Compute a heuristic match score (higher is better)
// 1) Exact/substring hits get high base; 2) otherwise use normalized Levenshtein distance
export function scoreMatch(queryRaw: string, targetRaw: string): number {
  const query = normalizeText(queryRaw);
  const target = normalizeText(targetRaw);
  if (!query) return 0;
  if (target.includes(query)) {
    // Reward earlier/shorter substring matches
    const pos = target.indexOf(query);
    return 100 - pos - Math.max(0, target.length - query.length);
  }

  // Token-aware: check each word token too, but require better similarity
  const tokens = target.split(/[^a-z0-9]+/g).filter(Boolean);
  for (const token of tokens) {
    if (token.includes(query)) {
      // Only give high score if the match is substantial (not just "and" matching)
      const similarity = query.length / Math.max(query.length, token.length);
      if (similarity >= 0.6) {
        // Require at least 60% similarity
        return 80 - Math.abs(token.length - query.length);
      }
    }
  }

  // Levenshtein fallback is for typos only: allow a small absolute number of
  // edits against the whole target or any single token. Relative similarity is
  // not enough here; half the letters of an unrelated word can match (e.g.
  // "rotate" vs "update" is edit distance 3 of 6).
  const maxEdits = query.length <= 4 ? 1 : 2;
  let best = 0;
  for (const candidate of [target, ...tokens]) {
    if (Math.abs(query.length - candidate.length) > maxEdits) continue;
    const distance = levenshtein(query, candidate);
    if (distance > maxEdits) continue;
    const maxLen = Math.max(query.length, candidate.length, 1);
    const similarity = 1 - distance / maxLen; // 0..1
    const score = Math.floor(similarity * 60); // scale below substring scores
    if (score > best) best = score;
  }
  return best;
}

export function minScoreForQuery(query: string): number {
  const len = normalizeText(query).length;
  return len <= 3 ? 40 : 30;
}

// Decide if a target matches a query based on a threshold
export function isFuzzyMatch(
  query: string,
  target: string,
  minScore?: number,
): boolean {
  const threshold =
    typeof minScore === "number" ? minScore : minScoreForQuery(query);
  return scoreMatch(query, target) >= threshold;
}

// Convenience: rank a list of items by best score across provided getters
export function rankByFuzzy<T>(
  items: T[],
  query: string,
  getters: Array<(item: T) => string>,
  minScore?: number,
): Array<{ item: T; score: number; matchedText?: string }> {
  const results: Array<{ item: T; score: number; matchedText?: string }> = [];
  const threshold =
    typeof minScore === "number" ? minScore : minScoreForQuery(query);
  for (const item of items) {
    let best = 0;
    let matchedText = "";
    for (const get of getters) {
      const value = get(item);
      if (!value) continue;
      const s = scoreMatch(query, value);
      if (s > best) {
        best = s;
        matchedText = value;
      }
      if (best >= 95) {
        break;
      }
    }
    if (best >= threshold) results.push({ item, score: best, matchedText });
  }
  results.sort((a, b) => b.score - a.score);
  return results;
}

export function normalizeForSearch(text: string): string {
  return normalizeText(text);
}

// Convert ids like "addPassword", "add-password", "add_password" to words for matching
export function idToWords(id: string): string {
  const spaced = id
    .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
    .replace(/[._-]+/g, " ");
  return normalizeText(spaced);
}
