import { createLogger } from "../logger.js";
import { computeLcsTable } from "./lcs.js";
const log = createLogger("cache");
const DIFF_LINE_CAP = 5e3;
const DIFF_TOKEN_CAP = 5e4;
const UNIFIED_CONTEXT = 3;
function normalizeLineEndings(text) {
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
function splitLines(text) {
  if (text === "") return [];
  const normalized = normalizeLineEndings(text);
  const lines = normalized.split("\n");
  if (lines.length > 0 && lines[lines.length - 1] === "") {
    lines.pop();
  }
  return lines;
}
function buildEditScript(oldLines, newLines) {
  const m = oldLines.length;
  const n = newLines.length;
  const stride = n + 1;
  const dp = computeLcsTable(oldLines, newLines);
  const ops = [];
  let i = m;
  let j = n;
  while (i > 0 && j > 0) {
    if (oldLines[i - 1] === newLines[j - 1]) {
      ops.push({ type: "equal", oldLine: oldLines[i - 1], newLine: newLines[j - 1] });
      i--;
      j--;
    } else if (dp[(i - 1) * stride + j] >= dp[i * stride + (j - 1)]) {
      ops.push({ type: "delete", oldLine: oldLines[i - 1] });
      i--;
    } else {
      ops.push({ type: "insert", newLine: newLines[j - 1] });
      j--;
    }
  }
  while (i > 0) {
    ops.push({ type: "delete", oldLine: oldLines[i - 1] });
    i--;
  }
  while (j > 0) {
    ops.push({ type: "insert", newLine: newLines[j - 1] });
    j--;
  }
  ops.reverse();
  return ops;
}
function countsFromOps(ops) {
  let added = 0;
  let removed = 0;
  let modified = 0;
  let i = 0;
  while (i < ops.length) {
    if (ops[i].type === "equal") {
      i++;
      continue;
    }
    let dels = 0;
    let ins = 0;
    while (i < ops.length && ops[i].type !== "equal") {
      if (ops[i].type === "delete") dels++;
      else if (ops[i].type === "insert") ins++;
      i++;
    }
    const pair = Math.min(dels, ins);
    modified += pair;
    removed += dels - pair;
    added += ins - pair;
  }
  return { added, removed, modified };
}
function changedCharsFromOps(ops) {
  let total = 0;
  for (const op of ops) {
    if (op.type === "delete") total += op.oldLine.length;
    else if (op.type === "insert") total += op.newLine.length;
  }
  return total;
}
function approximateSummaryForTruncated(oldLines, newLines) {
  const m = oldLines.length;
  const n = newLines.length;
  if (m === 0) return { added_lines: n, removed_lines: 0, modified_lines: 0, total_changed_chars: charLen(newLines) };
  if (n === 0) return { added_lines: 0, removed_lines: m, modified_lines: 0, total_changed_chars: charLen(oldLines) };
  const overlap = Math.min(m, n);
  const modified = overlap;
  const added = Math.max(0, n - overlap);
  const removed = Math.max(0, m - overlap);
  return {
    added_lines: added,
    removed_lines: removed,
    modified_lines: modified,
    total_changed_chars: charLen(oldLines) + charLen(newLines)
  };
}
function charLen(lines) {
  let total = 0;
  for (const l of lines) total += l.length;
  return total;
}
function computeUnifiedDiff(oldText, newText) {
  const oldLines = splitLines(oldText);
  const newLines = splitLines(newText);
  if (oldLines.length > DIFF_LINE_CAP || newLines.length > DIFF_LINE_CAP) {
    log.debug("diff-engine: line cap exceeded, returning approximate summary", {
      oldLineCount: oldLines.length,
      newLineCount: newLines.length
    });
    return {
      diff: "",
      truncated: true,
      summary: approximateSummaryForTruncated(oldLines, newLines)
    };
  }
  const ops = buildEditScript(oldLines, newLines);
  const counts = countsFromOps(ops);
  const summary = {
    added_lines: counts.added,
    removed_lines: counts.removed,
    modified_lines: counts.modified,
    total_changed_chars: changedCharsFromOps(ops)
  };
  if (counts.added === 0 && counts.removed === 0 && counts.modified === 0) {
    return { diff: "", truncated: false, summary };
  }
  const normalized = normalizeEditOrder(ops);
  const diff = renderUnifiedDiff(normalized);
  return { diff, truncated: false, summary };
}
function computeDiffSummaryOnly(oldText, newText) {
  const oldLines = splitLines(oldText);
  const newLines = splitLines(newText);
  if (oldLines.length > DIFF_LINE_CAP || newLines.length > DIFF_LINE_CAP) {
    return {
      diff: "",
      truncated: true,
      summary: approximateSummaryForTruncated(oldLines, newLines)
    };
  }
  const ops = buildEditScript(oldLines, newLines);
  const counts = countsFromOps(ops);
  return {
    diff: "",
    truncated: false,
    summary: {
      added_lines: counts.added,
      removed_lines: counts.removed,
      modified_lines: counts.modified,
      total_changed_chars: changedCharsFromOps(ops)
    }
  };
}
function normalizeEditOrder(ops) {
  const out = [];
  let i = 0;
  while (i < ops.length) {
    if (ops[i].type === "equal") {
      out.push(ops[i]);
      i++;
      continue;
    }
    const dels = [];
    const ins = [];
    while (i < ops.length && ops[i].type !== "equal") {
      if (ops[i].type === "delete") dels.push(ops[i]);
      else ins.push(ops[i]);
      i++;
    }
    out.push(...dels, ...ins);
  }
  return out;
}
function renderUnifiedDiff(ops) {
  const groups = [];
  let oldLine = 1;
  let newLine = 1;
  let pendingContext = [];
  let current = null;
  let trailingEquals = 0;
  const flushCurrent = () => {
    if (current) {
      groups.push(current);
      current = null;
      trailingEquals = 0;
    }
  };
  for (const op of ops) {
    if (op.type === "equal") {
      if (current) {
        current.ops.push(op);
        trailingEquals++;
        if (trailingEquals > 2 * UNIFIED_CONTEXT) {
          const keep = current.ops.length - (trailingEquals - UNIFIED_CONTEXT);
          current.ops = current.ops.slice(0, keep);
          flushCurrent();
          pendingContext = [op];
        }
      } else {
        pendingContext.push(op);
        if (pendingContext.length > UNIFIED_CONTEXT) {
          pendingContext.shift();
        }
      }
      oldLine++;
      newLine++;
    } else {
      if (!current) {
        const leadingContext = pendingContext.slice(-UNIFIED_CONTEXT);
        const oldStart = oldLine - leadingContext.length;
        const newStart = newLine - leadingContext.length;
        current = { ops: [...leadingContext, op], oldStart, newStart };
        pendingContext = [];
      } else {
        current.ops.push(op);
      }
      trailingEquals = 0;
      if (op.type === "delete") oldLine++;
      else newLine++;
    }
  }
  if (current) {
    const tail = current.ops.length;
    let trail = 0;
    for (let k = tail - 1; k >= 0; k--) {
      if (current.ops[k].type === "equal") trail++;
      else break;
    }
    if (trail > UNIFIED_CONTEXT) {
      current.ops = current.ops.slice(0, tail - (trail - UNIFIED_CONTEXT));
    }
    flushCurrent();
  }
  const lines = [];
  lines.push("--- old");
  lines.push("+++ new");
  for (const g of groups) {
    let oldLen = 0;
    let newLen = 0;
    for (const op of g.ops) {
      if (op.type === "equal") {
        oldLen++;
        newLen++;
      } else if (op.type === "delete") {
        oldLen++;
      } else {
        newLen++;
      }
    }
    lines.push(`@@ -${g.oldStart},${oldLen} +${g.newStart},${newLen} @@`);
    for (const op of g.ops) {
      if (op.type === "equal") lines.push(` ${op.oldLine}`);
      else if (op.type === "delete") lines.push(`-${op.oldLine}`);
      else lines.push(`+${op.newLine}`);
    }
  }
  return lines.join("\n");
}
function computeHunks(oldText, newText, granularity) {
  const oldLines = splitLines(oldText);
  const newLines = splitLines(newText);
  if (oldLines.length > DIFF_LINE_CAP || newLines.length > DIFF_LINE_CAP) {
    return {
      hunks: [],
      truncated: true,
      summary: approximateSummaryForTruncated(oldLines, newLines)
    };
  }
  if (granularity === "section") {
    return computeSectionHunks(oldText, newText);
  }
  if (granularity === "word") {
    return computeWordHunks(oldText, newText);
  }
  const ops = buildEditScript(oldLines, newLines);
  const hunks = [];
  let i = 0;
  while (i < ops.length) {
    if (ops[i].type === "equal") {
      i++;
      continue;
    }
    const removes = [];
    const adds = [];
    while (i < ops.length && ops[i].type !== "equal") {
      const op = ops[i];
      if (op.type === "delete") removes.push(op.oldLine);
      else if (op.type === "insert") adds.push(op.newLine);
      i++;
    }
    if (removes.length > 0 && adds.length > 0) {
      hunks.push({ before: removes.join("\n"), after: adds.join("\n"), change_type: "modified" });
    } else if (removes.length > 0) {
      hunks.push({ before: removes.join("\n"), after: "", change_type: "removed" });
    } else if (adds.length > 0) {
      hunks.push({ before: "", after: adds.join("\n"), change_type: "added" });
    }
  }
  const counts = countsFromOps(ops);
  const summary = {
    added_lines: counts.added,
    removed_lines: counts.removed,
    modified_lines: counts.modified,
    total_changed_chars: changedCharsFromOps(ops)
  };
  return { hunks, truncated: false, summary };
}
function tokenizeWords(text) {
  if (!text) return [];
  return text.match(/\s+|[^\s]+/g) ?? [];
}
function computeWordHunks(oldText, newText) {
  const oldTokens = tokenizeWords(oldText);
  const newTokens = tokenizeWords(newText);
  if (oldTokens.length > DIFF_TOKEN_CAP || newTokens.length > DIFF_TOKEN_CAP) {
    log.debug("diff-engine: word-token cap exceeded, falling back to line granularity", {
      oldTokenCount: oldTokens.length,
      newTokenCount: newTokens.length,
      cap: DIFF_TOKEN_CAP
    });
    const oldLines2 = splitLines(oldText);
    const newLines2 = splitLines(newText);
    if (oldLines2.length > DIFF_LINE_CAP || newLines2.length > DIFF_LINE_CAP) {
      return {
        hunks: [],
        truncated: true,
        summary: approximateSummaryForTruncated(oldLines2, newLines2)
      };
    }
    const lineFallback = computeHunks(oldText, newText, "line");
    return {
      hunks: lineFallback.hunks,
      truncated: true,
      summary: lineFallback.summary
    };
  }
  const ops = buildEditScript(oldTokens, newTokens);
  const hunks = [];
  let i = 0;
  while (i < ops.length) {
    if (ops[i].type === "equal") {
      i++;
      continue;
    }
    const removes = [];
    const adds = [];
    while (i < ops.length && ops[i].type !== "equal") {
      const op = ops[i];
      if (op.type === "delete") removes.push(op.oldLine);
      else if (op.type === "insert") adds.push(op.newLine);
      i++;
    }
    const beforeStr = removes.join("");
    const afterStr = adds.join("");
    if (beforeStr.trim().length === 0 && afterStr.trim().length === 0) continue;
    if (removes.length > 0 && adds.length > 0) {
      hunks.push({ before: beforeStr, after: afterStr, change_type: "modified" });
    } else if (removes.length > 0) {
      hunks.push({ before: beforeStr, after: "", change_type: "removed" });
    } else if (adds.length > 0) {
      hunks.push({ before: "", after: afterStr, change_type: "added" });
    }
  }
  const oldLines = splitLines(oldText);
  const newLines = splitLines(newText);
  const lineOps = buildEditScript(oldLines, newLines);
  const counts = countsFromOps(lineOps);
  const summary = {
    added_lines: counts.added,
    removed_lines: counts.removed,
    modified_lines: counts.modified,
    total_changed_chars: changedCharsFromOps(lineOps)
  };
  return { hunks, truncated: false, summary };
}
const HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
function splitSections(text) {
  const lines = splitLines(text);
  const sections = [];
  let current = { title: void 0, body: "" };
  const bodyLines = [];
  let hasSeenHeading = false;
  const flush = () => {
    current.body = bodyLines.join("\n");
    sections.push(current);
    bodyLines.length = 0;
  };
  for (const line of lines) {
    const m = HEADING_RE.exec(line);
    if (m) {
      if (hasSeenHeading || bodyLines.length > 0) {
        flush();
      }
      current = { title: m[2], body: "" };
      hasSeenHeading = true;
    } else {
      bodyLines.push(line);
    }
  }
  flush();
  return sections;
}
function computeSectionHunks(oldText, newText) {
  const oldSections = splitSections(oldText);
  const newSections = splitSections(newText);
  const newByTitle = /* @__PURE__ */ new Map();
  for (const s of newSections) {
    if (s.title) {
      const arr = newByTitle.get(s.title) ?? [];
      arr.push(s);
      newByTitle.set(s.title, arr);
    }
  }
  const consumedNew = /* @__PURE__ */ new Set();
  const hunks = [];
  let totalAdded = 0;
  let totalRemoved = 0;
  let totalModified = 0;
  let totalChars = 0;
  const oldPrelude = oldSections.find((s) => !s.title);
  const newPrelude = newSections.find((s) => !s.title);
  if (oldPrelude || newPrelude) {
    const before = oldPrelude?.body ?? "";
    const after = newPrelude?.body ?? "";
    if (before !== after) {
      const inner = computeDiffSummaryOnly(before, after);
      if (before && after) {
        hunks.push({ before, after, change_type: "modified" });
      } else if (after) {
        hunks.push({ before: "", after, change_type: "added" });
      } else {
        hunks.push({ before, after: "", change_type: "removed" });
      }
      totalAdded += inner.summary.added_lines;
      totalRemoved += inner.summary.removed_lines;
      totalModified += inner.summary.modified_lines;
      totalChars += inner.summary.total_changed_chars;
    }
  }
  for (const oldSec of oldSections) {
    if (!oldSec.title) continue;
    const candidates = newByTitle.get(oldSec.title);
    if (!candidates || candidates.length === 0) {
      hunks.push({
        section_title: oldSec.title,
        before: oldSec.body,
        after: "",
        change_type: "removed"
      });
      totalRemoved += splitLines(oldSec.body).length;
      totalChars += oldSec.body.length;
      continue;
    }
    const newSec = candidates.shift();
    consumedNew.add(newSec);
    if (oldSec.body === newSec.body) {
      continue;
    }
    const inner = computeDiffSummaryOnly(oldSec.body, newSec.body);
    hunks.push({
      section_title: oldSec.title,
      before: oldSec.body,
      after: newSec.body,
      change_type: "modified"
    });
    totalAdded += inner.summary.added_lines;
    totalRemoved += inner.summary.removed_lines;
    totalModified += inner.summary.modified_lines;
    totalChars += inner.summary.total_changed_chars;
  }
  for (const newSec of newSections) {
    if (!newSec.title) continue;
    if (consumedNew.has(newSec)) continue;
    hunks.push({
      section_title: newSec.title,
      before: "",
      after: newSec.body,
      change_type: "added"
    });
    totalAdded += splitLines(newSec.body).length;
    totalChars += newSec.body.length;
  }
  return {
    hunks,
    truncated: false,
    summary: {
      added_lines: totalAdded,
      removed_lines: totalRemoved,
      modified_lines: totalModified,
      total_changed_chars: totalChars
    }
  };
}
function computeDiffEnvelope(input) {
  const { oldMarkdown, newMarkdown, output, granularity } = input;
  if (oldMarkdown === newMarkdown) {
    return {
      changed: false,
      summary: {
        added_lines: 0,
        removed_lines: 0,
        modified_lines: 0,
        total_changed_chars: 0
      }
    };
  }
  if (output === "summary") {
    const u2 = computeUnifiedDiff(oldMarkdown, newMarkdown);
    const out2 = {
      changed: u2.summary.added_lines > 0 || u2.summary.removed_lines > 0 || u2.summary.modified_lines > 0 || u2.truncated,
      summary: u2.summary
    };
    if (u2.truncated) out2.truncated = true;
    return out2;
  }
  if (output === "hunks") {
    const h = computeHunks(oldMarkdown, newMarkdown, granularity);
    const changed2 = h.hunks.length > 0 || h.summary.added_lines > 0 || h.summary.removed_lines > 0 || h.summary.modified_lines > 0;
    const out2 = {
      changed: changed2,
      hunks: h.hunks,
      summary: h.summary
    };
    if (h.truncated) out2.truncated = true;
    return out2;
  }
  const u = computeUnifiedDiff(oldMarkdown, newMarkdown);
  const changed = u.summary.added_lines > 0 || u.summary.removed_lines > 0 || u.summary.modified_lines > 0 || u.truncated;
  const out = {
    changed,
    summary: u.summary
  };
  if (!u.truncated) out.unified_diff = u.diff;
  if (u.truncated) out.truncated = true;
  return out;
}
export {
  DIFF_LINE_CAP,
  DIFF_TOKEN_CAP,
  computeDiffEnvelope,
  computeDiffSummaryOnly,
  computeHunks,
  computeUnifiedDiff
};
//# sourceMappingURL=diff-engine.js.map