/**
 * Fuzzy filtering for searchable select lists (openclaw-style).
 */

export function normalizeLowercaseStringOrEmpty(value: string): string {
  return value.trim().toLowerCase().replace(/\s+/g, ' ');
}

const WORD_BOUNDARY_CHARS = /[\s\-_./:#@]/;

export function isWordBoundary(text: string, index: number): boolean {
  return index === 0 || WORD_BOUNDARY_CHARS.test(text[index - 1] ?? '');
}

export function findWordBoundaryIndex(text: string, query: string): number | null {
  if (!query) {
    return null;
  }
  const textLower = normalizeLowercaseStringOrEmpty(text);
  const queryLower = normalizeLowercaseStringOrEmpty(query);
  const maxIndex = textLower.length - queryLower.length;
  if (maxIndex < 0) {
    return null;
  }
  for (let i = 0; i <= maxIndex; i++) {
    if (textLower.startsWith(queryLower, i) && isWordBoundary(textLower, i)) {
      return i;
    }
  }
  return null;
}

export function fuzzyMatchLower(queryLower: string, textLower: string): number | null {
  if (queryLower.length === 0) {
    return 0;
  }
  if (queryLower.length > textLower.length) {
    return null;
  }

  let queryIndex = 0;
  let score = 0;
  let lastMatchIndex = -1;
  let consecutiveMatches = 0;

  for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
    if (textLower[i] === queryLower[queryIndex]) {
      const atWordBoundary = isWordBoundary(textLower, i);
      if (lastMatchIndex === i - 1) {
        consecutiveMatches++;
        score -= consecutiveMatches * 5;
      } else {
        consecutiveMatches = 0;
        if (lastMatchIndex >= 0) {
          score += (i - lastMatchIndex - 1) * 2;
        }
      }
      if (atWordBoundary) {
        score -= 10;
      }
      score += i * 0.1;
      lastMatchIndex = i;
      queryIndex++;
    }
  }
  return queryIndex < queryLower.length ? null : score;
}

export function fuzzyFilterLower<T extends { searchTextLower?: string }>(
  items: T[],
  queryLower: string,
): T[] {
  const trimmed = queryLower.trim();
  if (!trimmed) {
    return items;
  }

  const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0);
  if (tokens.length === 0) {
    return items;
  }

  const results: { item: T; score: number }[] = [];
  for (const item of items) {
    const text = item.searchTextLower ?? '';
    let totalScore = 0;
    let allMatch = true;
    for (const token of tokens) {
      const score = fuzzyMatchLower(token, text);
      if (score !== null) {
        totalScore += score;
      } else {
        allMatch = false;
        break;
      }
    }
    if (allMatch) {
      results.push({ item, score: totalScore });
    }
  }
  results.sort((a, b) => a.score - b.score);
  return results.map((r) => r.item);
}

export function prepareSearchItems<
  T extends { label?: string; description?: string; searchText?: string },
>(items: T[]): (T & { searchTextLower: string })[] {
  return items.map((item) => {
    const parts: string[] = [];
    if (item.label) {
      parts.push(item.label);
    }
    if (item.description) {
      parts.push(item.description);
    }
    if (item.searchText) {
      parts.push(item.searchText);
    }
    return { ...item, searchTextLower: normalizeLowercaseStringOrEmpty(parts.join(' ')) };
  });
}
