'use strict';

/**
 * CTM Fuzzy Search Utilities
 *
 * Pure functions for typo-tolerant search:
 * - Damerau-Levenshtein edit distance
 * - 1-edit variant generation
 * - Double Metaphone phonetic encoding
 * - QWERTY keyboard proximity scoring
 * - Query expansion orchestrator
 */

// --- Damerau-Levenshtein Distance ---

function levenshtein(a, b) {
  a = (a || '').toLowerCase();
  b = (b || '').toLowerCase();
  if (a === b) return 0;
  const la = a.length, lb = b.length;
  if (!la) return lb;
  if (!lb) return la;

  const d = Array.from({ length: la + 1 }, () => new Uint16Array(lb + 1));
  for (let i = 0; i <= la; i++) d[i][0] = i;
  for (let j = 0; j <= lb; j++) d[0][j] = j;

  for (let i = 1; i <= la; i++) {
    for (let j = 1; j <= lb; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      d[i][j] = Math.min(
        d[i - 1][j] + 1,
        d[i][j - 1] + 1,
        d[i - 1][j - 1] + cost
      );
      if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
        d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
      }
    }
  }
  return d[la][lb];
}

// --- 1-Edit Variant Generation ---

function generate1EditVariants(term) {
  const result = new Set();
  const s = (term || '').toLowerCase();
  if (s.length < 3) return result;
  const ALPHA = 'abcdefghijklmnopqrstuvwxyz';

  for (let i = 0; i < s.length; i++) {
    result.add(s.slice(0, i) + s.slice(i + 1));
  }
  for (let i = 0; i < s.length; i++) {
    for (const c of ALPHA) {
      if (c !== s[i]) result.add(s.slice(0, i) + c + s.slice(i + 1));
    }
  }
  for (let i = 0; i < s.length - 1; i++) {
    if (s[i] !== s[i + 1]) {
      result.add(s.slice(0, i) + s[i + 1] + s[i] + s.slice(i + 2));
    }
  }
  result.delete(s);
  return result;
}

// --- Double Metaphone ---

