"use strict";

/**
 * 文案与 ASR 时间轴对齐（对齐 Electron ZimuShengcheng / Levenshtein）
 */

const PUNCTUATION_RE =
  /[，。！？、；：\u201C\u201D"「」『』（）【】《》,.!?;:'"]/;

const DEFAULT_CHAR_DURATION_MS = 150;
const TIME_GAP_SPLIT_THRESHOLD_MS = 500;

function levenshteinDistance(a, b) {
  const n = a.length;
  const m = b.length;
  const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
  for (let i = 0; i <= n; i++) dp[i][0] = i;
  for (let j = 0; j <= m; j++) dp[0][j] = j;
  for (let i = 1; i <= n; i++) {
    for (let j = 1; j <= m; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
    }
  }
  return dp[n][m];
}

function findBestMatch(userText, asrText) {
  const n = userText.length;
  const m = asrText.length;
  const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
  const bt = Array.from({ length: n + 1 }, () => Array(m + 1).fill(""));
  for (let i = 0; i <= n; i++) {
    dp[i][0] = i;
    bt[i][0] = "delete";
  }
  for (let j = 0; j <= m; j++) {
    dp[0][j] = j;
    bt[0][j] = "insert";
  }
  for (let i = 1; i <= n; i++) {
    for (let j = 1; j <= m; j++) {
      const cost = userText[i - 1] === asrText[j - 1] ? 0 : 1;
      const del = dp[i - 1][j] + 1;
      const ins = dp[i][j - 1] + 1;
      const sub = dp[i - 1][j - 1] + cost;
      const best = Math.min(del, ins, sub);
      dp[i][j] = best;
      if (best === del) bt[i][j] = "delete";
      else if (best === ins) bt[i][j] = "insert";
      else if (cost === 0) bt[i][j] = "match";
      else bt[i][j] = "replace";
    }
  }
  const alignments = [];
  let i = n;
  let j = m;
  while (i > 0 || j > 0) {
    const op = bt[i][j];
    if (op === "match") {
      alignments.unshift({
        userChar: userText[i - 1],
        asrChar: asrText[j - 1],
        operation: "match",
        asrIndex: j - 1,
      });
      i -= 1;
      j -= 1;
    } else if (op === "replace") {
      alignments.unshift({
        userChar: userText[i - 1],
        asrChar: asrText[j - 1],
        operation: "replace",
        asrIndex: j - 1,
      });
      i -= 1;
      j -= 1;
    } else if (op === "delete") {
      alignments.unshift({
        userChar: userText[i - 1],
        asrChar: "",
        operation: "delete",
        asrIndex: j,
      });
      i -= 1;
    } else {
      alignments.unshift({
        userChar: "",
        asrChar: asrText[j - 1],
        operation: "insert",
        asrIndex: j - 1,
      });
      j -= 1;
    }
  }
  const distance = dp[n][m];
  const similarity = 1 - distance / Math.max(n, m, 1);
  return { alignments, distance, similarity };
}

function buildSegmentCharTimings(segment) {
  const text = segment.text || "";
  const len = text.length;
  if (len === 0) return [];
  const step = (segment.end - segment.start) / Math.max(len, 1);
  const fallback = Array.from({ length: len }, (_, idx) => ({
    start: segment.start + idx * step,
    end: segment.start + (idx + 1) * step,
  }));
  const words = Array.isArray(segment.words) ? segment.words : [];
  if (words.length === 0) return fallback;

  const mapped = Array(len).fill(null);
  let searchFrom = 0;
  for (const w of words) {
    const word = String(w?.word ?? w?.text ?? "");
    const start = Number(w?.start ?? w?.beginTime ?? w?.begin_time);
    const end = Number(w?.end ?? w?.endTime ?? w?.end_time);
    if (!word || !Number.isFinite(start) || !Number.isFinite(end) || end <= start) continue;
    const pos = text.indexOf(word, searchFrom);
    if (pos === -1) continue;
    const wordStep = (end - start) / Math.max(word.length, 1);
    for (let k = 0; k < word.length && pos + k < len; k++) {
      mapped[pos + k] = { start: start + k * wordStep, end: start + (k + 1) * wordStep };
    }
    searchFrom = pos + word.length;
  }
  return mapped.map((item, idx) => item || fallback[idx]);
}

