import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import Konva from 'konva';
import { getNodePageRect } from '../../utils/konvaCoords';

/**
 * Pencil overlay for editing text — MOBILE ONLY in the current flow.
 *
 * The desktop editing model has been reverted: selecting a text
 * element on the canvas auto-switches the right rail to the Text
 * tab, with edits flowing live to the selected element. No pencil
 * affordance is rendered on desktop.
 *
 * On mobile, the right-rail-equivalent (the bottom sheet) is hidden
 * by default — auto-opening it on every text selection would be
 * disruptive (the sheet covers most of the canvas). So mobile keeps
 * an explicit gesture: tap the on-canvas pencil next to a selected
 * text element to open the sheet on the Text tab in edit mode. This
 * is the same component that used to be desktop-only; the gate in
 * App.jsx flipped from `!isMobile` to `isMobile`.
 *
 * The pencil renders a 40×40 button positioned just outside the
 * top-right corner of the selected text node's bounding box. 40px
 * is comfortably above the WCAG / iOS minimum touch target (44pt /
 * ~44px logical px) for the small canvas sizes typical on mobile;
 * users with thumb-sized text targets can still hit the pencil
 * without colliding with the text itself.
 *
 * Coordinate plumbing: stage's outer CSS scale is applied to the
 * node's internal getClientRect to get a page-pixel rect. We listen
 * for window resize and scroll so the button stays glued when the
 * user pans the page or resizes.
 *
 * Tap handler fires `onStartEdit({ id, rect })` — App.jsx's
 * handleStartTextEdit — which sets editingTextElementId, flips the
 * sidebar's active tab to Text, and opens the bottom sheet.
 */