function doubleMetaphone(word) {
  if (!word) return ['', ''];
  const original = word.toUpperCase();
  let current = 0;
  const length = original.length;
  const last = length - 1;
  let primary = '';
  let alternate = '';
  const MAX_LENGTH = 4;

  const at = (pos) => pos >= 0 && pos < length ? original[pos] : '';
  const sliceAt = (pos, len) => original.slice(pos, pos + len);
  const isVowel = (pos) => 'AEIOUY'.includes(at(pos));

  const stringAt = (start, len, ...list) => {
    const s = sliceAt(start, len);
    return list.some(l => l === s);
  };

  const add = (p, a) => {
    if (p) primary += p;
    alternate += (a !== undefined ? a : p) || '';
  };

  // Skip initial silent letters
  if (stringAt(0, 2, 'GN', 'KN', 'PN', 'AE', 'WR')) current++;

  // Initial X → S
  if (at(0) === 'X') { add('S'); current++; }

  while (primary.length < MAX_LENGTH || alternate.length < MAX_LENGTH) {
    if (current >= length) break;
    const ch = at(current);

    // Vowels: only encode at start
    if ('AEIOUY'.includes(ch)) {
      if (current === 0) add('A');
      current++;
      continue;
    }

    switch (ch) {
      case 'B':
        add('P');
        current += at(current + 1) === 'B' ? 2 : 1;
        break;

      case 'C':
        if (current > 1 && !isVowel(current - 2) && stringAt(current - 1, 3, 'ACH') && at(current + 2) !== 'I' && (at(current + 2) !== 'E' || stringAt(current - 2, 6, 'BACHER', 'MACHER'))) {
          add('K'); current += 2; break;
        }
        if (current === 0 && stringAt(current, 6, 'CAESAR')) { add('S'); current += 2; break; }
        if (stringAt(current, 2, 'CH')) {
          if (current > 0 && stringAt(current, 4, 'CHAE')) { add('K', 'X'); current += 2; break; }
          if (current === 0 && (stringAt(current + 1, 5, 'HARAC', 'HARIS') || stringAt(current + 1, 3, 'HOR', 'HYM', 'HIA', 'HEM')) && !stringAt(0, 5, 'CHORE')) { add('K'); current += 2; break; }
          if (stringAt(0, 4, 'VAN ', 'VON ') || stringAt(0, 3, 'SCH') || stringAt(current - 2, 6, 'ORCHES', 'ARCHIT', 'ORCHID') || stringAt(current + 2, 1, 'T', 'S') || ((stringAt(current - 1, 1, 'A', 'O', 'U', 'E') || current === 0) && stringAt(current + 2, 1, 'L', 'R', 'N', 'M', 'B', 'H', 'F', 'V', 'W', ' '))) {
            add('K');
          } else if (current > 0) {
            add(stringAt(0, 2, 'MC') ? 'K' : 'X', 'K');
          } else {
            add('X');
          }
          current += 2; break;
        }
        if (stringAt(current, 2, 'CZ') && !stringAt(current - 2, 4, 'WICZ')) { add('S', 'X'); current += 2; break; }
        if (stringAt(current + 1, 3, 'CIA')) { add('X'); current += 3; break; }
        if (stringAt(current, 2, 'CC') && !(current === 1 && at(0) === 'M')) {
          if (stringAt(current + 2, 1, 'I', 'E', 'H') && !stringAt(current + 2, 2, 'HU')) {
            if ((current === 1 && at(current - 1) === 'A') || stringAt(current - 1, 5, 'UCCEE', 'UCCES')) add('KS');
            else add('X');
            current += 3; break;
          } else { add('K'); current += 2; break; }
        }
        if (stringAt(current, 2, 'CK', 'CG', 'CQ')) { add('K'); current += 2; break; }
        if (stringAt(current, 2, 'CI', 'CE', 'CY')) {
          if (stringAt(current, 3, 'CIO', 'CIE', 'CIA')) add('S', 'X');
          else add('S');
          current += 2; break;
        }
        add('K');
        if (stringAt(current + 1, 2, ' C', ' Q', ' G')) current += 3;
        else current += (stringAt(current + 1, 1, 'C', 'K', 'Q') && !stringAt(current + 1, 2, 'CE', 'CI')) ? 2 : 1;
        break;

      case 'D':
        if (stringAt(current, 2, 'DG')) {
          if (stringAt(current + 2, 1, 'I', 'E', 'Y')) { add('J'); current += 3; }
          else { add('TK'); current += 2; }
        } else {
          add('T');
          current += stringAt(current, 2, 'DT', 'DD') ? 2 : 1;
        }
        break;

      case 'F':
        add('F');
        current += at(current + 1) === 'F' ? 2 : 1;
        break;

      case 'G':
        if (at(current + 1) === 'H') {
          if (current > 0 && !isVowel(current - 1)) { add('K'); current += 2; break; }
          if (current === 0) {
            if (at(current + 2) === 'I') add('J'); else add('K');
            current += 2; break;
          }
          if ((current > 1 && stringAt(current - 2, 1, 'B', 'H', 'D')) || (current > 2 && stringAt(current - 3, 1, 'B', 'H', 'D')) || (current > 3 && stringAt(current - 4, 1, 'B', 'H'))) {
            current += 2; break;
          }
          if (current > 2 && at(current - 1) === 'U' && stringAt(current - 3, 1, 'C', 'G', 'L', 'R', 'T')) {
            add('F');
          } else if (current > 0 && at(current - 1) !== 'I') {
            add('K');
          }
          current += 2; break;
        }
        if (at(current + 1) === 'N') {
          if (current === 1 && isVowel(0) && !false) { add('KN', 'N'); }
          else if (!stringAt(current + 2, 2, 'EY') && at(current + 1) !== 'Y' && !false) { add('N', 'KN'); }
          else add('KN');
          current += 2; break;
        }
        if (stringAt(current + 1, 2, 'LI') && !false) { add('KL', 'L'); current += 2; break; }
        if (current === 0 && (at(1) === 'Y' || stringAt(1, 2, 'ES', 'EP', 'EB', 'EL', 'EY', 'IB', 'IL', 'IN', 'IE', 'EI', 'ER'))) {
          add('K', 'J'); current += 2; break;
        }
        if ((stringAt(current + 1, 2, 'ER') || at(current + 1) === 'Y') && !stringAt(0, 6, 'DANGER', 'RANGER', 'MANGER') && !stringAt(current - 1, 1, 'E', 'I') && !stringAt(current - 1, 3, 'RGY', 'OGY')) {
          add('K', 'J'); current += 2; break;
        }
        if (stringAt(current + 1, 1, 'E', 'I', 'Y') || stringAt(current - 1, 4, 'AGGI', 'OGGI')) {
          if (stringAt(0, 4, 'VAN ', 'VON ') || stringAt(0, 3, 'SCH') || stringAt(current + 1, 2, 'ET')) add('K');
          else if (stringAt(current + 1, 4, 'IER ')) add('J');
          else add('J', 'K');
          current += 2; break;
        }
        add('K');
        current += at(current + 1) === 'G' ? 2 : 1;
        break;

      case 'H':
        if ((current === 0 || isVowel(current - 1)) && isVowel(current + 1)) { add('H'); current += 2; }
        else current++;
        break;

      case 'J':
        if (stringAt(current, 4, 'JOSE') || stringAt(0, 4, 'SAN ')) {
          if ((current === 0 && at(current + 4) === ' ') || stringAt(0, 4, 'SAN ')) add('H');
          else add('J', 'H');
          current++; break;
        }
        if (current === 0) add('J', 'A');
        else if (isVowel(current - 1) && !false && (at(current + 1) === 'A' || at(current + 1) === 'O')) add('J', 'H');
        else if (current === last) add('J', ' ');
        else if (!stringAt(current + 1, 1, 'L', 'T', 'K', 'S', 'N', 'M', 'B', 'H', 'F', 'G', 'R', 'W', 'Z') && !stringAt(current - 1, 1, 'S', 'K', 'L')) add('J');
        current += at(current + 1) === 'J' ? 2 : 1;
        break;

      case 'K':
        add('K');
        current += at(current + 1) === 'K' ? 2 : 1;
        break;

      case 'L':
        if (at(current + 1) === 'L') {
          if ((current === length - 3 && stringAt(current - 1, 4, 'ILLO', 'ILLA', 'ALLE')) || ((stringAt(last - 1, 2, 'AS', 'OS') || stringAt(last, 1, 'A', 'O')) && stringAt(current - 1, 4, 'ALLE'))) {
            add('L', ''); current += 2; break;
          }
          current += 2;
        } else current++;
        add('L');
        break;

      case 'M':
        add('M');
        if (stringAt(current - 1, 3, 'UMB') && (current + 1 === last || stringAt(current + 2, 2, 'ER'))) current += 2;
        else current += at(current + 1) === 'M' ? 2 : 1;
        break;

      case 'N':
        add('N');
        current += at(current + 1) === 'N' ? 2 : 1;
        break;

      case 'P':
        if (at(current + 1) === 'H') { add('F'); current += 2; }
        else { add('P'); current += stringAt(current + 1, 1, 'P', 'B') ? 2 : 1; }
        break;

      case 'Q':
        add('K');
        current += at(current + 1) === 'Q' ? 2 : 1;
        break;

      case 'R':
        if (current === last && !false && stringAt(current - 2, 2, 'IE') && !stringAt(current - 4, 2, 'ME', 'MA')) add('', 'R');
        else add('R');
        current += at(current + 1) === 'R' ? 2 : 1;
        break;

      case 'S':
        if (stringAt(current - 1, 3, 'ISL', 'YSL')) { current++; break; }
        if (current === 0 && stringAt(0, 5, 'SUGAR')) { add('X', 'S'); current++; break; }
        if (stringAt(current, 2, 'SH')) {
          if (stringAt(current + 1, 4, 'HEIM', 'HOEK', 'HOLM', 'HOLZ')) add('S');
          else add('X');
          current += 2; break;
        }
        if (stringAt(current, 3, 'SIO', 'SIA') || stringAt(current, 4, 'SIAN')) { add('S', 'X'); current += 3; break; }
        if ((current === 0 && stringAt(1, 1, 'M', 'N', 'L', 'W')) || stringAt(current + 1, 1, 'Z')) { add('S', 'X'); current += stringAt(current + 1, 1, 'Z') ? 2 : 1; break; }
        if (stringAt(current, 2, 'SC')) {
          if (at(current + 2) === 'H') {
            if (stringAt(current + 3, 2, 'OO', 'ER', 'EN', 'UY', 'ED', 'EM')) {
              if (stringAt(current + 3, 2, 'ER', 'EN')) add('X', 'SK');
              else add('SK');
              current += 3; break;
            }
            if (current === 0 && !isVowel(3) && at(3) !== 'W') add('X', 'S');
            else add('X');
            current += 3; break;
          }
          if (stringAt(current + 2, 1, 'I', 'E', 'Y')) { add('S'); current += 3; break; }
          add('SK'); current += 3; break;
        }
        if (current === last && stringAt(current - 2, 2, 'AI', 'OI')) add('', 'S');
        else add('S');
        current += stringAt(current + 1, 1, 'S', 'Z') ? 2 : 1;
        break;

      case 'T':
        if (stringAt(current, 4, 'TION')) { add('X'); current += 3; break; }
        if (stringAt(current, 3, 'TIA', 'TCH')) { add('X'); current += 3; break; }
        if (stringAt(current, 2, 'TH') || stringAt(current, 3, 'TTH')) {
          if (stringAt(current + 2, 2, 'OM', 'AM') || stringAt(0, 4, 'VAN ', 'VON ') || stringAt(0, 3, 'SCH')) add('T');
          else add('0', 'T');
          current += 2; break;
        }
        add('T');
        current += stringAt(current + 1, 1, 'T', 'D') ? 2 : 1;
        break;

      case 'V':
        add('F');
        current += at(current + 1) === 'V' ? 2 : 1;
        break;

      case 'W':
        if (stringAt(current, 2, 'WR')) { add('R'); current += 2; break; }
        if (current === 0 && (isVowel(current + 1) || stringAt(current, 2, 'WH'))) {
          if (isVowel(current + 1)) add('A', 'F');
          else add('A');
        }
        if ((current === last && isVowel(current - 1)) || stringAt(current - 1, 5, 'EWSKI', 'EWSKY', 'OWSKI', 'OWSKY') || stringAt(0, 3, 'SCH')) {
          add('', 'F'); current++; break;
        }
        if (stringAt(current, 4, 'WICZ', 'WITZ')) { add('TS', 'FX'); current += 4; break; }
        current++;
        break;

      case 'X':
        if (!(current === last && (stringAt(current - 3, 3, 'IAU', 'EAU') || stringAt(current - 2, 2, 'AU', 'OU')))) add('KS');
        current += stringAt(current + 1, 1, 'C', 'X') ? 2 : 1;
        break;

      case 'Z':
        if (at(current + 1) === 'H') { add('J'); current += 2; break; }
        if (stringAt(current + 1, 2, 'ZO', 'ZI', 'ZA') || (false && current > 0 && at(current - 1) !== 'T')) add('S', 'TS');
        else add('S');
        current += at(current + 1) === 'Z' ? 2 : 1;
        break;

      default:
        current++;
    }
  }

  return [primary.slice(0, MAX_LENGTH), alternate.slice(0, MAX_LENGTH)];
}

