// tools/fuzzy_match.ts
/**
 * 九级模糊匹配引擎
 * 按优先级依次尝试 9 种策略，首个成功即返回。
 */

export interface FuzzyMatchResult {
  strategy: string;
  startIndex: number;
  endIndex: number;
  matchedText: string;
}

type Strategy = {
  name: string;
  find: (content: string, search: string) => FuzzyMatchResult[];
};

/** 在 content 中查找所有匹配 search 的位置，按策略优先级 */
export function fuzzyFindAll(
  content: string,
  search: string,
): FuzzyMatchResult[] {
  for (const strategy of strategies) {
    const results = strategy.find(content, search);
    if (results.length > 0) return results;
  }
  return [];
}

// --- 辅助函数 ---

/** Levenshtein 距离 */
function levenshtein(a: string, b: string): number {
  const m = a.length,
    n = b.length;
  const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
    Array.from({ length: n + 1 }, (_, j) =>
      i === 0 ? j : j === 0 ? i : 0,
    ),
  );
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      dp[i][j] =
        a[i - 1] === b[j - 1]
          ? dp[i - 1][j - 1]
          : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
    }
  }
  return dp[m][n];
}

/** 两字符串相似度 0-1 */
function similarity(a: string, b: string): number {
  if (a === b) return 1;
  const maxLen = Math.max(a.length, b.length);
  if (maxLen === 0) return 1;
  return 1 - levenshtein(a, b) / maxLen;
}

/** 将 content 按 normalizer 转换后查找 search，映射回原始索引 */
function findByNormalization(
  content: string,
  search: string,
  normalizer: (s: string) => string,
  strategyName: string,
): FuzzyMatchResult[] {
  const normContent = normalizer(content);
  const normSearch = normalizer(search);
  if (normSearch.length === 0) return [];

  const charMap = buildCharMap(content, normContent);

  const results: FuzzyMatchResult[] = [];
  let pos = 0;
  while (true) {
    const idx = normContent.indexOf(normSearch, pos);
    if (idx === -1) break;
    const endIdx = idx + normSearch.length;
    const origStart = charMap[idx] ?? 0;
    const origEnd =
      endIdx < charMap.length ? charMap[endIdx] : content.length;
    results.push({
      strategy: strategyName,
      startIndex: origStart,
      endIndex: origEnd,
      matchedText: content.slice(origStart, origEnd),
    });
    pos = idx + 1;
  }
  return results;
}

function buildCharMap(original: string, normalized: string): number[] {
  const origLines = original.split('\n');
  const normLines = normalized.split('\n');

  if (origLines.length === normLines.length) {
    const map: number[] = [];
    let origOffset = 0;
    let normOffset = 0;
    for (let i = 0; i < normLines.length; i++) {
      const origLen = origLines[i].length;
      const normLen = normLines[i].length;
      for (let j = 0; j < normLen; j++) {
        map[normOffset + j] =
          origOffset +
          Math.round((j / Math.max(normLen, 1)) * origLen);
      }
      if (i < normLines.length - 1) {
        map[normOffset + normLen] = origOffset + origLen;
      }
      origOffset += origLen + 1;
      normOffset += normLen + 1;
    }
    return map;
  }

  const map: number[] = [];
  const ratio = original.length / Math.max(normalized.length, 1);
  for (let i = 0; i < normalized.length; i++) {
    map[i] = Math.round(i * ratio);
  }
  return map;
}

// --- 9 种策略 ---