export function TextEditAffordance({
  element,
  stageContainerRef,
  onStartEdit,
}) {
  // Position state. null until we've measured the node at least once;
  // hidden until then so the button doesn't flash at (0, 0) on first
  // render before the layout effect runs. We also reset to null when
  // the element id changes so the pencil doesn't briefly linger at the
  // PREVIOUS element's position while waiting for the next measure to
  // resolve — see the elementRef sync below for why that mattered.
  const [pos, setPos] = useState(null);

  // Ref to the latest element so the rAF-throttled measure (called from
  // window resize/scroll listeners) can read it without re-binding on
  // every render — same pattern as DesignCanvas's snapEnabledRef.
  //
  // IMPORTANT — sync in render body, not in a useEffect.
  // ─────────────────────────────────────────────────────────────────────────────
  // Previously this was `useEffect(() => { elementRef.current = element; }, [element])`,
  // which runs AFTER the layout effect below. So on an element-prop
  // change (user clicks a different text element), the sequence was:
  //
  //   1. Render with new `element` prop.
  //   2. useLayoutEffect fires → calls measure().
  //   3. measure() reads elementRef.current → STILL the OLD element.
  //   4. measure() looks up the OLD node, sets `pos` to its rect.
  //   5. useEffect fires → finally updates elementRef.current to new
  //      element. But measure() has already run with stale data.
  //
  // Result: the pencil stayed glued to the previously-selected text
  // until something else (resize, scroll, prop unrelated to selection)
  // forced a re-measure.
  //
  // Fix: assign during render. By the time useLayoutEffect runs,
  // elementRef.current already points at the new element. Assignment
  // to a ref's `.current` during render is safe — it doesn't trigger
  // re-renders, and React's StrictMode double-invocation is also
  // tolerant (the assignment is idempotent).
  const elementRef = useRef(element);
  elementRef.current = element;

  // Track the previous element id so we can clear the visible position
  // when the selection changes. Without this, the old pos lingered
  // visibly for one paint while the layout effect's measure() ran —
  // measure() does its work synchronously, but if the new node hasn't
  // mounted yet (rare but possible during a tab switch + selection
  // change in the same batch), the bail-out path leaves pos at its
  // last value. Forcing null on id change makes the pencil disappear
  // until the new position is known, which is less visually startling
  // than seeing it pop from one element to another.
  const prevIdRef = useRef(element?.id);
  if (prevIdRef.current !== element?.id) {
    prevIdRef.current = element?.id;
    if (pos !== null) {
      // Schedule a clear; using setPos inside render is illegal, so
      // gate via a microtask. The layout effect below will re-measure
      // and set the new position on the same render tick, so this
      // clear is only visible if measure() fails to resolve a node.
      // Using queueMicrotask rather than setPos directly avoids the
      // "Cannot update during render" warning while keeping the
      // clear in the same event loop tick.
      queueMicrotask(() => {
        // Re-check: another render may have already populated pos
        // for the new element before this microtask runs.
        if (prevIdRef.current === element?.id) {
          setPos((curr) => (curr && curr._forId !== element?.id ? null : curr));
        }
      });
    }
  }

  // Measure: find the Konva node by `name` attr (= element id), compute
  // its page-pixel rect, and update `pos`. Bails to null when the
  // element is gone or the stage isn't ready.
  const measure = () => {
    const el = elementRef.current;
    // Read the container fresh each measure. The ref is set by
    // DesignCanvas's Stage ref callback on mount, which fires AFTER
    // App's first render — so on the very first paint stageContainerRef
    // may still be null. The window-level event listeners + the
    // element-prop-driven effect both eventually re-call measure once
    // it's populated, so we just bail until then.
    const container = stageContainerRef?.current ?? null;
    if (!el || !container) {
      setPos((prev) => (prev === null ? prev : null));
      return;
    }
    // `container` is the Konva Stage's outer DOM element. Konva attaches
    // the underlying Stage instance via the `_konva` field on the
    // container's children — but the safer cross-version path is the
    // public `Konva.Stage.container()` reverse: we walk down to find a
    // canvas, then read its parent's `__konvaNode` (set on creation).
    // Instead of relying on private fields, we look up the Stage via
    // the `Konva.stages` registry: it's documented and stable.
    let stage = null;
    // Konva exposes a static `Konva.stages` array of every Stage
    // currently mounted on the page. Walking it is O(stages) — in a
    // single-canvas app there's exactly one Stage, so this is
    // O(1) in practice. The fallback `_konvaNode` backref on the
    // canvas DOM element handles versions where the stages array
    // shape changes.
    if (Konva && Array.isArray(Konva.stages)) {
      for (const s of Konva.stages) {
        try { if (s.container() === container) { stage = s; break; } } catch { /* ignore */ }
      }
    }
    if (!stage) {
      // Fallback: the parent's first canvas child has a `_konvaNode`
      // backref on most versions. Try that before giving up.
      const canvas = container.querySelector('canvas');
      stage = canvas?._konvaNode?.getStage?.() ?? null;
    }
    if (!stage) {
      setPos((prev) => (prev === null ? prev : null));
      return;
    }

    const node = stage.findOne('.' + el.id);
    if (!node) {
      setPos((prev) => (prev === null ? prev : null));
      return;
    }
    const rect = getNodePageRect(node);
    if (!rect) {
      setPos((prev) => (prev === null ? prev : null));
      return;
    }
    // Tag the rect with the element id it was measured for. The
    // render-body element-id-change guard above uses this so a stale
    // microtask-scheduled clear can't wipe a position that's already
    // been recomputed for the new element.
    rect._forId = el.id;
    setPos(rect);
  };

  // Re-measure whenever the selected element's identity OR any prop
  // that affects its rendered bbox changes — fontSize, text, fontFamily,
  // x, y, rotation, arc. The dep list intentionally lists each one
  // rather than the whole element object because Konva nodes mutate
  // their attrs in place during transforms; the wrapping element prop
  // is recreated by useDesignEditor on every update, so depending on
  // it directly would also work, but enumerating gives a clearer
  // signal of what actually drives the re-measure.
  useLayoutEffect(() => {
    measure();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    element?.id,
    element?.x, element?.y,
    element?.text, element?.fontFamily, element?.fontSize,
    element?.rotation, element?.arc,
    element?.flipX, element?.flipY,
    stageContainerRef,
  ]);

  // Window-level events that can move the stage on screen without
  // changing element props. Re-measure rAF-throttled so a fast scroll
  // or resize doesn't drop hundreds of setState calls into React.
  useEffect(() => {
    let raf = null;
    const onUpdate = () => {
      if (raf !== null) return;
      raf = requestAnimationFrame(() => {
        raf = null;
        measure();
      });
    };
    window.addEventListener('resize', onUpdate);
    window.addEventListener('scroll', onUpdate, true); // capture: catch nested scroll containers too
    return () => {
      if (raf !== null) cancelAnimationFrame(raf);
      window.removeEventListener('resize', onUpdate);
      window.removeEventListener('scroll', onUpdate, true);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  if (!element || element.type !== 'text' || !pos) return null;

  // Anchor: just outside the top-right corner of the bbox, offset
  // slightly into the empty space so the button doesn't overlap the
  // selection border. 40px button sized for comfortable mobile
  // tapping (above the ~44pt iOS touch-target minimum once CSS
  // pixel-density scaling is considered), 6px gap from the right
  // edge so the button doesn't visually crowd the bbox border.
  const BTN = 40;
  const GAP = 6;
  const left = pos.left + pos.width + GAP;
  const top = pos.top - GAP;

  const handleClick = (e) => {
    e.stopPropagation();
    onStartEdit?.({ id: element.id, rect: pos });
  };

  // The button uses position: fixed so it tracks the viewport directly;
  // any ancestor's transform or overflow won't affect placement.
  const style = {
    position: 'fixed',
    left: `${left}px`,
    top: `${top}px`,
    width: `${BTN}px`,
    height: `${BTN}px`,
    border: '1.5px solid #ec4899',
    background: '#fff',
    color: '#ec4899',
    borderRadius: '50%',
    boxShadow: '0 2px 8px rgba(236, 72, 153, 0.32)',
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    cursor: 'pointer',
    padding: 0,
    // z-index sits between the FAB (700) and the mobile bottom
    // sheet (800) so the pencil floats above the canvas chrome but
    // is OCCLUDED by the bottom sheet when it opens. Previously 999
    // — high enough to draw over the sheet, which meant when the
    // user opened the "add item" sheet with a text selected, the
    // pencil bled through the sheet's content area and looked like
    // it belonged to whatever element it happened to overlap. The
    // canvas itself has no z-index of its own (regular flow), so
    // a value of 750 stays above all canvas-area chrome (zoom
    // controls at z-index 10, mobile toolbar at 600, FAB at 700)
    // while letting the sheet (800) cover it.
    zIndex: 750,
  };

  return createPortal(
    <button
      type="button"
      style={style}
      onMouseDown={(e) => e.stopPropagation()}
      onClick={handleClick}
      aria-label="Edit text"
      title="Edit text"
    >
      {/* Pencil glyph — same path as the mobile-toolbar Edit text
          button so the two surfaces agree visually. Sized to fill
          ~half the button diameter (20px in a 40px button) so the
          icon reads clearly at touch-target distances. */}
      <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <path d="M12 20h9" />
        <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
      </svg>
    </button>,
    document.body,
  );
}
