/**
 * Handles Controller (Phase 4.9)
 *
 * Renders interactive resize handles on top of the selected element.
 * Integrates drag-to-resize with TransactionManager.beginMultiStyle() so each gesture
 * becomes a single undo/redo step.
 *
 * Design notes:
 * - Uses DOM (not Canvas) for reliable hit targets, cursors, and pointer capture.
 * - Uses rAF-throttled updates to bound work to at most once per frame.
 * - Handles are positioned using transform for GPU-accelerated performance.
 */

import {
  WEB_EDITOR_DISTANCE_LABEL_MIN_PX,
  WEB_EDITOR_LOG_PREFIX,
  WEB_EDITOR_SNAP_HYSTERESIS_PX,
  WEB_EDITOR_SNAP_THRESHOLD_PX,
} from '../constants';
import {
  collectSiblingAnchors,
  collectViewportAnchors,
  computeDistanceLabels,
  computeResizeSnap,
  mergeAnchors,
  type SnapAnchors,
  type SnapLockX,
  type SnapLockY,
} from '../core/snap-engine';
import type { PositionTracker } from '../core/position-tracker';
import type { MultiStyleTransactionHandle, TransactionManager } from '../core/transaction-manager';
import type { CanvasOverlay, ViewportRect } from './canvas-overlay';
import { Disposer } from '../utils/disposables';

// =============================================================================
// Types
// =============================================================================

/** Resize handle direction (8 cardinal + ordinal directions) */
export type ResizeHandleDir = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';

/** Position mode for determining how to apply position changes */
type ResizePositionMode = 'fixed' | 'absolute' | 'relative' | 'static';

/** Options for creating the handles controller */
export interface HandlesControllerOptions {
  /** overlayRoot from ShadowHost (Canvas container) */
  container: HTMLElement;
  /** Canvas overlay for drawing alignment guide lines (Phase 4.2) */
  canvasOverlay: CanvasOverlay;
  /** Transaction manager for atomic multi-style resize commits */
  transactionManager: TransactionManager;
  /** Position tracker for final sync after commit/cancel */
  positionTracker: PositionTracker;
}

/** Handles controller public interface */
export interface HandlesController {
  /** Update the current selected element (null hides handles and cancels active drag) */
  setTarget(target: Element | null): void;
  /** Update the current selection rect in viewport coordinates (null hides handles) */
  setSelectionRect(rect: ViewportRect | null): void;
  /** Cleanup resources */
  dispose(): void;
}

/** Box-sizing extras for accurate size calculations */
interface BoxExtras {
  boxSizing: 'border-box' | 'content-box';
  horizontalExtras: number; // padding + border (horizontal)
  verticalExtras: number; // padding + border (vertical)
}

/** Origin info for absolute positioning calculations */
interface AbsoluteOrigin {
  originX: number;
  originY: number;
  scrollLeft: number;
  scrollTop: number;
}

/** Active resize session state */
interface ResizeSession {
  /** Pointer ID for capture tracking */
  pointerId: number;
  /** Direction being dragged */
  dir: ResizeHandleDir;
  /** Handle element being dragged */
  handleEl: HTMLElement;
  /** Target element being resized */
  target: HTMLElement;
  /** Position mode of target */
  mode: ResizePositionMode;
  /** CSS properties tracked for this gesture (used to start tx after threshold) */
  properties: readonly string[];
  /** Transaction handle for atomic commit/rollback (created after threshold) */
  tx: MultiStyleTransactionHandle | null;
  /** Whether the drag threshold has been exceeded (resize is active) */
  hasPassedThreshold: boolean;

  /** Whether this direction affects width */
  affectsWidth: boolean;
  /** Whether this direction affects height */
  affectsHeight: boolean;
  /** Whether dragging from west edge */
  hasWest: boolean;
  /** Whether dragging from north edge */
  hasNorth: boolean;

  // ===========================================================================
  // Snap state (Phase 4.2)
  // ===========================================================================

  /** Pre-collected anchors for this gesture (siblings + viewport) */
  anchors: SnapAnchors | null;
  /** Active X-axis snap lock (for hysteresis) */
  lockX: SnapLockX | null;
  /** Active Y-axis snap lock (for hysteresis) */
  lockY: SnapLockY | null;
  /** Whether guides were drawn last frame (for change detection) */
  hadGuidesLastFrame: boolean;
  /** Whether distance labels were drawn last frame (for change detection) */
  hadDistanceLabelsLastFrame: boolean;