const strategies: Strategy[] = [
  // 1. exact — 精确匹配
  {
    name: 'exact',
    find(content, search) {
      const results: FuzzyMatchResult[] = [];
      let pos = 0;
      while (true) {
        const idx = content.indexOf(search, pos);
        if (idx === -1) break;
        results.push({
          strategy: 'exact',
          startIndex: idx,
          endIndex: idx + search.length,
          matchedText: search,
        });
        pos = idx + 1;
      }
      return results;
    },
  },

  // 2. line_trimmed — 每行 trim 后匹配
  {
    name: 'line_trimmed',
    find(content, search) {
      return findByNormalization(
        content,
        search,
        s => s.split('\n').map(l => l.trim()).join('\n'),
        'line_trimmed',
      );
    },
  },

  // 3. whitespace_normalized — 所有空白压缩为单空格
  {
    name: 'whitespace_normalized',
    find(content, search) {
      return findByNormalization(
        content,
        search,
        s => s.replace(/\s+/g, ' '),
        'whitespace_normalized',
      );
    },
  },

  // 4. indent_flexible — 去除行首缩进
  {
    name: 'indent_flexible',
    find(content, search) {
      return findByNormalization(
        content,
        search,
        s => s.split('\n').map(l => l.trimStart()).join('\n'),
        'indent_flexible',
      );
    },
  },

  // 5. escape_normalized — 转义字符还原
  {
    name: 'escape_normalized',
    find(content, search) {
      const norm = (s: string) =>
        s.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
      return findByNormalization(content, search, norm, 'escape_normalized');
    },
  },

  // 6. trimmed_boundary — 首尾行 trim
  {
    name: 'trimmed_boundary',
    find(content, search) {
      const norm = (s: string) => {
        const lines = s.split('\n');
        if (lines.length > 0) lines[0] = lines[0].trim();
        if (lines.length > 1) lines[lines.length - 1] = lines[lines.length - 1].trim();
        return lines.join('\n');
      };
      return findByNormalization(content, search, norm, 'trimmed_boundary');
    },
  },

  // 7. unicode_normalized — Unicode 标点归一化
  {
    name: 'unicode_normalized',
    find(content, search) {
      const norm = (s: string) =>
        s
          .replace(/[\u2018\u2019\u201A]/g, "'")
          .replace(/[\u201C\u201D\u201E]/g, '"')
          .replace(/[\u2013\u2014]/g, '--')
          .replace(/\u2026/g, '...');
      return findByNormalization(content, search, norm, 'unicode_normalized');
    },
  },

  // 8. block_anchor — 首尾行锚定 + 中间相似度
  {
    name: 'block_anchor',
    find(content, search) {
      const searchLines = search.split('\n');
      if (searchLines.length < 3) return [];
      const firstLine = searchLines[0].trim();
      const lastLine = searchLines[searchLines.length - 1].trim();
      const contentLines = content.split('\n');
      const results: FuzzyMatchResult[] = [];

      for (let i = 0; i < contentLines.length; i++) {
        if (contentLines[i].trim() !== firstLine) continue;
        for (
          let j = i + searchLines.length - 1;
          j < contentLines.length && j < i + searchLines.length + 5;
          j++
        ) {
          if (contentLines[j].trim() !== lastLine) continue;
          const candidateMiddle = contentLines.slice(i + 1, j).join('\n');
          const searchMiddle = searchLines.slice(1, -1).join('\n');
          if (similarity(candidateMiddle, searchMiddle) >= 0.6) {
            const startIndex =
              contentLines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
            const matchedText = contentLines.slice(i, j + 1).join('\n');
            results.push({
              strategy: 'block_anchor',
              startIndex,
              endIndex: startIndex + matchedText.length,
              matchedText,
            });
          }
        }
      }
      return results;
    },
  },

  // 9. context_aware — 逐行相似度滑动窗口
  {
    name: 'context_aware',
    find(content, search) {
      const searchLines = search.split('\n');
      const contentLines = content.split('\n');
      if (searchLines.length === 0) return [];
      const results: FuzzyMatchResult[] = [];

      for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
        let matchCount = 0;
        for (let j = 0; j < searchLines.length; j++) {
          if (
            similarity(
              contentLines[i + j].trim(),
              searchLines[j].trim(),
            ) >= 0.8
          )
            matchCount++;
        }
        if (matchCount / searchLines.length >= 0.5) {
          const startIndex =
            contentLines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
          const matchedText = contentLines
            .slice(i, i + searchLines.length)
            .join('\n');
          results.push({
            strategy: 'context_aware',
            startIndex,
            endIndex: startIndex + matchedText.length,
            matchedText,
          });
        }
      }
      return results;
    },
  },
];