function buildCharTimeMap(segments) {
  const map = new Map();
  let index = 0;
  for (const segment of segments) {
    for (const timing of buildSegmentCharTimings(segment)) {
      map.set(index, timing);
      index += 1;
    }
  }
  return map;
}

function mapToTimestamps(alignments, charMap) {
  const segments = [];
  let text = "";
  let start = 0;
  let end = 0;
  let corrected = false;
  let isFirstChar = true;

  for (let y = 0; y < alignments.length; y++) {
    const row = alignments[y];
    const timing =
      row.asrIndex != null && row.asrIndex >= 0 ? charMap.get(row.asrIndex) : null;
    const isPunct = row.userChar && PUNCTUATION_RE.test(row.userChar);

    if (row.operation === "match" || row.operation === "replace") {
      if (!isPunct) text += row.userChar;
      if (row.operation === "replace") corrected = true;
      if (timing) {
        if (isFirstChar) {
          start = timing.start;
          isFirstChar = false;
        }
        end = timing.end;
      }
    } else if (row.operation === "insert") {
      if (timing) {
        if (isFirstChar) {
          start = timing.start;
          isFirstChar = false;
        }
        end = timing.end;
      }
    } else if (row.operation === "delete") {
      if (!isPunct) {
        text += row.userChar;
        const t = isFirstChar ? start : end;
        if (isFirstChar) {
          start = t;
          isFirstChar = false;
        }
        if (!timing) end = t + DEFAULT_CHAR_DURATION_MS;
      }
      corrected = true;
    }

    const isLast = y === alignments.length - 1;
    const nextRow = alignments[y + 1];
    const nextTiming =
      nextRow && nextRow.asrIndex != null && nextRow.asrIndex >= 0
        ? charMap.get(nextRow.asrIndex)
        : null;
    const gapSplit =
      !isLast && timing && nextTiming && nextTiming.start - end > TIME_GAP_SPLIT_THRESHOLD_MS;

    if ((isLast || isPunct || gapSplit) && text.length > 0) {
      if (end <= start) end = start + text.length * DEFAULT_CHAR_DURATION_MS;
      segments.push({
        text,
        start,
        end,
        corrected,
      });
      start = end;
      text = "";
      corrected = false;
      isFirstChar = true;
    }
  }

  if (text.length > 0) {
    if (end <= start) end = start + text.length * DEFAULT_CHAR_DURATION_MS;
    segments.push({ text, start, end, corrected });
  }
  return segments;
}

/**
 * @param {string} userText
 * @param {Array<{ text: string, start: number, end: number, words?: unknown[] }>} asrSegments
 */
function correctTextWithLevenshtein(userText, asrSegments) {
  const user = String(userText || "").replace(/\s+/g, "");
  const list = Array.isArray(asrSegments) ? asrSegments : [];
  if (!user || list.length === 0) {
    return { segments: [], similarity: 0 };
  }

  const asrFullText = list.map((s) => s.text || "").join("");
  const match = findBestMatch(user, asrFullText);
  const charMap = buildCharTimeMap(list);
  const corrected = mapToTimestamps(match.alignments, charMap);

  const MIN_GAP_MS = 50;
  const out = corrected
    .map((seg) => ({
      text: String(seg.text || "").trim(),
      start: Math.round(seg.start),
      end: Math.round(seg.end),
    }))
    .filter((seg) => seg.text);

  for (let i = 0; i < out.length - 1; i++) {
    const cur = out[i];
    const next = out[i + 1];
    if (cur.end >= next.start) {
      cur.end = Math.max(cur.start + MIN_GAP_MS, next.start - MIN_GAP_MS);
    }
    if (cur.end <= cur.start) cur.end = cur.start + MIN_GAP_MS;
  }

  return { segments: out, similarity: match.similarity };
}

function similarity(a, b) {
  const max = Math.max(a.length, b.length);
  if (max === 0) return 1;
  return 1 - levenshteinDistance(a, b) / max;
}

module.exports = {
  correctTextWithLevenshtein,
  similarity,
};
