// A line diff, written here rather than pulled in. jsdiff would do this, but it is a runtime dependency
// shipped to the browser to run a textbook LCS, and the LCS is shorter than the integration would be.
//
// Deliberately no line numbers. An Edit's `old_string` / `new_string` are fragments with no file
// position attached, so any number we printed beside them would be a number we invented — and a
// plausible-looking wrong line number is worse than none.

export type DiffLine = { kind: 'add' | 'del' | 'ctx'; text: string };

/**
 * The LCS table is O(n*m); at the cap that is ~640k int32s, which is fine, and past it the panel was
 * never going to be readable anyway. `null` means "too big to diff" and the caller falls back to
 * showing the raw strings.
 */
const MAX_LINES = 800;

export function lineDiff(before: string, after: string): DiffLine[] | null {
  const a = before.split('\n');
  const b = after.split('\n');
  if (a.length > MAX_LINES || b.length > MAX_LINES) return null;

  const n = a.length;
  const m = b.length;
  const width = m + 1;
  const lcs = new Int32Array((n + 1) * width);

  for (let i = n - 1; i >= 0; i--) {
    for (let j = m - 1; j >= 0; j--) {
      lcs[i * width + j] =
        a[i] === b[j]
          ? lcs[(i + 1) * width + (j + 1)]! + 1
          : Math.max(lcs[(i + 1) * width + j]!, lcs[i * width + (j + 1)]!);
    }
  }

  const out: DiffLine[] = [];
  let i = 0;
  let j = 0;
  while (i < n && j < m) {
    if (a[i] === b[j]) {
      out.push({ kind: 'ctx', text: a[i]! });
      i++;
      j++;
    } else if (lcs[(i + 1) * width + j]! >= lcs[i * width + (j + 1)]!) {
      out.push({ kind: 'del', text: a[i]! });
      i++;
    } else {
      out.push({ kind: 'add', text: b[j]! });
      j++;
    }
  }
  while (i < n) out.push({ kind: 'del', text: a[i++]! });
  while (j < m) out.push({ kind: 'add', text: b[j++]! });
  return out;
}

/** Counts for the collapsed row, so the size of an edit is visible without expanding it. */
export function diffStat(lines: DiffLine[]): { added: number; removed: number } {
  let added = 0;
  let removed = 0;
  for (const line of lines) {
    if (line.kind === 'add') added++;
    else if (line.kind === 'del') removed++;
  }
  return { added, removed };
}

/**
 * What an Edit/Write tool call is actually changing, or null when the call is neither. `Write` has no
 * prior text on the wire, so it reads as an all-addition diff — which is what writing a new file is.
 */
export function editDiff(toolName: string, input: Record<string, unknown>): DiffLine[] | null {
  if (toolName === 'Edit') {
    const before = input.old_string;
    const after = input.new_string;
    if (typeof before !== 'string' || typeof after !== 'string') return null;
    return lineDiff(before, after);
  }
  if (toolName === 'Write') {
    const content = input.content;
    if (typeof content !== 'string') return null;
    // Built directly rather than diffed against '': `''.split('\n')` is `['']`, not `[]`, so an empty
    // "before" side would open every new file with a phantom deleted blank line.
    const lines = content.split('\n');
    if (lines.length > MAX_LINES) return null;
    return lines.map((text) => ({ kind: 'add' as const, text }));
  }
  return null;
}