function phoneticMatch(a, b) {
  if (!a || !b) return false;
  const [p1, a1] = doubleMetaphone(a);
  const [p2, a2] = doubleMetaphone(b);
  if (!p1 || !p2) return false;
  return p1 === p2 || p1 === a2 || a1 === p2;
}

// --- Keyboard Proximity ---

const QWERTY_NEIGHBORS = {
  q: 'wa', w: 'qeas', e: 'wrds', r: 'etfd', t: 'rygf',
  y: 'tuhg', u: 'yijh', i: 'uokj', o: 'iplk', p: 'ol',
  a: 'qwsz', s: 'awedxz', d: 'serfcx', f: 'drtgvc', g: 'ftyhbv',
  h: 'gyujnb', j: 'huikmn', k: 'jiolm', l: 'kop',
  z: 'asx', x: 'zsdc', c: 'xdfv', v: 'cfgb', b: 'vghn',
  n: 'bhjm', m: 'njk',
};

function keyboardProximityScore(a, b) {
  a = (a || '').toLowerCase();
  b = (b || '').toLowerCase();
  if (a === b) return 0;
  let score = 0;
  const minLen = Math.min(a.length, b.length);
  const maxLen = Math.max(a.length, b.length);
  score += (maxLen - minLen) * 2;
  for (let i = 0; i < minLen; i++) {
    if (a[i] !== b[i]) {
      const neighbors = QWERTY_NEIGHBORS[a[i]] || '';
      score += neighbors.includes(b[i]) ? 1 : 3;
    }
  }
  return score;
}

