/**
 * Pure ProseMirror paragraph/text lookup helpers shared by the React and
 * Vue adapters.
 *
 * "Vanilla view" text extraction skips text inside `insertion` marks
 * (tracked-change additions that aren't accepted yet) so the agent's view
 * of the document matches what `addComment` / `proposeChange` can anchor
 * to. Tracked deletions stay included — they're still in the doc until
 * accepted.
 *
 * Previously duplicated at
 * `packages/react/src/components/DocxEditor/internals/{pmAnchors,vanillaText}.ts`
 * and `packages/vue/src/utils/paraTextHelpers.ts`. Both adapters now
 * re-export from here.
 */

import type { Node as PMNode } from 'prosemirror-model';

/**
 * PM position range for a paragraph identified by Word `w14:paraId`.
 * Stable across edits — inverse of `formatContentForLLM`'s `[paraId]` line tag.
 *
 * Returns inclusive `from` (position before the textblock) and exclusive
 * `to` (`from + nodeSize`). Text content lives in `[from + 1, to - 1]`.
 */
export function findParaIdRange(doc: PMNode, paraId: string): { from: number; to: number } | null {
  if (!paraId || !paraId.trim()) return null;
  let result: { from: number; to: number } | null = null;
  doc.descendants((node, pos) => {
    if (result !== null) return false;
    if (node.isTextblock && node.attrs?.paraId === paraId) {
      result = { from: pos, to: pos + node.nodeSize };
      return false;
    }
    return true;
  });
  return result;
}

/** Text of a single PM node (typically a paragraph), vanilla view. */
export function getVanillaNodeText(node: PMNode): string {
  const parts: string[] = [];
  node.descendants((child) => {
    if (!child.isText || !child.text) return true;
    if (child.marks.some((m) => m.type.name === 'insertion')) return false;
    parts.push(child.text);
    return true;
  });
  return parts.join('');
}

/** Text between two doc positions, vanilla view. */
export function getVanillaTextBetween(doc: PMNode, from: number, to: number): string {
  if (from >= to) return '';
  const parts: string[] = [];
  doc.nodesBetween(from, to, (child, pos) => {
    if (!child.isText || !child.text) return;
    if (child.marks.some((m) => m.type.name === 'insertion')) return;
    const start = Math.max(from, pos);
    const end = Math.min(to, pos + child.text.length);
    if (start < end) parts.push(child.text.slice(start - pos, end - pos));
  });
  return parts.join('');
}

/**
 * Find `searchText` within a PM paragraph range and return its position.
 *
 * Returns null if:
 *   - searchText is empty
 *   - searchText is not found
 *   - searchText appears more than once (ambiguous; caller disambiguates)
 *
 * The fullText is built from PM text nodes only and matches the vanilla
 * view the agent reads via `read_document`: tracked insertions are
 * excluded (not in the doc yet), tracked deletions are included (still
 * in the doc until accepted), and comment markers are stripped.
 */
export function findTextInPmParagraph(
  doc: PMNode,
  paragraphFrom: number,
  paragraphTo: number,
  searchText: string
): { from: number; to: number } | null {
  if (!searchText) return null;

  let fullText = '';
  const textPositions: { pos: number; len: number }[] = [];

  doc.nodesBetween(paragraphFrom, paragraphTo, (node, pos) => {
    if (!node.isText || !node.text) return;
    if (node.marks.some((m) => m.type.name === 'insertion')) return;
    textPositions.push({ pos, len: node.text.length });
    fullText += node.text;
  });

  const firstMatch = fullText.indexOf(searchText);
  if (firstMatch === -1) return null;
  // Reject ambiguous searches — the LLM gets a clearer error than a silent mistarget.
  const secondMatch = fullText.indexOf(searchText, firstMatch + 1);
  if (secondMatch !== -1) return null;

  // Map string offset back to PM position.
  let charOffset = 0;
  let fromPos = paragraphFrom;
  let toPos = paragraphFrom;

  for (const tp of textPositions) {
    const segEnd = charOffset + tp.len;
    if (charOffset <= firstMatch && firstMatch < segEnd) {
      fromPos = tp.pos + (firstMatch - charOffset);
    }
    if (charOffset <= firstMatch + searchText.length && firstMatch + searchText.length <= segEnd) {
      toPos = tp.pos + (firstMatch + searchText.length - charOffset);
      break;
    }
    charOffset = segEnd;
  }

  return { from: fromPos, to: toPos };
}