  /** Start pointer position */
  startClientX: number;
  startClientY: number;
  /** Last pointer position (for rAF) */
  lastClientX: number;
  lastClientY: number;

  /** Starting element rect */
  startRect: ViewportRect;
  /** Starting position value (left/top or margin-left/margin-top) */
  startPosX: number;
  startPosY: number;
  /** Absolute positioning origin info (null for non-absolute) */
  absOrigin: AbsoluteOrigin | null;
  /** Box model extras for size calculations */
  extras: BoxExtras;

  /** Previous body styles for restoration */
  prevBodyCursor: string;
  prevBodyUserSelect: string;
}

// =============================================================================
// Constants
// =============================================================================

/** Minimum element size in border-box pixels */
const MIN_BORDER_BOX_SIZE_PX = 1;

/** Minimum pointer movement (px) to start resizing (prevents click -> transaction) */
const RESIZE_DRAG_THRESHOLD_PX = 3;

/** All resize handle directions */
const HANDLE_DIRS: readonly ResizeHandleDir[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];

/** Cursor style for each direction */
const CURSOR_BY_DIR: Readonly<Record<ResizeHandleDir, string>> = {
  n: 'ns-resize',
  s: 'ns-resize',
  e: 'ew-resize',
  w: 'ew-resize',
  ne: 'nesw-resize',
  sw: 'nesw-resize',
  nw: 'nwse-resize',
  se: 'nwse-resize',
};

// =============================================================================
// Utility Functions
// =============================================================================

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function isValidRect(rect: ViewportRect | null): rect is ViewportRect {
  if (!rect) return false;
  return (
    isFiniteNumber(rect.left) &&
    isFiniteNumber(rect.top) &&
    isFiniteNumber(rect.width) &&
    isFiniteNumber(rect.height) &&
    rect.width > 0.5 &&
    rect.height > 0.5
  );
}

function clampMin(value: number, min: number): number {
  if (!Number.isFinite(value)) return min;
  return value < min ? min : value;
}

/**
 * Parse a CSS pixel value (e.g., "10px", "auto", "10") to a number.
 * Returns null for non-numeric values like "auto".
 */
function parsePx(value: string): number | null {
  const trimmed = value.trim();
  if (!trimmed || trimmed === 'auto' || trimmed === 'none') return null;

  const match = trimmed.match(/^(-?\d+(?:\.\d+)?)px$/);
  if (match) {
    const num = Number(match[1]);
    return Number.isFinite(num) ? num : null;
  }

  const num = Number(trimmed);
  return Number.isFinite(num) ? num : null;
}

/**
 * Format a number as a CSS pixel value.
 * Rounds to 2 decimal places to avoid floating point noise.
 */
function formatPx(value: number): string {
  if (!Number.isFinite(value)) return '0px';
  const rounded = Math.round(value * 100) / 100;
  const normalized = Object.is(rounded, -0) ? 0 : rounded;
  return `${normalized}px`;
}

/**
 * Get the resize position mode from a CSS position value.
 */
function getResizeMode(position: string): ResizePositionMode {
  const p = position.trim().toLowerCase();
  if (p === 'fixed') return 'fixed';
  if (p === 'absolute') return 'absolute';
  if (p === 'relative' || p === 'sticky') return 'relative';
  return 'static';
}

// Direction helper functions
function dirHasWest(dir: ResizeHandleDir): boolean {
  return dir === 'w' || dir === 'nw' || dir === 'sw';
}

function dirHasEast(dir: ResizeHandleDir): boolean {
  return dir === 'e' || dir === 'ne' || dir === 'se';
}

function dirHasNorth(dir: ResizeHandleDir): boolean {
  return dir === 'n' || dir === 'nw' || dir === 'ne';
}

function dirHasSouth(dir: ResizeHandleDir): boolean {
  return dir === 's' || dir === 'sw' || dir === 'se';
}

/**
 * Read an element's bounding rect in viewport coordinates.
 */
