// Fuzzy string matching utilities for artist deduplication.
// Implements Levenshtein distance and Jaro-Winkler similarity.

/**
 * Levenshtein distance between two strings.
 * Returns the minimum number of single-character edits (insertions, deletions, substitutions)
 * required to change one string into the other.
 */
export function levenshteinDistance(a: string, b: string): number {
  if (a === b) return 0;
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;

  const matrix = new Array(a.length + 1).fill(null).map(() => new Array(b.length + 1).fill(0));

  for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
  for (let j = 0; j <= b.length; j++) matrix[0][j] = j;

  for (let i = 1; i <= a.length; i++) {
    for (let j = 1; j <= b.length; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      matrix[i][j] = Math.min(
        matrix[i - 1][j] + 1,      // deletion
        matrix[i][j - 1] + 1,      // insertion
        matrix[i - 1][j - 1] + cost // substitution
      );
    }
  }

  return matrix[a.length][b.length];
}

/**
 * Normalized Levenshtein similarity (0..1).
 * 1 = identical, 0 = completely different.
 */
export function levenshteinSimilarity(a: string, b: string): number {
  const maxLen = Math.max(a.length, b.length);
  if (maxLen === 0) return 1;
  const distance = levenshteinDistance(a, b);
  return 1 - distance / maxLen;
}

/**
 * Jaro similarity between two strings.
 * Returns a value between 0 (no similarity) and 1 (identical).
 */
export function jaroSimilarity(a: string, b: string): number {
  if (a === b) return 1;
  if (a.length === 0 || b.length === 0) return 0;

  const matchWindow = Math.floor(Math.max(a.length, b.length) / 2) - 1;
  const aMatches = new Array(a.length).fill(false);
  const bMatches = new Array(b.length).fill(false);

  let matches = 0;
  let transpositions = 0;

  // Find matches
  for (let i = 0; i < a.length; i++) {
    const start = Math.max(0, i - matchWindow);
    const end = Math.min(i + matchWindow + 1, b.length);
    for (let j = start; j < end; j++) {
      if (bMatches[j] || a[i] !== b[j]) continue;
      aMatches[i] = bMatches[j] = true;
      matches++;
      break;
    }
  }

  if (matches === 0) return 0;

  // Count transpositions
  let k = 0;
  for (let i = 0; i < a.length; i++) {
    if (!aMatches[i]) continue;
    while (!bMatches[k]) k++;
    if (a[i] !== b[k]) transpositions++;
    k++;
  }

  const m = matches;
  return (m / a.length + m / b.length + (m - transpositions / 2) / m) / 3;
}

/**
 * Jaro-Winkler similarity.
 * Gives more weight to common prefixes.
 * Returns a value between 0 (no similarity) and 1 (identical).
 */
export function jaroWinklerSimilarity(a: string, b: string, prefixScale = 0.1): number {
  const jaro = jaroSimilarity(a, b);
  
  if (jaro < 0.7) return jaro; // Only boost if already somewhat similar

  // Calculate common prefix length (max 4)
  let prefixLen = 0;
  const minLen = Math.min(a.length, b.length, 4);
  for (let i = 0; i < minLen; i++) {
    if (a[i] === b[i]) prefixLen++;
    else break;
  }

  return jaro + prefixLen * prefixScale * (1 - jaro);
}

/**
 * Normalize artist name for matching.
 * Lowercase, strip diacritics, remove collaboration suffixes.
 */
export function normalizeForMatching(name: string): string {
  return name
    .toLowerCase()
    .trim()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '') // Remove diacritics
    .replace(/\s*\(?\s*(feat(?:uring)?\.?|ft\.?|vs\.?|x)\s+.*$/i, '')
    .replace(/,\s*$/, '')
    .replace(/^([^,]+),.*$/, '$1')
    .replace(/[&+]/g, 'and')
    .replace(/\s+/g, ' ')
    .trim();
}

/**
 * Find best match for a name among candidates.
 * Returns { match: string, score: number } or null if no good match.
 */
export function findBestMatch<T extends { name: string }>(
  query: string,
  candidates: T[],
  threshold = 0.9
): { match: T; score: number } | null {
  const normalizedQuery = normalizeForMatching(query);
  let bestMatch: { match: T; score: number } | null = null;

  for (const candidate of candidates) {
    const normalizedCandidate = normalizeForMatching(candidate.name);
    
    // Exact normalized match
    if (normalizedQuery === normalizedCandidate) {
      return { match: candidate, score: 1 };
    }

    // Jaro-Winkler similarity
    const score = jaroWinklerSimilarity(normalizedQuery, normalizedCandidate);
    
    if (score >= threshold && (!bestMatch || score > bestMatch.score)) {
      bestMatch = { match: candidate, score };
    }
  }

  return bestMatch;
}

/**
 * Check if two artist names likely represent the same artist.
 */
export function areSameArtist(name1: string, name2: string, threshold = 0.95): boolean {
  const norm1 = normalizeForMatching(name1);
  const norm2 = normalizeForMatching(name2);
  
  if (norm1 === norm2) return true;
  
  const score = jaroWinklerSimilarity(norm1, norm2);
  return score >= threshold;
}