/**
 * Map comment-mark / tracked-change positions in the ProseMirror document
 * to vertical pixel offsets inside the visible scroll container, so the
 * comments + tracked-changes sidebar can render markers aligned with
 * their anchor text. Uses caret-position lookup for paragraphs/images
 * and walks table fragments row-by-row for table content.
 */

import type { EditorView } from 'prosemirror-view';
import { getCaretPosition, getPageTop } from '@eigenpal/docx-editor-core/layout-bridge';
import type {
  FlowBlock,
  Layout,
  Measure,
  TableBlock,
  TableMeasure,
} from '@eigenpal/docx-editor-core/layout-engine';
import { VIEWPORT_PADDING_TOP } from './styles';

/**
 * Compute anchor Y positions for comments/tracked-changes sidebar.
 * Uses getCaretPosition for paragraphs/images; for table content, finds
 * the containing fragment and drills into rows for exact Y offset.
 * Returns a Map of "comment-{id}" / "revision-{revisionId}" → scroll-container Y.
 */
export function computeAnchorPositions(
  pmView: EditorView | null,
  layout: Layout,
  blocks: FlowBlock[],
  measures: Measure[],
  renderedPageGap: number
): Map<string, number> {
  const positions = new Map<string, number>();
  if (!pmView?.state) return positions;

  const { doc: pmDoc, schema } = pmView.state;
  const commentType = schema.marks.comment;
  const insertionType = schema.marks.insertion;
  const deletionType = schema.marks.deletion;
  if (!commentType && !insertionType && !deletionType) return positions;

  const seen = new Set<string>();
  // Offset from layout coords to scroll-container coords:
  // viewport paddingTop + pages container padding (CSS padding = pageGap)
  const contentOffset = VIEWPORT_PADDING_TOP + renderedPageGap;

  const registerKey = (key: string, pos: number) => {
    if (seen.has(key)) return;
    seen.add(key);

    // Try exact position (paragraphs/images)
    const caret = getCaretPosition(layout, blocks, measures, pos);
    if (caret) {
      positions.set(key, caret.y + contentOffset);
      return;
    }

    // Fallback: find containing fragment (tables, etc.) by PM position
    for (let pi = 0; pi < layout.pages.length; pi++) {
      const page = layout.pages[pi];
      for (const frag of page.fragments) {
        const fStart = frag.pmStart ?? 0;
        const fEnd = (frag as { pmEnd?: number }).pmEnd ?? fStart;
        if (pos < fStart || pos > fEnd) continue;

        const rowOffsetY =
          frag.kind === 'table' ? getTableRowOffset(blocks, measures, frag, pos) : 0;
        positions.set(key, frag.y + rowOffsetY + getPageTop(layout, pi) + contentOffset);
        return;
      }
    }
  };

  pmDoc.descendants((node, pos) => {
    // Structural tracked-change attrs on non-text nodes (whole-table insert,
    // row insert/delete, cell insert, paragraph-break tracked, etc). Without
    // these, an empty inserted table has no anchor — the sidebar's
    // hasPositions check stays false and the whole rail renders at opacity 0.
    //
    // The attrs use three different shapes for the revisionId:
    //   • flat       — trIns / trDel / pPrIns / pPrDel: `{ revisionId, ... }`
    //   • nested     — cellMarker: `{ kind, info: { revisionId, ... } }`
    //   • array+info — *PrChange (paragraph/row/cell/table): `[{ info: { id } }, ...]`
    // Pre-fix all three by extracting the revisionId at registration time.
    const attrs = node.attrs as Record<string, unknown> | undefined;
    if (attrs) {
      const flat = [attrs.trIns, attrs.trDel, attrs.pPrIns, attrs.pPrDel];
      for (const entry of flat) {
        const revId = (entry as { revisionId?: unknown } | null | undefined)?.revisionId;
        if (typeof revId === 'number') registerKey(`revision-${revId}`, pos);
      }
      const cellMarker = attrs.cellMarker as { info?: { revisionId?: unknown } } | null;
      const cellRev = cellMarker?.info?.revisionId;
      if (typeof cellRev === 'number') registerKey(`revision-${cellRev}`, pos);
      const propChangeArrays = [
        attrs.pPrChange,
        attrs.trPrChange,
        attrs.tcPrChange,
        attrs.tblPrChange,
      ];
      for (const arr of propChangeArrays) {
        if (!Array.isArray(arr)) continue;
        for (const entry of arr as Array<{ info?: { id?: unknown } }>) {
          const id = entry?.info?.id;
          if (typeof id === 'number') registerKey(`revision-${id}`, pos);
        }
      }
    }

    // Text AND inline atoms (image, shape) can carry comment / tracked-change
    // marks, so an inserted picture's card gets a sidebar anchor like inserted
    // text. Without this an image-only change has no positioned card.
    if (!node.isInline) return;
    for (const mark of node.marks) {
      let key: string | null = null;
      if (commentType && mark.type === commentType) {
        key = `comment-${mark.attrs.commentId}`;
      } else if (
        (insertionType && mark.type === insertionType) ||
        (deletionType && mark.type === deletionType)
      ) {
        key = `revision-${mark.attrs.revisionId}`;
      }
      if (!key) continue;
      registerKey(key, pos);
    }
  });

  return positions;
}

/**
 * Find the Y offset within a table fragment to the row containing a PM position.
 * Sums row heights until finding the row that contains the given position.
 */
function getTableRowOffset(
  blocks: FlowBlock[],
  measures: Measure[],
  frag: { blockId: string | number; fromRow: number; toRow: number },
  pmPos: number
): number {
  const blockIdx = blocks.findIndex((b) => b.id === frag.blockId);
  if (blockIdx === -1) return 0;
  const tBlock = blocks[blockIdx];
  const tMeasure = measures[blockIdx];
  if (tBlock.kind !== 'table' || tMeasure.kind !== 'table') return 0;

  let offsetY = 0;
  for (let ri = frag.fromRow; ri < frag.toRow; ri++) {
    const row = (tBlock as TableBlock).rows[ri];
    if (!row) break;
    const posInRow = row.cells.some((cell) =>
      cell.blocks.some((b) => {
        const s = (b as { pmStart?: number }).pmStart ?? 0;
        const e = (b as { pmEnd?: number }).pmEnd ?? s;
        return pmPos >= s && pmPos <= e;
      })
    );
    if (posInRow) break;
    offsetY += (tMeasure as TableMeasure).rows[ri]?.height ?? 0;
  }
  return offsetY;
}