function readViewportRect(element: Element): ViewportRect | null {
  try {
    const r = element.getBoundingClientRect();
    if (
      !Number.isFinite(r.left) ||
      !Number.isFinite(r.top) ||
      !Number.isFinite(r.width) ||
      !Number.isFinite(r.height)
    ) {
      return null;
    }
    return {
      left: r.left,
      top: r.top,
      width: Math.max(0, r.width),
      height: Math.max(0, r.height),
    };
  } catch {
    return null;
  }
}

/**
 * Safely get computed style for an element.
 */
function safeGetComputedStyle(element: Element): CSSStyleDeclaration | null {
  try {
    return window.getComputedStyle(element);
  } catch {
    return null;
  }
}

/**
 * Sum two CSS property values as pixels.
 */
function sumStylePx(style: CSSStyleDeclaration, propA: string, propB: string): number {
  const a = parsePx(style.getPropertyValue(propA)) ?? 0;
  const b = parsePx(style.getPropertyValue(propB)) ?? 0;
  return a + b;
}

/**
 * Read box model extras (padding + border) from computed style.
 */
function readBoxExtras(style: CSSStyleDeclaration): BoxExtras {
  const boxSizingRaw = style.getPropertyValue('box-sizing').trim();
  const boxSizing: BoxExtras['boxSizing'] =
    boxSizingRaw === 'border-box' ? 'border-box' : 'content-box';

  const paddingX = sumStylePx(style, 'padding-left', 'padding-right');
  const paddingY = sumStylePx(style, 'padding-top', 'padding-bottom');
  const borderX = sumStylePx(style, 'border-left-width', 'border-right-width');
  const borderY = sumStylePx(style, 'border-top-width', 'border-bottom-width');

  return {
    boxSizing,
    horizontalExtras: paddingX + borderX,
    verticalExtras: paddingY + borderY,
  };
}

/**
 * Convert border-box size to CSS width/height value based on box-sizing.
 */
function borderBoxToCssSize(
  borderBoxPx: number,
  extrasPx: number,
  boxSizing: BoxExtras['boxSizing'],
): number {
  if (boxSizing === 'border-box') return borderBoxPx;
  return Math.max(0, borderBoxPx - extrasPx);
}

/**
 * Compute the origin point for absolute positioning calculations.
 * For absolute elements, this is the padding-box of the offsetParent.
 */
function computeAbsoluteOrigin(target: HTMLElement): AbsoluteOrigin {
  try {
    const op = target.offsetParent;
    if (op instanceof HTMLElement) {
      const rect = op.getBoundingClientRect();
      const style = safeGetComputedStyle(op);
      const borderLeft = style ? (parsePx(style.getPropertyValue('border-left-width')) ?? 0) : 0;
      const borderTop = style ? (parsePx(style.getPropertyValue('border-top-width')) ?? 0) : 0;
      return {
        originX: rect.left + borderLeft,
        originY: rect.top + borderTop,
        scrollLeft: op.scrollLeft,
        scrollTop: op.scrollTop,
      };
    }
  } catch {
    // Best-effort fallback below
  }
  // Fallback to viewport origin (for fixed or when offsetParent is null)
  return { originX: 0, originY: 0, scrollLeft: 0, scrollTop: 0 };
}

/**
 * Stop event propagation and prevent default.
 */
function stopEvent(event: Event): void {
  if (event.cancelable) event.preventDefault();
  event.stopPropagation();
}

// =============================================================================
// Factory
// =============================================================================

/**
 * Create a handles controller for resize interactions.
 */
