import { TextSelection } from 'prosemirror-state';
import type { EditorView } from 'prosemirror-view';
import { useEffect, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
import type { ReactNode } from 'react';
import type {
  Document,
  Theme,
  SectionProperties,
  HeaderFooter,
  BlockContent,
} from '@eigenpal/docx-editor-core/types/document';
import type { Comment } from '@eigenpal/docx-editor-core/types/content';
import type { Plugin } from 'prosemirror-state';
import {
  computeHfCaretRectFromView,
  computeHfSelectionRectsFromView,
  invalidateHfDomCache,
} from '@eigenpal/docx-editor-core/layout-bridge';
import { applyCellSelectionHighlight } from './internals/domSelection';
import { extractSelectionState } from '@eigenpal/docx-editor-core/prosemirror';
import type { ExtensionManager } from '@eigenpal/docx-editor-core/prosemirror/extensions';
import type { SelectionState } from '@eigenpal/docx-editor-core/prosemirror';
import { PagedEditor, type PagedEditorRef } from './PagedEditor';
import {
  InlineHeaderFooterEditor,
  type InlineHeaderFooterEditorRef,
} from '../InlineHeaderFooterEditor';
import { UnifiedSidebar } from '../UnifiedSidebar';
import { CommentMarginMarkers } from '../CommentMarginMarkers';
import { Tooltip } from '../ui/Tooltip';
import { MaterialSymbol } from '../ui/Icons';
import { PENDING_COMMENT_ID } from './commentFactories';
import type { HyperlinkPopupData } from '../ui/HyperlinkPopup';
import type { WrapType } from '@eigenpal/docx-editor-core/docx/wrapTypes';
import type { ReactSidebarItem } from '../../plugin-api/types';
import type { RenderedDomContext } from '../../plugin-api/types';

/**
 * Body of the editor: the paged ProseMirror host, its sidebar overlay
 * (UnifiedSidebar + comment margin markers), the floating "Add comment"
 * button anchored to a non-empty selection, and the inline header/footer
 * editor that appears when a user double-clicks an H/F slot.
 *
 * The floating button dispatches a pending comment mark inline rather
 * than going through onAddComment — same shape as the right-click menu's
 * addComment branch.
 */
export function DocxEditorPagedArea({
  // PagedEditor refs + state
  pagedEditorRef,
  hfEditorRef,
  scrollContainerRef,
  editorContentRef,
  // Document + section
  document,
  theme,
  initialSectionProperties,
  finalSectionProperties,
  // Header/footer
  headerContent,
  footerContent,
  firstPageHeaderContent,
  firstPageFooterContent,
  hfEditPosition,
  setHfEditPosition,
  hfEditIsFirstPage,
  onHeaderFooterDoubleClick,
  onHeaderFooterSave,
  onRemoveHeaderFooter,
  onBodyClick,
  getHfTargetElement,
  // Editor
  zoom,
  readOnly,
  extensionManager,
  externalPlugins,
  onDocumentChange,
  onSelectionChange,
  onPagedSelectionChange,
  onReady,
  onEditorViewReady,
  onRenderedDomContextReady,
  pluginOverlays,
  onHyperlinkClick,
  hyperlinkPopupData,
  onHyperlinkPopupNavigate,
  onHyperlinkPopupCopy,
  onHyperlinkPopupEdit,
  onHyperlinkPopupRemove,
  onHyperlinkPopupClose,
  onContextMenu,
  // Sidebar
  sidebarOpen,
  sidebarItems,
  anchorPositions,
  onAnchorPositionsChange,
  pluginRenderedDomContext,
  pageWidthPx,
  expandedSidebarItem,
  setExpandedSidebarItem,
  comments,
  resolvedCommentIds,
  resolvedIdsForRender,
  setShowCommentsSidebar,
  // Scroll page indicator
  onTotalPagesChange,
  // Floating comment button
  floatingCommentBtn,
  isAddingComment,
  setCommentSelectionRange,
  setAddCommentYPosition,
  setIsAddingComment,
  setFloatingCommentBtn,
}: {
  pagedEditorRef: React.RefObject<PagedEditorRef | null>;
  hfEditorRef: React.RefObject<InlineHeaderFooterEditorRef | null>;
  scrollContainerRef: React.RefObject<HTMLDivElement | null>;
  editorContentRef: React.RefObject<HTMLDivElement | null>;
  document: Document | null;
  theme: Theme | null | undefined;
  initialSectionProperties: SectionProperties | undefined;
  finalSectionProperties: SectionProperties | undefined;
  headerContent: HeaderFooter | null | undefined;
  footerContent: HeaderFooter | null | undefined;
  firstPageHeaderContent: HeaderFooter | null | undefined;
  firstPageFooterContent: HeaderFooter | null | undefined;
  hfEditPosition: 'header' | 'footer' | null;
  setHfEditPosition: React.Dispatch<React.SetStateAction<'header' | 'footer' | null>>;
  hfEditIsFirstPage: boolean;
  onHeaderFooterDoubleClick: (position: 'header' | 'footer', pageNumber?: number) => void;
  onHeaderFooterSave: (content: BlockContent[]) => void;
  onRemoveHeaderFooter: () => void;
  onBodyClick: () => void;
  getHfTargetElement: (pos: 'header' | 'footer') => HTMLElement | null;
  zoom: number;
  readOnly: boolean;
  extensionManager: ExtensionManager;
  externalPlugins: Plugin[];
  onDocumentChange: (doc: Document) => void;
  onSelectionChange: (state: SelectionState | null) => void;
  onPagedSelectionChange: () => void;
  onReady: (ref: PagedEditorRef) => void;
  onEditorViewReady: ((view: EditorView) => void) | undefined;
  onRenderedDomContextReady: ((ctx: RenderedDomContext) => void) | undefined;
  pluginOverlays: ReactNode;
  onHyperlinkClick: (data: HyperlinkPopupData) => void;
  hyperlinkPopupData: HyperlinkPopupData | null;
  onHyperlinkPopupNavigate: (href: string) => void;
  onHyperlinkPopupCopy: (href: string) => void;
  onHyperlinkPopupEdit: (displayText: string, href: string) => void;
  onHyperlinkPopupRemove: () => void;
  onHyperlinkPopupClose: () => void;
  onContextMenu: (data: {
    x: number;
    y: number;
    hasSelection: boolean;
    image?: {
      pos: number;
      wrapType: WrapType;
      cssFloat?: 'left' | 'right' | 'none' | null;
      inlinePositionEmu?: { horizontalEmu: number; verticalEmu: number };
    } | null;
  }) => void;
  sidebarOpen: boolean;
  sidebarItems: ReactSidebarItem[];
  anchorPositions: Map<string, number>;
  onAnchorPositionsChange: (positions: Map<string, number>) => void;
  pluginRenderedDomContext: RenderedDomContext | null | undefined;
  pageWidthPx: number;
  expandedSidebarItem: string | null;
  setExpandedSidebarItem: React.Dispatch<React.SetStateAction<string | null>>;
  comments: Comment[];
  resolvedCommentIds: Set<number>;
  resolvedIdsForRender: Set<number>;
  setShowCommentsSidebar: React.Dispatch<React.SetStateAction<boolean>>;
  onTotalPagesChange: (totalPages: number) => void;
  floatingCommentBtn: { top: number; left: number } | null;
  isAddingComment: boolean;
  setCommentSelectionRange: React.Dispatch<
    React.SetStateAction<{ from: number; to: number } | null>
  >;
  setAddCommentYPosition: React.Dispatch<React.SetStateAction<number | null>>;
  setIsAddingComment: React.Dispatch<React.SetStateAction<boolean>>;
  setFloatingCommentBtn: React.Dispatch<React.SetStateAction<{ top: number; left: number } | null>>;
}) {
  // Resolve the active HF block for the inline editor — first-page variant
  // wins when `titlePg` is set and the user double-clicked page 1.
  const activeHf = hfEditPosition
    ? hfEditIsFirstPage
      ? hfEditPosition === 'header'
        ? firstPageHeaderContent
        : firstPageFooterContent
      : hfEditPosition === 'header'
        ? headerContent
        : footerContent
    : null;

  // Phase 4 of HF editing unification: the painter is the visible HF
  // renderer (phase 2) and the inline overlay's PM is off-screen — so the
  // user has no visible caret in the painted region. We compute one here
  // on every HF transaction (doc OR selection-only) by mapping the HF
  // EditorView's selection head to the painter's `data-pm-start` markers
  // and render a fixed-positioned blinking div over the painted HF.
  const [hfCaretRect, setHfCaretRect] = useState<{
    top: number;
    left: number;
    height: number;
  } | null>(null);

  // HF selection rects — drawn when the user drag-selects a range inside
  // the painted header/footer. Body's SelectionOverlay is gated off in HF
  // mode (see PagedEditor) so the body rects don't render alongside.
  const [hfSelectionRects, setHfSelectionRects] = useState<
    Array<{ top: number; left: number; width: number; height: number }>
  >([]);

  // The caret/selection rects come back from core in viewport coords. The
  // overlay is portalled into the (`position: relative`) sibling parent of
  // `.paged-editor__pages` and positioned `absolute`, so the browser moves it
  // with the painter on scroll for free — BUT only if we store the host-local
  // coordinate, NOT the viewport one. Viewport coords go stale the moment the
  // page scrolls; since unrelated re-renders fire on scroll (e.g. the page
  // indicator), re-converting viewport→local each render would re-add the
  // scroll delta and the caret would drift away from the footer (#671
  // follow-up). Converting ONCE here yields a scroll-invariant value that the
  // absolutely-positioned div tracks natively with zero per-frame JS.
  const toHfHostLocal = useCallback(<T extends { top: number; left: number }>(rect: T): T => {
    const pagesEl = window.document.querySelector('.paged-editor__pages') as HTMLElement | null;
    const host = pagesEl?.parentElement as HTMLElement | null;
    if (!host) return rect;
    const c = host.getBoundingClientRect();
    return {
      ...rect,
      top: rect.top - c.top + host.scrollTop,
      left: rect.left - c.left + host.scrollLeft,
    };
  }, []);

  // Recompute the painted HF overlay (caret + drag-selection rects + multi-cell
  // highlight) for the active section from the live HF view. Called on engage,
  // on every HF transaction, and on resize. Coords are converted to host-local
  // once here so they survive scroll (see `toHfHostLocal`).
  const applyHfOverlay = useCallback(
    (view: EditorView) => {
      if (!hfEditPosition) {
        setHfCaretRect(null);
        setHfSelectionRects([]);
        return;
      }
      const caret = computeHfCaretRectFromView(view, hfEditPosition);
      setHfCaretRect(caret ? toHfHostLocal(caret) : null);
      setHfSelectionRects(computeHfSelectionRectsFromView(view, hfEditPosition).map(toHfHostLocal));
      const pagesEl = window.document.querySelector('.paged-editor__pages') as HTMLElement | null;
      // Multi-cell selection renders via `.layout-table-cell-selected`, scoped
      // to the active section so footer selections don't light up header cells.
      if (pagesEl) applyCellSelectionHighlight(pagesEl, view.state, { scope: hfEditPosition });
    },
    [hfEditPosition, toHfHostLocal]
  );

  // Initial-caret-on-engage: when the user double-clicks into HF mode the
  // persistent PM's selection sits at position 0 with no transaction fired,
  // so `onHfTransaction` never gets a chance to paint the caret. Wait for
  // the painter to finish its repaint (rAF × 2 — one for React commit,
  // one for the painter pass `runLayoutPipeline` schedules), then measure
  // against the freshly painted spans.
  useEffect(() => {
    if (!hfEditPosition) {
      setHfCaretRect(null);
      setHfSelectionRects([]);
      invalidateHfDomCache();
      return;
    }
    const measure = () => {
      const view = hfEditorRef.current?.getView();
      if (view) applyHfOverlay(view);
    };

    // Deterministic "painter is done" signal — `useLayoutPipeline` dispatches
    // `painter:painted` after `renderPages` writes the page DOM. Listen for
    // it instead of the rAF chain so the measurement always sees the fresh
    // `data-pm-start` spans. Also invalidate the cached HF DOM snapshot so
    // the next caret compute re-walks the host.
    const pagesEl = window.document.querySelector('.paged-editor__pages') as HTMLElement | null;
    const onPainted = () => {
      invalidateHfDomCache();
      measure();
    };
    pagesEl?.addEventListener('painter:painted', onPainted);

    // Safety: if the painter doesn't fire for the initial engage (no doc
    // change → no layout pass), still measure on the next frame so the
    // caret shows up at all.
    const raf = requestAnimationFrame(measure);

    // Resize still needs a recompute because the painter re-runs after a
    // viewport resize and the span layout shifts. The painter dispatches
    // `painter:painted` after its rerun, but the listener may have been
    // re-registered between the two — wire resize as a belt-and-braces.
    const onResize = () => measure();
    window.addEventListener('resize', onResize);

    return () => {
      cancelAnimationFrame(raf);
      pagesEl?.removeEventListener('painter:painted', onPainted);
      window.removeEventListener('resize', onResize);
      invalidateHfDomCache();
    };
  }, [hfEditPosition, hfEditorRef, applyHfOverlay]);

  return (
    <>
      <PagedEditor
        ref={pagedEditorRef}
        document={document}
        styles={document?.package.styles}
        theme={document?.package.theme || theme}
        sectionProperties={initialSectionProperties}
        finalSectionProperties={finalSectionProperties}
        headerContent={headerContent}
        footerContent={footerContent}
        firstPageHeaderContent={firstPageHeaderContent}
        firstPageFooterContent={firstPageFooterContent}
        onHeaderFooterDoubleClick={onHeaderFooterDoubleClick}
        hfEditMode={hfEditPosition}
        onBodyClick={onBodyClick}
        onHfTransaction={(_rId, view, _docChanged) => {
          // Phase 5: the persistent HF PM is the sole editor. On every
          // transaction (typing, click → setSelection, undo/redo) we need
          // the caret to follow — deferred to rAF so the painter's repaint
          // (triggered by `runLayoutPipeline` inside PagedEditor) lands
          // before we measure against `data-pm-start` spans. Toolbar
          // selection state still rides through `onSelectionChange` on
          // the inline overlay's old wiring path, which now reads from
          // the persistent view via `hfEditorRef.getView()`.
          // Painter dispatches `painter:painted` after `renderPages`.
          // Wait for it so the cache invalidation + caret measurement sees
          // the fresh span layout. Selection-only transactions skip the
          // painter, so use a one-shot rAF as a fallback.
          const pagesEl = window.document.querySelector(
            '.paged-editor__pages'
          ) as HTMLElement | null;
          let painted = false;
          const apply = () => {
            if (painted) return;
            painted = true;
            invalidateHfDomCache();
            applyHfOverlay(view);
          };
          pagesEl?.addEventListener('painter:painted', apply, { once: true });
          requestAnimationFrame(() => {
            if (!painted) {
              pagesEl?.removeEventListener('painter:painted', apply);
              apply();
            }
          });
          onSelectionChange(extractSelectionState(view.state));
        }}
        // Click routing through `onHfPagesMouseDown` was retired; usePagesPointer
        // now routes every HF gesture (click, drag, dblclick, image, hyperlink,
        // context menu) through the active-surface helper directly.
        zoom={zoom}
        readOnly={readOnly}
        extensionManager={extensionManager}
        onDocumentChange={onDocumentChange}
        onSelectionChange={onPagedSelectionChange}
        externalPlugins={externalPlugins}
        onReady={(ref) => {
          onReady(ref);
          const view = ref.getView();
          if (view) onEditorViewReady?.(view);
        }}
        onRenderedDomContextReady={onRenderedDomContextReady}
        pluginOverlays={pluginOverlays}
        onHyperlinkClick={onHyperlinkClick}
        hyperlinkPopupData={hyperlinkPopupData}
        onHyperlinkPopupNavigate={onHyperlinkPopupNavigate}
        onHyperlinkPopupCopy={onHyperlinkPopupCopy}
        onHyperlinkPopupEdit={onHyperlinkPopupEdit}
        onHyperlinkPopupRemove={onHyperlinkPopupRemove}
        onHyperlinkPopupClose={onHyperlinkPopupClose}
        onContextMenu={onContextMenu}
        commentsSidebarOpen={sidebarOpen}
        onAnchorPositionsChange={onAnchorPositionsChange}
        onTotalPagesChange={onTotalPagesChange}
        resolvedCommentIds={resolvedIdsForRender}
        scrollContainerRef={scrollContainerRef}
        sidebarOverlay={
          <>
            {sidebarItems.length > 0 && (
              <UnifiedSidebar
                items={sidebarItems}
                anchorPositions={anchorPositions}
                renderedDomContext={pluginRenderedDomContext ?? null}
                pageWidth={pageWidthPx}
                zoom={zoom}
                editorContainerRef={scrollContainerRef}
                onExpandedItemChange={setExpandedSidebarItem}
                activeItemId={expandedSidebarItem}
              />
            )}
            <CommentMarginMarkers
              comments={comments}
              anchorPositions={anchorPositions}
              zoom={zoom}
              pageWidth={pageWidthPx}
              sidebarOpen={sidebarOpen}
              resolvedCommentIds={resolvedCommentIds}
              onMarkerClick={() => setShowCommentsSidebar(true)}
            />
          </>
        }
      />

      {floatingCommentBtn != null && !isAddingComment && !readOnly && (
        <Tooltip content="Add comment" side="bottom" delayMs={300}>
          <button
            type="button"
            onMouseDown={(e) => {
              e.preventDefault();
              e.stopPropagation();
              const view = pagedEditorRef.current?.getView();
              if (view) {
                const { from, to } = view.state.selection;
                if (from !== to) {
                  setCommentSelectionRange({ from, to });
                  const pendingMark = view.state.schema.marks.comment.create({
                    commentId: PENDING_COMMENT_ID,
                  });
                  const tr = view.state.tr.addMark(from, to, pendingMark);
                  tr.setSelection(TextSelection.create(tr.doc, to));
                  view.dispatch(tr);
                }
              }
              setAddCommentYPosition(floatingCommentBtn.top);
              setShowCommentsSidebar(true);
              setIsAddingComment(true);
              setFloatingCommentBtn(null);
            }}
            style={{
              position: 'absolute',
              top: floatingCommentBtn.top,
              left: floatingCommentBtn.left,
              transform: 'translate(-50%, -50%)',
              zIndex: 50,
              width: 28,
              height: 28,
              borderRadius: 6,
              border: '1px solid var(--doc-focus-ring)',
              backgroundColor: 'var(--doc-surface)',
              color: 'var(--doc-primary)',
              cursor: 'pointer',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              boxShadow: '0 1px 3px var(--doc-shadow)',
              transition: 'background-color 0.15s ease, box-shadow 0.15s ease',
            }}
            onMouseOver={(e) => {
              (e.currentTarget as HTMLButtonElement).style.backgroundColor =
                'var(--doc-primary-light)';
              (e.currentTarget as HTMLButtonElement).style.boxShadow =
                '0 1px 4px var(--doc-focus-ring)';
            }}
            onMouseOut={(e) => {
              (e.currentTarget as HTMLButtonElement).style.backgroundColor = 'var(--doc-surface)';
              (e.currentTarget as HTMLButtonElement).style.boxShadow =
                '0 1px 3px var(--doc-shadow)';
            }}
          >
            <MaterialSymbol name="add_comment" size={16} />
          </button>
        </Tooltip>
      )}

      {/* HF caret + selection rects portalled into the SIBLING parent of
          `.paged-editor__pages` (same scroll container the body's
          `SelectionOverlay` uses). `position: absolute` + host-local coords
          (already converted via `toHfHostLocal` at compute time) means the
          browser moves them with the painter on scroll — zero JS per wheel
          tick. The coords are scroll-invariant, so re-renders never re-add the
          scroll delta. Crisper than `position: fixed` + scroll listener. The
          painter never touches this layer (siblings, not children of
          `.paged-editor__pages`), so the wipe-on-rebuild regression that bit
          the previous portal attempt is avoided. */}
      {hfEditPosition &&
        (hfCaretRect || hfSelectionRects.length > 0) &&
        (() => {
          const pagesEl = window.document.querySelector(
            '.paged-editor__pages'
          ) as HTMLElement | null;
          const host = pagesEl?.parentElement as HTMLElement | null;
          if (!pagesEl || !host) return null;
          return createPortal(
            <>
              {hfCaretRect && hfSelectionRects.length === 0 && (
                <>
                  <div
                    aria-hidden="true"
                    style={{
                      position: 'absolute',
                      top: hfCaretRect.top,
                      left: hfCaretRect.left,
                      width: 2,
                      height: hfCaretRect.height,
                      background: '#4285f4',
                      pointerEvents: 'none',
                      zIndex: 11,
                      animation: 'hf-caret-blink 1.06s steps(1) infinite',
                    }}
                  />
                </>
              )}
              {hfSelectionRects.map((r, i) => {
                return (
                  <div
                    key={`hf-sel-${i}-${r.top}-${r.left}`}
                    aria-hidden="true"
                    style={{
                      position: 'absolute',
                      top: r.top,
                      left: r.left,
                      width: r.width,
                      height: r.height,
                      background: 'rgba(66, 133, 244, 0.25)',
                      pointerEvents: 'none',
                      zIndex: 10,
                    }}
                  />
                );
              })}
            </>,
            host
          );
        })()}

      {hfEditPosition &&
        activeHf &&
        (() => {
          const targetEl = getHfTargetElement(hfEditPosition);
          const parentEl = editorContentRef.current;
          if (!targetEl || !parentEl) return null;
          // Phase 5: the inline overlay is now UI chrome only — it takes
          // the persistent HF EditorView as a prop and never creates its
          // own PM. Toolbar / save / undo all route through the persistent
          // view via `hfEditorRef`.
          const persistentView = pagedEditorRef.current?.getHfPmView(activeHf) ?? null;
          return (
            <InlineHeaderFooterEditor
              ref={hfEditorRef}
              headerFooter={activeHf}
              position={hfEditPosition}
              view={persistentView}
              targetElement={targetEl}
              parentElement={parentEl}
              onSave={onHeaderFooterSave}
              onClose={() => {
                setHfEditPosition(null);
                setHfCaretRect(null);
              }}
              onRemove={onRemoveHeaderFooter}
            />
          );
        })()}
    </>
  );
}
