import { useEditor } from '@craftjs/core';
import { regenerateTreeIds } from '../utils/craft-tree';
import { findDeletableTarget } from '../utils/craft-helpers';

export interface NodeActions {
  moveUp: () => void;
  moveDown: () => void;
  /** Duplicates `nodeId`, inserting the copy immediately after the source in
   *  the parent's children and selecting it. Returns the new node's id (so
   *  callers -- e.g. `MobileSelectionToolbar` -- can scroll it into view),
   *  or null if the duplicate could not be performed (no-op for ROOT/null,
   *  or a caught error). */
  duplicate: () => string | null;
  deleteNode: () => void;
  selectParent: () => void;
  /** True if `nodeId` has an earlier sibling under the same parent (so
   *  `moveUp` would actually move it). False for ROOT/null/no-parent. */
  canMoveUp: boolean;
  /** True if `nodeId` has a later sibling under the same parent (so
   *  `moveDown` would actually move it). False for ROOT/null/no-parent. */
  canMoveDown: boolean;
  /** True if `findDeletableTarget` resolves to a real, deletable node (either
   *  `nodeId` itself, or an ancestor when `nodeId` is an empty linked-node
   *  slot whose siblings are all also empty -- see `craft-helpers.ts`). */
  canDelete: boolean;
  /** True if `nodeId` has a real parent to select -- i.e. the parent is
   *  neither ROOT nor missing. `selectParent` on a top-level section (whose
   *  parent IS 'ROOT') would only select the page-wide ROOT node, which has
   *  no on-canvas outline and no toolbar of its own -- a dead end for a
   *  mobile user with no way back. False for ROOT/null/no-parent too. */
  canSelectParent: boolean;
}

/**
 * Shared node-action logic (move up/down, duplicate, delete, select parent)
 * extracted from `ContextMenu.tsx` (Phase B) so both the desktop right-click
 * menu AND the mobile on-canvas selection toolbar (`MobileSelectionToolbar`)
 * drive the exact same behavior from one place instead of two independent
 * copies drifting apart.
 *
 * IMPORTANT gotcha this hook works around: `@craftjs/core`'s `useEditor`
 * collector (`@craftjs/utils`' `useCollector`) only synchronously computes
 * the collector function ONCE, on this hook's very first mount. After that,
 * it updates purely in reaction to the underlying store's OWN change
 * notifications, invoking whatever collector closure is current AT THAT
 * NOTIFICATION -- so a collector that closes over `nodeId` (an argument that
 * changes across renders of the SAME mounted hook instance, e.g. every time
 * `MobileSelectionToolbar` re-renders with a newly-selected node) goes stale
 * for exactly one render: the cached value from the last store notification
 * (computed against the PREVIOUS nodeId) is what gets returned, until some
 * unrelated store event happens to trigger a fresh computation. An earlier
 * version of this hook computed `canMoveUp`/`canMoveDown`/`canDelete` inside
 * such a collector and was caught showing the PREVIOUS selection's move
 * boundaries in the mobile toolbar for one render after tapping a new node
 * (verified with real Playwright touch taps against a real Craft.js
 * document -- a plain mocked `useEditor` in a unit test doesn't reproduce
 * this, since a hand-rolled mock has no reason to replicate the real
 * library's caching).
 *
 * The fix: get `actions`/`query` from a collector-FREE `useEditor()` call
 * (an always-live reference, same pattern `ContextMenu.tsx` used before this
 * extraction) and compute `canMoveUp`/`canMoveDown`/`canDelete` as plain
 * synchronous code during render using that live `query` -- never cached.
 *
 * That still leaves one gap: `nodeId` staying the SAME across renders while
 * its position changes (e.g. tapping "Move Up" repeatedly on the same
 * still-selected node) needs SOMETHING to trigger a re-render so the boundary
 * flags below get recomputed. An earlier version of this hook forced that
 * via a second `useEditor((state) => ({ _: state.nodes }))` subscription --
 * but subscribing to the WHOLE node map reacts to every `dom` ref
 * assignment too (Craft's `connectors.connect(ref)` calls `actions.setDOM`
 * synchronously from a React ref callback during COMMIT, i.e. while some
 * OTHER component is still mounting), which trips React's "Cannot update a
 * component while rendering a different component" warning the moment a
 * freshly-added container with children mounts. Callers that need
 * `canMoveUp`/`canMoveDown` to refresh after a move they themselves
 * triggered (`MobileSelectionToolbar`) should instead bump their OWN local
 * state right after calling `moveUp`/`moveDown` -- a plain, local,
 * event-handler-triggered re-render, not a store-wide subscription.
 */
