export type FuzzyMatch = {
  score: number;
  indices: number[];
};

/**
 * Subsequence fuzzy match with fzf-style scoring.
 * All query characters must appear in order; higher scores rank first.
 */
export function fuzzyMatch(query: string, text: string): FuzzyMatch | null {
  const q = query.trim().toLowerCase();
  if (!q) return { score: 0, indices: [] };

  const t = text.toLowerCase();
  if (!t) return null;

  // Fast path: contiguous substring
  const substringIndex = t.indexOf(q);
  if (substringIndex !== -1) {
    const indices = Array.from({ length: q.length }, (_, i) => substringIndex + i);
    // Prefer earlier matches and shorter haystacks
    const score = 10_000 - substringIndex * 10 - (t.length - q.length);
    return { score, indices };
  }

  let ti = 0;
  const indices: number[] = [];
  let score = 0;
  let prevIndex = -1;
  let consecutive = 0;

  for (let qi = 0; qi < q.length; qi += 1) {
    const ch = q[qi];
    let found = -1;
    for (; ti < t.length; ti += 1) {
      if (t[ti] === ch) {
        found = ti;
        ti += 1;
        break;
      }
    }
    if (found === -1) return null;

    indices.push(found);

    // Consecutive bonus
    if (prevIndex === found - 1) {
      consecutive += 1;
      score += 15 + consecutive * 5;
    } else {
      consecutive = 0;
      score += 1;
    }

    // Word-boundary / start bonus
    if (
      found === 0 ||
      t[found - 1] === ' ' ||
      t[found - 1] === '-' ||
      t[found - 1] === '_' ||
      t[found - 1] === '&' ||
      t[found - 1] === '/'
    ) {
      score += 20;
    }

    // Prefer earlier matches
    score -= found;

    prevIndex = found;
  }

  // Prefer tighter overall spans
  const span = indices[indices.length - 1] - indices[0] + 1;
  score += Math.max(0, 50 - (span - q.length) * 2);
  // Prefer shorter labels slightly
  score -= Math.max(0, t.length - q.length) * 0.1;

  return { score, indices };
}

/** Best fuzzy match across multiple strings (title, keywords, etc.). */
export function fuzzyMatchAny(query: string, texts: Array<string | undefined | null>): FuzzyMatch | null {
  let best: FuzzyMatch | null = null;
  for (const text of texts) {
    if (!text) continue;
    const match = fuzzyMatch(query, text);
    if (match && (!best || match.score > best.score)) {
      best = match;
    }
  }
  return best;
}

export function fuzzyFilter<T>(
  items: T[],
  query: string,
  getTexts: (item: T) => Array<string | undefined | null>
): Array<{ item: T; score: number }> {
  const q = query.trim();
  if (!q) {
    return items.map((item) => ({ item, score: 0 }));
  }

  const results: Array<{ item: T; score: number }> = [];
  for (const item of items) {
    const match = fuzzyMatchAny(q, getTexts(item));
    if (match) {
      results.push({ item, score: match.score });
    }
  }
  results.sort((a, b) => b.score - a.score);
  return results;
}