// --- Query Expansion Orchestrator ---

function normalizeFuzzySearchValue(value) {
  return String(value || '')
    .trim()
    .toLowerCase()
    .replace(/(^|[\s([{])[$/]+(?=[a-z0-9_-])/g, '$1')
    .replace(/\s+/g, ' ');
}

function expandQueryFuzzy(query, options = {}) {
  const terms = normalizeFuzzySearchValue(query).split(/\s+/).filter(t => t.length >= 2);
  if (terms.length === 0) return { ftsQuery: '', expansions: [] };

  const MAX_VARIANTS_PER_TERM = 8;
  const joiner = String(options.joiner || '').toUpperCase() === 'OR' ? ' OR ' : ' AND ';
  const parts = [];
  const expansions = [];

  for (const term of terms) {
    const clean = term.replace(/"/g, '').toLowerCase();

    if (clean.length < 3) {
      parts.push('"' + clean + '"');
      continue;
    }

    const editVariants = generate1EditVariants(clean);

    // Identify transposition variants (highest-value: >80% of real typos per Damerau)
    const transpositions = new Set();
    for (let i = 0; i < clean.length - 1; i++) {
      if (clean[i] !== clean[i + 1]) {
        transpositions.add(clean.slice(0, i) + clean[i + 1] + clean[i] + clean.slice(i + 2));
      }
    }

    const kept = [];
    for (const v of editVariants) {
      if (v.length < 2) continue;
      const isTransposition = transpositions.has(v);
      const isPhonetic = phoneticMatch(clean, v);
      const kbScore = keyboardProximityScore(clean, v);
      if (isTransposition || isPhonetic || kbScore <= 2) {
        kept.push({ variant: v, phonetic: isPhonetic, kbScore, transposition: isTransposition });
      }
    }

    // Transpositions first (most common real typos), then phonetic, then keyboard
    kept.sort((a, b) => {
      if (a.transposition !== b.transposition) return a.transposition ? -1 : 1;
      if (a.phonetic !== b.phonetic) return a.phonetic ? -1 : 1;
      return a.kbScore - b.kbScore;
    });

    const topVariants = kept.slice(0, MAX_VARIANTS_PER_TERM);

    if (topVariants.length > 0) {
      const allTerms = [clean, ...topVariants.map(v => v.variant)];
      parts.push('(' + allTerms.map(t => '"' + t + '"').join(' OR ') + ')');
    } else {
      parts.push('"' + clean + '"');
    }

    expansions.push({
      original: clean,
      variants: topVariants.map(v => v.variant),
      phonetic: topVariants.filter(v => v.phonetic).map(v => v.variant),
    });
  }

  return { ftsQuery: parts.join(joiner), expansions };
}

module.exports = {
  levenshtein, generate1EditVariants,
  doubleMetaphone, phoneticMatch,
  keyboardProximityScore, QWERTY_NEIGHBORS,
  expandQueryFuzzy, normalizeFuzzySearchValue,
};