export function useNodeActions(nodeId: string | null | undefined): NodeActions {
  const { actions, query } = useEditor();

  const isRootOrNull = !nodeId || nodeId === 'ROOT';

  let canMoveUp = false;
  let canMoveDown = false;
  let canSelectParent = false;
  if (!isRootOrNull) {
    try {
      const node = query.node(nodeId).get();
      const parentId: string | null | undefined = node?.data?.parent;
      if (parentId) {
        canSelectParent = parentId !== 'ROOT';
        const siblings: string[] = query.node(parentId).get()?.data?.nodes || [];
        const idx = siblings.indexOf(nodeId);
        canMoveUp = idx > 0;
        canMoveDown = idx !== -1 && idx < siblings.length - 1;
      }
    } catch {
      // Node no longer exists (e.g. deleted out from under a stale
      // reference) -- leave both false.
    }
  }

  const canDelete = !isRootOrNull && !!findDeletableTarget(query, nodeId);

  const getParentId = (): string | null => {
    if (!nodeId) return null;
    try {
      const node = query.node(nodeId).get();
      return node?.data?.parent || null;
    } catch {
      return null;
    }
  };

  const duplicate = (): string | null => {
    if (!nodeId || nodeId === 'ROOT') return null;
    try {
      const parentId = getParentId();
      if (!parentId) return null;

      const tree = regenerateTreeIds(query.node(nodeId).toNodeTree());

      // Insert the copy IMMEDIATELY AFTER the source node, not appended at
      // the end of the parent -- on a top-level section, "append at end"
      // landed the copy at the bottom of the page, off-screen, with the
      // ORIGINAL still selected, so a duplicate looked like nothing had
      // happened at all. Resolve the source's index among its siblings so
      // the copy lands right next to what it was copied from; if that can't
      // be resolved for any reason, fall back to the old append behavior
      // rather than guessing at an index (or throwing).
      let insertIndex: number | undefined;
      try {
        const siblings: string[] = query.node(parentId).get()?.data?.nodes || [];
        const sourceIndex = siblings.indexOf(nodeId);
        if (sourceIndex !== -1) insertIndex = sourceIndex + 1;
      } catch {
        // Leave insertIndex undefined -- addNodeTree appends when omitted.
      }

      if (insertIndex !== undefined) {
        actions.addNodeTree(tree, parentId, insertIndex);
      } else {
        actions.addNodeTree(tree, parentId);
      }

      // Select the new copy (not the original) so the toolbar/context menu
      // and any on-canvas outline immediately reflect what was just created.
      actions.selectNode(tree.rootNodeId);

      return tree.rootNodeId;
    } catch (e) {
      console.error('Duplicate failed:', e);
      return null;
    }
  };

  const moveUp = () => {
    if (!nodeId || nodeId === 'ROOT') return;
    try {
      const parentId = getParentId();
      if (!parentId) return;
      const parent = query.node(parentId).get();
      const children = parent.data.nodes || [];
      const idx = children.indexOf(nodeId);
      if (idx > 0) {
        actions.move(nodeId, parentId, idx - 1);
      }
    } catch (e) {
      console.error('Move up failed:', e);
    }
  };

  const moveDown = () => {
    if (!nodeId || nodeId === 'ROOT') return;
    try {
      const parentId = getParentId();
      if (!parentId) return;
      const parent = query.node(parentId).get();
      const children = parent.data.nodes || [];
      const idx = children.indexOf(nodeId);
      if (idx < children.length - 1) {
        actions.move(nodeId, parentId, idx + 2);
      }
    } catch (e) {
      console.error('Move down failed:', e);
    }
  };

  const selectParent = () => {
    if (!nodeId || nodeId === 'ROOT') return;
    const parentId = getParentId();
    if (parentId) {
      actions.selectNode(parentId);
    }
  };

  const deleteNode = () => {
    const target = findDeletableTarget(query, nodeId);
    if (!target) return;
    try {
      actions.delete(target);
    } catch (e) {
      console.error('Delete failed:', e);
    }
  };

  return {
    moveUp,
    moveDown,
    duplicate,
    deleteNode,
    selectParent,
    canMoveUp,
    canMoveDown,
    canDelete,
    canSelectParent,
  };
}