export function createHandlesController(options: HandlesControllerOptions): HandlesController {
  const disposer = new Disposer();
  const { container, canvasOverlay, transactionManager, positionTracker } = options;

  // ===========================================================================
  // DOM Structure
  // ===========================================================================

  // Layer container (covers viewport, pointer-events: none)
  const layer = document.createElement('div');
  layer.className = 'we-handles-layer';
  layer.setAttribute('aria-hidden', 'true');
  container.append(layer);
  disposer.add(() => layer.remove());

  // Selection frame (positioned by selection rect)
  const frame = document.createElement('div');
  frame.className = 'we-selection-frame';
  frame.hidden = true;
  layer.append(frame);

  // Size HUD (displays W×H while dragging)
  const sizeHud = document.createElement('div');
  sizeHud.className = 'we-size-hud';
  sizeHud.hidden = true;
  frame.append(sizeHud);

  // Create 8 resize handles
  const handleEls = new Map<ResizeHandleDir, HTMLDivElement>();
  for (const dir of HANDLE_DIRS) {
    const el = document.createElement('div');
    el.className = 'we-resize-handle';
    el.dataset.dir = dir;
    el.tabIndex = -1;
    frame.append(el);
    handleEls.set(dir, el);
  }

  // ===========================================================================
  // State
  // ===========================================================================

  let currentTarget: HTMLElement | null = null;
  let currentSelectionRect: ViewportRect | null = null;
  let session: ResizeSession | null = null;

  // rAF scheduling
  let rafId: number | null = null;

  function cancelRaf(): void {
    if (rafId !== null) {
      cancelAnimationFrame(rafId);
      rafId = null;
    }
  }
  disposer.add(cancelRaf);

  // ===========================================================================
  // Rendering
  // ===========================================================================

  /**
   * Render the selection frame at the given rect.
   */
  function renderSelectionRect(rect: ViewportRect | null): void {
    const shouldShow = !!currentTarget && isValidRect(rect);
    if (!shouldShow) {
      frame.hidden = true;
      return;
    }

    frame.hidden = false;
    frame.style.transform = `translate3d(${rect.left}px, ${rect.top}px, 0)`;
    frame.style.width = `${rect.width}px`;
    frame.style.height = `${rect.height}px`;
  }

  /**
   * Update the size HUD text.
   */
  function setHud(text: string | null): void {
    if (!text) {
      sizeHud.hidden = true;
      sizeHud.textContent = '';
      return;
    }
    sizeHud.hidden = false;
    sizeHud.textContent = text;
  }

  // ===========================================================================
  // Session Lifecycle
  // ===========================================================================

  /**
   * Restore body styles after resize session ends.
   */
  function restoreBodyStyles(s: ResizeSession): void {
    document.body.style.cursor = s.prevBodyCursor;
    document.body.style.userSelect = s.prevBodyUserSelect;
  }

  /**
   * Cancel the current resize session and rollback changes.
   */
  function cancelSession(reason: string): void {
    const s = session;
    if (!s) return;

    cancelRaf();
    session = null;

    // Rollback transaction (only if a resize actually started)
    if (s.tx) {
      try {
        s.tx.rollback();
      } catch (error) {
        console.warn(`${WEB_EDITOR_LOG_PREFIX} Resize rollback failed:`, error);
      }
    }

    // Restore body styles
    try {
      restoreBodyStyles(s);
    } catch {
      // Best-effort
    }

    // Clear snap overlays (Phase 4.2 & 4.3)
    try {
      canvasOverlay.setGuideLines(null);
      canvasOverlay.setDistanceLabels(null);
      canvasOverlay.render();
    } catch {
      // Best-effort
    }

    setHud(null);
    renderSelectionRect(currentSelectionRect);

    // Force position sync after rollback
    try {
      positionTracker.forceUpdate();
    } catch {
      // Best-effort
    }

    if (reason) {
      console.log(`${WEB_EDITOR_LOG_PREFIX} Resize cancelled (${reason})`);
    }
  }

  /**
   * Commit the current resize session.
   */
  function commitSession(): void {
    const s = session;
    if (!s) return;

    cancelRaf();
    session = null;

    // Commit transaction (no-op if drag never crossed threshold)
    if (s.tx) {
      try {
        s.tx.commit({ merge: false });
      } catch (error) {
        console.warn(`${WEB_EDITOR_LOG_PREFIX} Resize commit failed:`, error);
        // Attempt rollback on commit failure
        try {
          s.tx.rollback();
        } catch {
          // Best-effort
        }
      }
    }

    // Restore body styles
    try {
      restoreBodyStyles(s);
    } catch {
      // Best-effort
    }

    // Clear snap overlays (Phase 4.2 & 4.3)
    try {
      canvasOverlay.setGuideLines(null);
      canvasOverlay.setDistanceLabels(null);
      canvasOverlay.render();
    } catch {
      // Best-effort
    }

    setHud(null);

    // Force position sync after commit
    try {
      positionTracker.forceUpdate();
    } catch {
      // Best-effort
    }
  }

  /**
   * Schedule an update frame if not already scheduled.
   */
  function scheduleFrame(): void {
    if (rafId !== null || disposer.isDisposed) return;
    rafId = requestAnimationFrame(() => {
      rafId = null;
      updateFrame();
    });
  }

  /**
   * Update frame - apply style changes based on current drag position.
   */
  function updateFrame(): void {
    const s = session;
    if (!s) return;

    // Verify target is still connected
    if (!s.target.isConnected) {
      cancelSession('target_disconnected');
      return;
    }

    const dx = s.lastClientX - s.startClientX;
    const dy = s.lastClientY - s.startClientY;

    // Only start a real resize after pointer movement exceeds the threshold.
    // This prevents "clicking a handle" from generating a transaction or writing styles.
    if (!s.hasPassedThreshold) {
      if (Math.hypot(dx, dy) < RESIZE_DRAG_THRESHOLD_PX) {
        return;
      }

      s.hasPassedThreshold = true;

      const startedTx = transactionManager.beginMultiStyle(s.target, Array.from(s.properties));
      if (!startedTx) {
        cancelSession('tx_unavailable');
        return;
      }
      s.tx = startedTx;

      // Collect snap anchors once when gesture becomes active (Phase 4.2)
      // This avoids per-frame layout thrashing from reading sibling rects
      try {
        const siblingAnchors = collectSiblingAnchors(s.target);
        const viewportAnchors = collectViewportAnchors();
        s.anchors = mergeAnchors(siblingAnchors, viewportAnchors);
      } catch {
        // Snap will be disabled if anchor collection fails
        s.anchors = null;
      }
    }

    const tx = s.tx;
    if (!tx) {
      cancelSession('tx_missing');
      return;
    }

    // Calculate new border-box dimensions
    let nextWidthBorderBox = s.startRect.width;
    let nextHeightBorderBox = s.startRect.height;

    if (s.affectsWidth) {
      if (dirHasEast(s.dir)) {
        nextWidthBorderBox = clampMin(s.startRect.width + dx, MIN_BORDER_BOX_SIZE_PX);
      }
      if (dirHasWest(s.dir)) {
        nextWidthBorderBox = clampMin(s.startRect.width - dx, MIN_BORDER_BOX_SIZE_PX);
      }
    }

    if (s.affectsHeight) {
      if (dirHasSouth(s.dir)) {
        nextHeightBorderBox = clampMin(s.startRect.height + dy, MIN_BORDER_BOX_SIZE_PX);
      }
      if (dirHasNorth(s.dir)) {
        nextHeightBorderBox = clampMin(s.startRect.height - dy, MIN_BORDER_BOX_SIZE_PX);
      }
    }

    // Build proposed preview rect (before snapping)
    const proposedLeftDelta = s.hasWest ? s.startRect.width - nextWidthBorderBox : 0;
    const proposedTopDelta = s.hasNorth ? s.startRect.height - nextHeightBorderBox : 0;
    const proposedRect: ViewportRect = {
      left: s.startRect.left + proposedLeftDelta,
      top: s.startRect.top + proposedTopDelta,
      width: nextWidthBorderBox,
      height: nextHeightBorderBox,
    };

    // Apply snapping if anchors are available (Phase 4.2)
    let finalRect = proposedRect;
    if (s.anchors) {
      const hasEast = dirHasEast(s.dir);
      const hasSouth = dirHasSouth(s.dir);

      const snapResult = computeResizeSnap({
        rect: proposedRect,
        resize: {
          hasWest: s.hasWest,
          hasEast,
          hasNorth: s.hasNorth,
          hasSouth,
        },
        anchors: s.anchors,
        thresholdPx: WEB_EDITOR_SNAP_THRESHOLD_PX,
        hysteresisPx: WEB_EDITOR_SNAP_HYSTERESIS_PX,
        minSizePx: MIN_BORDER_BOX_SIZE_PX,
        lockX: s.lockX,
        lockY: s.lockY,
        viewport: {
          width: window.innerWidth || 1,
          height: window.innerHeight || 1,
        },
      });

      // Update lock state for hysteresis
      s.lockX = snapResult.lockX;
      s.lockY = snapResult.lockY;
      finalRect = snapResult.snappedRect;

      // Compute distance labels (Phase 4.3)
      const distanceLabels = computeDistanceLabels({
        rect: finalRect,
        lockX: s.lockX,
        lockY: s.lockY,
        minGapPx: WEB_EDITOR_DISTANCE_LABEL_MIN_PX,
        viewport: {
          width: window.innerWidth || 1,
          height: window.innerHeight || 1,
        },
      });

      // Draw guide lines and distance labels (only update if state changed)
      const hasGuides = snapResult.guideLines.length > 0;
      const hasDistanceLabels = distanceLabels.length > 0;

      if (hasGuides || s.hadGuidesLastFrame || hasDistanceLabels || s.hadDistanceLabelsLastFrame) {
        try {
          canvasOverlay.setGuideLines(hasGuides ? snapResult.guideLines : null);
          canvasOverlay.setDistanceLabels(hasDistanceLabels ? distanceLabels : null);
          canvasOverlay.render();
        } catch {
          // Best-effort; snapping still applies even if overlay fails
        }
        s.hadGuidesLastFrame = hasGuides;
        s.hadDistanceLabelsLastFrame = hasDistanceLabels;
      }

      // Update dimensions from snapped rect
      nextWidthBorderBox = finalRect.width;
      nextHeightBorderBox = finalRect.height;
    }

    // Calculate edge deltas from snapped rect (for position updates)
    const leftEdgeDelta = finalRect.left - s.startRect.left;
    const topEdgeDelta = finalRect.top - s.startRect.top;

    // Render preview immediately
    renderSelectionRect(finalRect);

    // Update HUD
    setHud(`${Math.round(finalRect.width)} × ${Math.round(finalRect.height)}`);

    // Build style changes
    const styles: Record<string, string> = {};

    if (s.affectsWidth) {
      const widthCssPx = borderBoxToCssSize(
        nextWidthBorderBox,
        s.extras.horizontalExtras,
        s.extras.boxSizing,
      );
      styles.width = formatPx(widthCssPx);
    }

    if (s.affectsHeight) {
      const heightCssPx = borderBoxToCssSize(
        nextHeightBorderBox,
        s.extras.verticalExtras,
        s.extras.boxSizing,
      );
      styles.height = formatPx(heightCssPx);
    }

    // Position-mode specific handling
    if (s.mode === 'absolute' || s.mode === 'fixed') {
      // For absolute/fixed: update left/top and clear right/bottom to avoid over-constraint
      if (s.affectsWidth) {
        styles.left = formatPx(s.startPosX + leftEdgeDelta);
        styles.right = '';
      }
      if (s.affectsHeight) {
        styles.top = formatPx(s.startPosY + topEdgeDelta);
        styles.bottom = '';
      }
    } else if (s.mode === 'relative') {
      // For relative: only update position if dragging from edge that needs it
      if (s.affectsWidth && s.hasWest) {
        styles.left = formatPx(s.startPosX + leftEdgeDelta);
      }
      if (s.affectsHeight && s.hasNorth) {
        styles.top = formatPx(s.startPosY + topEdgeDelta);
      }
    } else {
      // For static: use margin as best-effort fallback
      if (s.affectsWidth && s.hasWest) {
        styles['margin-left'] = formatPx(s.startPosX + leftEdgeDelta);
      }
      if (s.affectsHeight && s.hasNorth) {
        styles['margin-top'] = formatPx(s.startPosY + topEdgeDelta);
      }
    }

    // Apply styles
    try {
      tx.set(styles);
    } catch (error) {
      console.warn(`${WEB_EDITOR_LOG_PREFIX} Resize preview apply failed:`, error);
      cancelSession('apply_failed');
    }
  }

  /**
   * Start a resize session.
   */
  function startResize(dir: ResizeHandleDir, handleEl: HTMLElement, event: PointerEvent): void {
    if (disposer.isDisposed) return;

    // Only handle primary button
    if (event.button !== 0) return;

    const target = currentTarget;
    if (!target || !target.isConnected) return;

    // Cancel any existing session
    if (session) cancelSession('restart');

    const computed = safeGetComputedStyle(target);
    if (!computed) return;

    // Block transformed elements (matrix math required for proper handling)
    const transform = computed.getPropertyValue('transform').trim();
    if (transform && transform !== 'none') {
      console.warn(
        `${WEB_EDITOR_LOG_PREFIX} Resize handles do not support transformed elements yet`,
      );
      return;
    }

    const position = computed.getPropertyValue('position');
    const mode = getResizeMode(position);

    // Determine which axes are affected
    const hasWest = dirHasWest(dir);
    const hasNorth = dirHasNorth(dir);
    const affectsWidth = hasWest || dirHasEast(dir);
    const affectsHeight = hasNorth || dirHasSouth(dir);

    // Read margins (needed for fixed/absolute origin and static auto detection)
    const marginLeftRaw = computed.getPropertyValue('margin-left').trim().toLowerCase();
    const marginTopRaw = computed.getPropertyValue('margin-top').trim().toLowerCase();
    const marginLeftPx = parsePx(marginLeftRaw) ?? 0;
    const marginTopPx = parsePx(marginTopRaw) ?? 0;

    // Static positioning: margin:auto is commonly used for centering in flex/grid.
    // Resizing from that side would force a numeric margin and break layout.
    if (mode === 'static') {
      if (hasWest && marginLeftRaw === 'auto') {
        console.warn(
          `${WEB_EDITOR_LOG_PREFIX} Resize from west is disabled when margin-left is auto`,
        );
        return;
      }
      if (hasNorth && marginTopRaw === 'auto') {
        console.warn(
          `${WEB_EDITOR_LOG_PREFIX} Resize from north is disabled when margin-top is auto`,
        );
        return;
      }
    }

    const rect = isValidRect(currentSelectionRect)
      ? currentSelectionRect
      : readViewportRect(target);
    if (!rect || !isValidRect(rect)) return;

    // Build properties list for transaction
    const properties: string[] = [];

    if (affectsWidth) {
      properties.push('width');
      if (mode === 'absolute' || mode === 'fixed') {
        properties.push('left', 'right');
      } else if (mode === 'relative') {
        if (hasWest) properties.push('left');
      } else {
        if (hasWest) properties.push('margin-left');
      }
    }

    if (affectsHeight) {
      properties.push('height');
      if (mode === 'absolute' || mode === 'fixed') {
        properties.push('top', 'bottom');
      } else if (mode === 'relative') {
        if (hasNorth) properties.push('top');
      } else {
        if (hasNorth) properties.push('margin-top');
      }
    }

    // Calculate starting position based on mode
    let absOrigin: AbsoluteOrigin | null = null;
    let startPosX = 0;
    let startPosY = 0;

    if (mode === 'absolute') {
      absOrigin = computeAbsoluteOrigin(target);
      // Subtract margin to get the actual CSS left/top value
      startPosX = affectsWidth
        ? rect.left - marginLeftPx - absOrigin.originX + absOrigin.scrollLeft
        : 0;
      startPosY = affectsHeight
        ? rect.top - marginTopPx - absOrigin.originY + absOrigin.scrollTop
        : 0;
    } else if (mode === 'fixed') {
      absOrigin = { originX: 0, originY: 0, scrollLeft: 0, scrollTop: 0 };
      // Subtract margin to get the actual CSS left/top value
      startPosX = affectsWidth ? rect.left - marginLeftPx : 0;
      startPosY = affectsHeight ? rect.top - marginTopPx : 0;
    } else if (mode === 'relative') {
      startPosX = affectsWidth && hasWest ? (parsePx(computed.getPropertyValue('left')) ?? 0) : 0;
      startPosY = affectsHeight && hasNorth ? (parsePx(computed.getPropertyValue('top')) ?? 0) : 0;
    } else {
      startPosX = affectsWidth && hasWest ? marginLeftPx : 0;
      startPosY = affectsHeight && hasNorth ? marginTopPx : 0;
    }

    const extras = readBoxExtras(computed);

    // Save current body styles
    const prevBodyCursor = document.body.style.cursor;
    const prevBodyUserSelect = document.body.style.userSelect;

    // Create session (transaction is created after threshold is crossed)
    session = {
      pointerId: event.pointerId,
      dir,
      handleEl,
      target,
      mode,
      properties,
      tx: null,
      hasPassedThreshold: false,
      affectsWidth,
      affectsHeight,
      hasWest,
      hasNorth,
      // Snap state (Phase 4.2 & 4.3) - initialized to null, populated after threshold
      anchors: null,
      lockX: null,
      lockY: null,
      hadGuidesLastFrame: false,
      hadDistanceLabelsLastFrame: false,
      startClientX: event.clientX,
      startClientY: event.clientY,
      lastClientX: event.clientX,
      lastClientY: event.clientY,
      startRect: rect,
      startPosX,
      startPosY,
      absOrigin,
      extras,
      prevBodyCursor,
      prevBodyUserSelect,
    };

    // Capture pointer for robust tracking
    try {
      handleEl.setPointerCapture(event.pointerId);
    } catch {
      // Pointer capture may fail on some elements/browsers
    }

    // Apply drag visual affordances
    document.body.style.cursor = CURSOR_BY_DIR[dir];
    document.body.style.userSelect = 'none';

    stopEvent(event);

    // Initial render and schedule first frame
    renderSelectionRect(rect);
    scheduleFrame();
  }

  // ===========================================================================
  // Event Handlers
  // ===========================================================================

  function handlePointerMove(event: PointerEvent): void {
    const s = session;
    if (!s) return;
    if (event.pointerId !== s.pointerId) return;

    stopEvent(event);
    s.lastClientX = event.clientX;
    s.lastClientY = event.clientY;
    scheduleFrame();
  }

  function handlePointerUp(event: PointerEvent): void {
    const s = session;
    if (!s) return;
    if (event.pointerId !== s.pointerId) return;

    stopEvent(event);
    s.lastClientX = event.clientX;
    s.lastClientY = event.clientY;
    commitSession();
  }

  function handlePointerCancel(event: PointerEvent): void {
    const s = session;
    if (!s) return;
    if (event.pointerId !== s.pointerId) return;

    stopEvent(event);
    cancelSession(event.type);
  }

  /**
   * Handle ESC key - cancel resize without triggering EventController deselect.
   * Uses stopImmediatePropagation to prevent other handlers from seeing the event.
   */
  function handleKeyDown(event: KeyboardEvent): void {
    if (!session) return;
    if (event.key !== 'Escape') return;

    event.preventDefault();
    event.stopImmediatePropagation();
    event.stopPropagation();
    cancelSession('escape');
  }

  function handleWindowBlur(): void {
    if (!session) return;
    cancelSession('blur');
  }

  function handleVisibilityChange(): void {
    if (!session) return;
    if (document.visibilityState !== 'visible') {
      cancelSession('visibilitychange');
    }
  }

  // ===========================================================================
  // Event Wiring
  // ===========================================================================

  for (const [dir, el] of handleEls) {
    disposer.listen(el, 'pointerdown', (event: PointerEvent) => startResize(dir, el, event));
    disposer.listen(el, 'pointermove', handlePointerMove);
    disposer.listen(el, 'pointerup', handlePointerUp);
    disposer.listen(el, 'pointercancel', handlePointerCancel);
    disposer.listen(el, 'lostpointercapture', handlePointerCancel);
  }

  // Global event handlers
  disposer.listen(document, 'keydown', handleKeyDown, { capture: true });
  disposer.listen(window, 'blur', handleWindowBlur);
  disposer.listen(document, 'visibilitychange', handleVisibilityChange);

  // ===========================================================================
  // Public API
  // ===========================================================================

  function setTarget(target: Element | null): void {
    if (disposer.isDisposed) return;

    // Cancel active session when selection changes
    if (session) cancelSession('target_change');

    if (target instanceof HTMLElement && target.isConnected) {
      currentTarget = target;
    } else {
      currentTarget = null;
    }

    renderSelectionRect(currentSelectionRect);
  }

  function setSelectionRect(rect: ViewportRect | null): void {
    if (disposer.isDisposed) return;

    currentSelectionRect = isValidRect(rect) ? rect : null;

    // Hide if target is gone
    if (!currentTarget || !currentTarget.isConnected) {
      frame.hidden = true;
      return;
    }

    // Cancel session if rect becomes invalid
    if (session && !currentSelectionRect) {
      cancelSession('rect_lost');
      return;
    }

    // When idle, follow position tracker updates
    if (!session) {
      renderSelectionRect(currentSelectionRect);
    }
  }

  function dispose(): void {
    cancelSession('dispose');
    currentTarget = null;
    currentSelectionRect = null;
    disposer.dispose();
  }

  // Initial state
  renderSelectionRect(null);

  return { setTarget, setSelectionRect, dispose };
}
