/**
 * Fuzzy matching logic to link components across different sources.
 */
export class ComponentMatcher {
  constructor() {
    this.MATCH_THRESHOLD = 0.8;
  }

  /**
   * Merges components from different sources based on name similarity.
   * @param {Array} gitSources
   * @param {Array} storybookSources
   * @param {Array} figmaSources
   * @returns {Array} Merged component objects
   */
  match(gitSources, storybookSources, figmaSources) {
    const merged = new Map();

    // 1. Start with Git as the primary source of truth for code
    gitSources.forEach(git => {
      this._upsert(merged, git.name, { git });
    });

    // 2. Match Storybook entries
    storybookSources.forEach(sb => {
      const matchName = this._findBestMatch(sb.name, Array.from(merged.keys()));
      if (matchName) {
        const existing = merged.get(matchName);
        merged.set(matchName, { ...existing, storybook: sb });
      } else {
        this._upsert(merged, sb.name, { storybook: sb });
      }
    });

    // 3. Match Figma entries
    figmaSources.forEach(figma => {
      const matchName = this._findBestMatch(figma.name, Array.from(merged.keys()));
      if (matchName) {
        const existing = merged.get(matchName);
        merged.set(matchName, { ...existing, figma });
      } else {
        this._upsert(merged, figma.name, { figma });
      }
    });

    return Array.from(merged.values()).map(item => ({
      name: item.git?.name || item.storybook?.name || item.figma?.name,
      sources: {
        git: item.git || null,
        storybook: item.storybook || null,
        figma: item.figma || null
      }
    }));
  }

  _upsert(map, name, data) {
    if (!map.has(name)) {
      map.set(name, {});
    }
    const existing = map.get(name);
    map.set(name, { ...existing, ...data });
  }

  _findBestMatch(target, candidates) {
    // 1. Try Exact Match (Case Insensitive)
    const exact = candidates.find(c => c.toLowerCase() === target.toLowerCase());
    if (exact) return exact;

    // 2. Fuzzy Match
    let bestMatch = null;
    let maxSimilarity = 0;

    for (const candidate of candidates) {
      const similarity = this._calculateSimilarity(target, candidate);
      if (similarity >= this.MATCH_THRESHOLD && similarity > maxSimilarity) {
        maxSimilarity = similarity;
        bestMatch = candidate;
      }
    }

    return bestMatch;
  }

  /**
   * Calculates similarity between 0 and 1 using Levenshtein distance.
   */
  _calculateSimilarity(s1, s2) {
    const a = s1.toLowerCase();
    const b = s2.toLowerCase();

    if (a.length === 0) return b.length === 0 ? 1.0 : 0.0;
    if (b.length === 0) return 0.0;

    const matrix = [];

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

    for (let i = 1; i <= b.length; i++) {
      for (let j = 1; j <= a.length; j++) {
        if (b.charAt(i - 1) === a.charAt(j - 1)) {
          matrix[i][j] = matrix[i - 1][j - 1];
        } else {
          matrix[i][j] = Math.min(
            matrix[i - 1][j - 1] + 1, // substitution
            Math.min(
              matrix[i][j - 1] + 1, // insertion
              matrix[i - 1][j] + 1 // deletion
            )
          );
        }
      }
    }

    const distance = matrix[b.length][a.length];
    const maxLength = Math.max(a.length, b.length);

    return 1.0 - (distance / maxLength);
  }
}
