/* ============================================================
   range-rebase.jsx — keep comment/suggestion ranges in sync
   when the underlying block text is edited.

   Strategy:
   - Compute a single (prefixLen, oldSuffixStart, newSuffixStart)
     "shrink-to-edit" by trimming common prefix + suffix from
     both old and new strings. That defines an edit window
     [delStart .. delEnd) replaced by length insLen.
   - For each anchored range:
       * range fully BEFORE the edit window → unchanged
       * range fully AFTER the edit window → shifted by delta
       * range overlaps the edit window     → mark stale (and
         keep the original range frozen for context)
   - For suggestions: also recheck `original` against the new
     slice; if it no longer matches we mark stale.

   Stale items are kept (we never silently delete user feedback)
   but flagged so the UI can show "verlopen" and acceptSuggestion
   refuses to apply them.
   ============================================================ */

function _diffEditWindow(oldStr, newStr) {
  if (oldStr === newStr) return null;
  const a = oldStr || '';
  const b = newStr || '';
  const aLen = a.length, bLen = b.length;
  // common prefix
  let p = 0;
  const maxP = Math.min(aLen, bLen);
  while (p < maxP && a.charCodeAt(p) === b.charCodeAt(p)) p++;
  // common suffix (from each end, but not into the prefix region)
  let s = 0;
  const maxS = Math.min(aLen - p, bLen - p);
  while (s < maxS && a.charCodeAt(aLen - 1 - s) === b.charCodeAt(bLen - 1 - s)) s++;
  const delStart = p;
  const delEnd   = aLen - s;        // [delStart..delEnd) was deleted from old
  const insLen   = bLen - s - p;    // length inserted into new at delStart
  return { delStart, delEnd, insLen, delta: insLen - (delEnd - delStart) };
}

/** Apply the edit-window to a single anchored range.
 *  Returns:
 *    { range: newRange }                — shifted cleanly
 *    { range: originalRange, stale }    — overlaps edit, keep frozen */
function _shiftRange(range, win) {
  if (!range) return { range };
  if (!win) return { range };
  const { delStart, delEnd, delta } = win;
  let { start, end } = range;
  // entirely before edit
  if (end <= delStart) return { range: { ...range, start, end } };
  // entirely after edit
  if (start >= delEnd) return { range: { ...range, start: start + delta, end: end + delta } };
  // overlap: stale
  return { range, stale: true };
}

/** Rebase all comments + suggestions on a block when ONE field changes.
 *  oldField, newField may be undefined (block didn't have that field). */
function rebaseFeedbackOnFieldEdit(doc, blockId, fieldName, oldField, newField) {
  if (oldField === newField) return doc;
  const win = _diffEditWindow(oldField || '', newField || '');
  if (!win) return doc;

  const matchField = (r) => r && (r.field || 'text') === fieldName;

  const comments = doc.comments.map(c => {
    if (c.blockId !== blockId || !matchField(c.range)) return c;
    const { range, stale } = _shiftRange(c.range, win);
    if (!stale) return { ...c, range };
    return { ...c, range, stale: true };
  });

  const suggestions = doc.suggestions.map(s => {
    if (s.blockId !== blockId || !matchField(s.range)) return s;
    if (s.status !== 'pending') return s; // accepted/rejected stay frozen
    const { range, stale } = _shiftRange(s.range, win);
    if (stale) return { ...s, range, stale: true };
    // Also verify the cleanly-shifted slice still matches `original`;
    // if the user edited *adjacent* characters that happen to fall
    // outside the edit window but still mutate the suggested slice,
    // _diffEditWindow already covered it. This second check guards
    // against off-by-one in unicode/idiosyncratic cases.
    const slice = (newField || '').slice(range.start, range.end);
    if (slice !== s.original) return { ...s, range, stale: true };
    return { ...s, range };
  });

  return { ...doc, comments, suggestions };
}

/** When a block's `items[]` index changes (e.g. list item edited at i),
 *  rebase ranges anchored to `item-i`. */
function rebaseFeedbackOnListEdit(doc, blockId, itemIdx, oldItem, newItem) {
  return rebaseFeedbackOnFieldEdit(doc, blockId, 'item-' + itemIdx, oldItem, newItem);
}

/** Inspect a patch on a block and rebase all relevant ranges.
 *  Returns the updated doc. */
function rebaseFeedbackOnBlockPatch(doc, blockId, oldBlock, patch) {
  if (!oldBlock) return doc;
  let next = doc;
  // Scalar text fields we know about across block types
  const TEXT_FIELDS = ['text', 'title', 'body', 'label', 'cite'];
  for (const f of TEXT_FIELDS) {
    if (patch[f] !== undefined && patch[f] !== oldBlock[f]) {
      next = rebaseFeedbackOnFieldEdit(next, blockId, f, oldBlock[f] || '', patch[f] || '');
    }
  }
  // Items array (numlist/ul)
  if (patch.items !== undefined && Array.isArray(oldBlock.items)) {
    const oldItems = oldBlock.items;
    const newItems = patch.items;
    const max = Math.max(oldItems.length, newItems.length);
    for (let i = 0; i < max; i++) {
      const oldI = oldItems[i] || '';
      const newI = newItems[i] || '';
      if (oldI !== newI) {
        next = rebaseFeedbackOnListEdit(next, blockId, i, oldI, newI);
      }
    }
    // If items count changed, also mark feedback that points beyond newItems.length as stale
    if (newItems.length < oldItems.length) {
      next = {
        ...next,
        comments: next.comments.map(c => {
          if (c.blockId !== blockId) return c;
          const f = c.range?.field || '';
          if (!f.startsWith('item-')) return c;
          const idx = Number(f.slice(5));
          if (idx >= newItems.length) return { ...c, stale: true };
          return c;
        }),
        suggestions: next.suggestions.map(s => {
          if (s.blockId !== blockId) return s;
          const f = s.range?.field || '';
          if (!f.startsWith('item-')) return s;
          const idx = Number(f.slice(5));
          if (idx >= newItems.length && s.status === 'pending') return { ...s, stale: true };
          return s;
        }),
      };
    }
  }
  return next;
}

Object.assign(window, {
  rebaseFeedbackOnFieldEdit,
  rebaseFeedbackOnListEdit,
  rebaseFeedbackOnBlockPatch,
  _diffEditWindow,
});
