/**
 * Tiny fuzzy matcher — substring + subsequence with light scoring.
 *
 * Score components (higher = better):
 *   +1000 — exact label match
 *   +500  — label starts with the query
 *   +200  — label contains the query (case-insensitive)
 *   +100  — section/description/keyword contains the query
 *   +50   — first-character matches the first query character
 *   -idx  — small penalty per character of "gap" between matched chars
 *
 * Returns `null` when no character of the query can be aligned at all.
 */
export function fuzzyScore(haystack: string, query: string): number | null {
  if (!query) return 0;
  const h = haystack.toLowerCase();
  const q = query.toLowerCase();

  if (h === q) return 1000;
  if (h.startsWith(q)) return 500;
  const idx = h.indexOf(q);
  if (idx !== -1) return 200 - idx;

  // Subsequence match — every char of q appears in order in h.
  let hi = 0;
  let qi = 0;
  let gaps = 0;
  let firstMatch = -1;
  while (hi < h.length && qi < q.length) {
    if (h[hi] === q[qi]) {
      if (firstMatch === -1) firstMatch = hi;
      qi += 1;
    } else if (qi > 0) {
      gaps += 1;
    }
    hi += 1;
  }
  if (qi !== q.length) return null;
  return 50 + (firstMatch === 0 ? 50 : 0) - gaps;
}

export interface ScoredCommandFields {
  label: string;
  description?: string;
  section?: string;
  keywords?: string[];
}

/**
 * Compose the best score across a command's searchable fields.
 * Falls back to `null` when nothing matches → caller hides the row.
 */
export function scoreCommand(
  fields: ScoredCommandFields,
  query: string,
): number | null {
  if (!query) return 0;
  const candidates = [
    fields.label,
    fields.description ?? '',
    fields.section ?? '',
    ...(fields.keywords ?? []),
  ];
  let best: number | null = null;
  for (const c of candidates) {
    if (!c) continue;
    const score = fuzzyScore(c, query);
    if (score !== null && (best === null || score > best)) best = score;
  }
  return best;
}
