import { useState, useCallback, useRef } from 'react';
import type { PageData, CBNode, EditorState, Breakpoint, HistoryEntry, GlobalStyles, GlobalColor, TypographyPreset, ButtonPreset, DesignSystem, DesignToken, TokenCategory, JalpiClass, InteractionRule, InteractionTrigger, InteractionAction, InteractionSettings } from '../types';
import { DEFAULT_DESIGN_SYSTEM, TOKEN_CATEGORIES } from '../utils/designTokens';

export const DEFAULT_GLOBAL_STYLES: GlobalStyles = {
  colors: [
    { id: 'primary',   name: 'Primary',   value: '#1a1a1a' },
    { id: 'secondary', name: 'Secondary', value: '#444444' },
    { id: 'accent',    name: 'Accent',    value: '#888888' },
    { id: 'neutral',   name: 'Neutral',   value: '#6b7280' },
    { id: 'white',     name: 'White',     value: '#ffffff' },
    { id: 'dark',      name: 'Dark',      value: '#111827' },
  ],
  typography: [
    { id: 'h1',   name: 'Heading 1', fontSize: '48px', fontWeight: '800', lineHeight: '1.2',  letterSpacing: '-0.02em', color: '#111827' },
    { id: 'h2',   name: 'Heading 2', fontSize: '36px', fontWeight: '700', lineHeight: '1.25', letterSpacing: '-0.01em', color: '#111827' },
    { id: 'h3',   name: 'Heading 3', fontSize: '28px', fontWeight: '700', lineHeight: '1.3',  color: '#111827' },
    { id: 'body', name: 'Body',      fontSize: '16px', fontWeight: '400', lineHeight: '1.7',  color: '#374151' },
    { id: 'lead', name: 'Lead',      fontSize: '20px', fontWeight: '400', lineHeight: '1.7',  color: '#374151' },
    { id: 'sm',   name: 'Small',     fontSize: '14px', fontWeight: '400', lineHeight: '1.6',  color: '#6b7280' },
  ],
  buttons: [
    { id: 'primary',   name: 'Primary',   backgroundColor: '#1a1a1a', color: '#ffffff',  borderRadius: '8px', paddingTop: '12px', paddingBottom: '12px', paddingLeft: '28px', paddingRight: '28px', fontSize: '15px', fontWeight: '600', border: 'none',              boxShadow: '0 2px 12px rgba(0,0,0,0.20)' },
    { id: 'secondary', name: 'Secondary', backgroundColor: 'transparent',  color: '#1a1a1a',  borderRadius: '8px', paddingTop: '11px', paddingBottom: '11px', paddingLeft: '27px', paddingRight: '27px', fontSize: '15px', fontWeight: '600', border: '1.5px solid #1a1a1a', boxShadow: 'none' },
    { id: 'ghost',     name: 'Ghost',     backgroundColor: 'rgba(0,0,0,0.04)', color: '#1a1a1a', borderRadius: '8px', paddingTop: '11px', paddingBottom: '11px', paddingLeft: '27px', paddingRight: '27px', fontSize: '15px', fontWeight: '500', border: 'none', boxShadow: 'none' },
  ],
  spacing: { xs: '4px', sm: '8px', md: '16px', lg: '32px', xl: '64px', '2xl': '128px' },
};

const MAX_HISTORY = 30;

function cloneDeep<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

// Pure helper — merges history into a setState return value without a nested setState call.
// Call this INSIDE a setState updater: return { ...prev, pageData, isDirty: true, ...mkHistory(prev, pageData, 'Label') }
function mkHistory(
  prev: EditorState,
  newData: PageData,
  label?: string
): Pick<EditorState, 'history' | 'historyIndex'> {
  const entry: HistoryEntry = { pageData: cloneDeep(newData), timestamp: Date.now(), label };
  const trimmed = prev.history.slice(0, prev.historyIndex + 1);
  const next    = [...trimmed, entry].slice(-MAX_HISTORY);
  return { history: next, historyIndex: next.length - 1 };
}

function generateId(type: string): string {
  return `${type}_${Math.random().toString(36).slice(2, 9)}`;
}

// Flat-map walk: find a node by id anywhere in the tree
function findNode(sections: CBNode[], id: string): CBNode | null {
  for (const node of sections) {
    if (node.id === id) return node;
    if (node.children) {
      const found = findNode(node.children, id);
      if (found) return found;
    }
  }
  return null;
}

// Return parent + index for a node id
function findParent(
  sections: CBNode[],
  id: string,
  parent: CBNode[] | null = null
): { parent: CBNode[]; index: number } | null {
  for (let i = 0; i < sections.length; i++) {
    if (sections[i].id === id) {
      return { parent: sections, index: i };
    }
    if (sections[i].children) {
      const found = findParent(sections[i].children!, id, sections);
      if (found) return found;
    }
  }
  return null;
}

// Immutably update a node anywhere in the tree
// Merge an imported list into an existing one by `id` (imported entries win on
// collision, new ids append, un-mentioned existing ids are kept), or fully
// replace when mode === 'replace'. Missing/malformed `incoming` leaves
// `existing` untouched either way — used by dsImportJSON() so a partial
// export (only some categories present) imports cleanly under both modes.
function mergeTokensById<T extends { id: string }>(existing: T[], incoming: unknown, mode: 'merge' | 'replace'): T[] {
  if (!Array.isArray(incoming)) return existing;
  if (mode === 'replace') return incoming as T[];
  const byId = new Map(existing.map((t) => [t.id, t]));
  for (const t of incoming as T[]) {
    if (t && typeof t.id === 'string') byId.set(t.id, t);
  }
  return Array.from(byId.values());
}

function updateNodeInTree(sections: CBNode[], id: string, updater: (n: CBNode) => CBNode): CBNode[] {
  return sections.map((node) => {
    if (node.id === id) return updater(node);
    if (node.children) {
      return { ...node, children: updateNodeInTree(node.children, id, updater) };
    }
    return node;
  });
}

// Immutably delete a node
function deleteNodeFromTree(sections: CBNode[], id: string): CBNode[] {
  return sections
    .filter((node) => node.id !== id)
    .map((node) => {
      if (node.children) {
        return { ...node, children: deleteNodeFromTree(node.children, id) };
      }
      return node;
    });
}

// Insert a NEW node before or after a target node anywhere in the tree
function insertNodeRelativeTo(
  sections: CBNode[],
  newNode: CBNode,
  targetId: string,
  position: 'before' | 'after'
): CBNode[] {
  return sections.flatMap((node) => {
    if (node.id === targetId) {
      return position === 'before' ? [newNode, node] : [node, newNode];
    }
    if (node.children) {
      return [{ ...node, children: insertNodeRelativeTo(node.children, newNode, targetId, position) }];
    }
    return [node];
  });
}

// Reassign fresh ids to a node's descendants (NOT the node itself — its id is
// set by the caller). Used when adding a widget whose defaultNode ships nested
// children with placeholder ids (e.g. Tabs (Advanced) tab-panel containers), so
// two instances on a page never collide. A no-op for widgets with no children.
function withFreshChildIds(node: CBNode): CBNode {
  if (!node.children || node.children.length === 0) return node;
  const reassign = (n: CBNode): CBNode => ({
    ...n,
    id: generateId(n.type),
    children: n.children?.map(reassign),
  });
  return { ...node, children: node.children.map(reassign) };
}

// Move a node to just before or after a target node anywhere in the tree
function moveNodeInTree(
  sections: CBNode[],
  nodeId: string,
  targetId: string,
  position: 'before' | 'after'
): CBNode[] {
  const node = findNode(sections, nodeId);
  if (!node || nodeId === targetId) return sections;

  const withoutNode = deleteNodeFromTree(sections, nodeId);

  function insert(arr: CBNode[]): CBNode[] {
    const out: CBNode[] = [];
    for (const item of arr) {
      if (item.id === targetId) {
        const updated = item.children ? { ...item, children: insert(item.children) } : item;
        if (position === 'before') {
          out.push(cloneDeep(node));
          out.push(updated);
        } else {
          out.push(updated);
          out.push(cloneDeep(node));
        }
      } else {
        out.push(item.children ? { ...item, children: insert(item.children) } : item);
      }
    }
    return out;
  }

  return insert(withoutNode);
}

export function useEditorStore(initialPostId: number, initialDesignSystem?: DesignSystem) {
  const [state, setState] = useState<EditorState>({
    postId: initialPostId,
    pageData: { version: '1.0', sections: [] },
    selectedId: null,
    hoveredId: null,
    breakpoint: 'desktop',
    isDirty: false,
    isSaving: false,
    isPublishing: false,
    history: [],
    historyIndex: -1,
    panelTab: 'properties',
    showLayers: true,
    zoom: 100,
    clipboard: null,
    styleClipboard: null,
    selectionSeq: 0,
    designSystem: initialDesignSystem ?? DEFAULT_DESIGN_SYSTEM,
    designSystemDirty: false,
    designSystemSaving: false,
    classes: [],
    classesLoaded: false,
    editingClassKey: null,
  });

  const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // --- history ---

  const undo = useCallback(() => {
    setState((prev) => {
      if (prev.historyIndex <= 0) return prev;
      const idx = prev.historyIndex - 1;
      return {
        ...prev,
        pageData: cloneDeep(prev.history[idx].pageData),
        historyIndex: idx,
        isDirty: true,
        selectedId: null,
      };
    });
  }, []);

  const redo = useCallback(() => {
    setState((prev) => {
      if (prev.historyIndex >= prev.history.length - 1) return prev;
      const idx = prev.historyIndex + 1;
      return {
        ...prev,
        pageData: cloneDeep(prev.history[idx].pageData),
        historyIndex: idx,
        isDirty: true,
        selectedId: null,
      };
    });
  }, []);

  // --- page data ---

  const setPageData = useCallback(
    (pageData: PageData, recordHistory = true) => {
      setState((prev) => ({
        ...prev,
        pageData,
        isDirty: true,
        ...(recordHistory ? mkHistory(prev, pageData) : {}),
      }));
    },
    []
  );

  const loadPageData = useCallback((pageData: PageData) => {
    const withDefaults: PageData = {
      ...pageData,
      globalStyles: pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES,
    };
    setState((prev) => ({
      ...prev,
      pageData: withDefaults,
      isDirty: false,
      history: [{ pageData: cloneDeep(withDefaults), timestamp: Date.now(), label: 'Initial' }],
      historyIndex: 0,
      selectedId: null,
    }));
  }, []);

  // --- selection ---

  const selectNode = useCallback((id: string | null) => {
    setState((prev) => ({ ...prev, selectedId: id, panelTab: 'properties', selectionSeq: prev.selectionSeq + 1 }));
  }, []);

  const hoverNode = useCallback((id: string | null) => {
    setState((prev) => ({ ...prev, hoveredId: id }));
  }, []);

  // --- node mutations ---

  const updateNode = useCallback(
    (id: string, updates: Partial<CBNode>) => {
      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, id, (node) => ({
          ...node,
          ...updates,
          styles: updates.styles !== undefined ? updates.styles : node.styles,
          content: updates.content !== undefined ? { ...node.content, ...updates.content } : node.content,
          settings: updates.settings !== undefined ? { ...node.settings, ...updates.settings } : node.settings,
        }));
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true };
      });
    },
    []
  );

  const updateNodeStyles = useCallback(
    (id: string, styles: Record<string, string>) => {
      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, id, (node) => ({
          ...node,
          styles: { ...node.styles, ...styles },
        }));
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true };
      });
    },
    []
  );

  const updateNodeSettings = useCallback(
    (id: string, settings: Record<string, unknown>) => {
      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, id, (node) => ({
          ...node,
          settings: { ...node.settings, ...settings },
        }));
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true };
      });
    },
    []
  );

  const updateNodeContent = useCallback(
    (id: string, content: Record<string, unknown>) => {
      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, id, (node) => ({
          ...node,
          content: { ...node.content, ...content } as CBNode['content'],
        }));
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true };
      });
    },
    []
  );

  // ── Tabs (Advanced) helpers ─────────────────────────────────────────────
  // Keep the tabs node's content.items[] (tab metadata) and children[] (per-tab
  // container panels) in lockstep. A tab is items[i] + children[i].
  type TabItem = { id: string; title: string; icon?: unknown; cssId?: string };
  const emptyTabPanel = (): CBNode => ({ id: generateId('container'), type: 'container', settings: { columns: 1 }, children: [] });

  /**
   * Turn an Advanced Menu item's mega panel on or off.
   *
   * Enabling appends a container child and records its index against the
   * WordPress menu-item id. Disabling only clears the flag and KEEPS both the
   * panel and its index — removing the child would shift every later panel's
   * index and silently re-point other items' content. The orphan costs one
   * empty container and is skipped at render.
   */
  const setAdvMenuMega = useCallback((nodeId: string, itemId: number, enabled: boolean) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (node) => {
        const content = (node.content ?? {}) as Record<string, unknown>;
        const mega = { ...((content.megaItems ?? {}) as Record<string, { enabled?: boolean; cols?: number; panelIndex?: number }>) };
        const key = String(itemId);
        const existing = mega[key];

        if (!enabled) {
          mega[key] = { ...(existing ?? {}), enabled: false, cols: existing?.cols ?? 3, panelIndex: existing?.panelIndex ?? -1 };
          return { ...node, content: { ...content, megaItems: mega } as unknown as CBNode['content'] };
        }

        // Re-enabling an item reuses the panel it had before, so content
        // authored earlier comes back rather than starting empty.
        const children = node.children ?? [];
        const hasPanel = typeof existing?.panelIndex === 'number' && existing.panelIndex >= 0 && !!children[existing.panelIndex];

        if (hasPanel) {
          mega[key] = { ...existing, enabled: true, cols: existing?.cols ?? 3 };
          return { ...node, content: { ...content, megaItems: mega } as unknown as CBNode['content'] };
        }

        mega[key] = { enabled: true, cols: existing?.cols ?? 3, panelIndex: children.length };
        return {
          ...node,
          content: { ...content, megaItems: mega } as unknown as CBNode['content'],
          children: [...children, emptyTabPanel()],
        };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, enabled ? 'Enable Mega Panel' : 'Disable Mega Panel') };
    });
  }, []);

  /** Set the column count for one Advanced Menu mega panel. */
  const setAdvMenuMegaCols = useCallback((nodeId: string, itemId: number, cols: number) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (node) => {
        const content = (node.content ?? {}) as Record<string, unknown>;
        const mega = { ...((content.megaItems ?? {}) as Record<string, { enabled?: boolean; cols?: number; panelIndex?: number }>) };
        const key = String(itemId);
        if (!mega[key]) return node;
        mega[key] = { ...mega[key], cols: Math.max(1, Math.min(12, cols)) };
        return { ...node, content: { ...content, megaItems: mega } as unknown as CBNode['content'] };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const addTab = useCallback((nodeId: string) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (node) => {
        const items = (((node.content ?? {}) as { items?: TabItem[] }).items ?? []) as TabItem[];
        const nextItems = [...items, { id: generateId('tab'), title: `Tab #${items.length + 1}`, icon: '', cssId: '' }];
        return {
          ...node,
          content: { ...node.content, items: nextItems, activeTab: nextItems.length - 1 } as unknown as CBNode['content'],
          children: [...(node.children ?? []), emptyTabPanel()],
        };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Add Tab') };
    });
  }, []);

  const removeTab = useCallback((nodeId: string, idx: number) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (node) => {
        const items = (((node.content ?? {}) as { items?: TabItem[] }).items ?? []) as TabItem[];
        if (items.length <= 1) return node; // keep at least one tab
        const nextItems = items.filter((_, i) => i !== idx);
        const nextChildren = (node.children ?? []).filter((_, i) => i !== idx);
        const active = Math.min(((node.content as { activeTab?: number })?.activeTab ?? 0), nextItems.length - 1);
        return {
          ...node,
          content: { ...node.content, items: nextItems, activeTab: Math.max(0, active) } as unknown as CBNode['content'],
          children: nextChildren,
        };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Remove Tab') };
    });
  }, []);

  const duplicateTab = useCallback((nodeId: string, idx: number) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (node) => {
        const items = (((node.content ?? {}) as { items?: TabItem[] }).items ?? []) as TabItem[];
        const children = (node.children ?? []);
        if (idx < 0 || idx >= items.length) return node;
        const dupItem: TabItem = { ...items[idx], id: generateId('tab'), title: `${items[idx].title} copy` };
        const dupPanel = withFreshChildIds({ ...cloneDeep(children[idx] ?? emptyTabPanel()), id: generateId('container') } as CBNode);
        const nextItems = [...items.slice(0, idx + 1), dupItem, ...items.slice(idx + 1)];
        const nextChildren = [...children.slice(0, idx + 1), dupPanel, ...children.slice(idx + 1)];
        return {
          ...node,
          content: { ...node.content, items: nextItems, activeTab: idx + 1 } as unknown as CBNode['content'],
          children: nextChildren,
        };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Duplicate Tab') };
    });
  }, []);

  const addSection = useCallback(
    (afterId?: string) => {
      const newSection: CBNode = {
        id: generateId('section'),
        type: 'section',
        settings: {
          background: { type: 'color', value: '#ffffff' },
          padding: { top: '80px', right: '0', bottom: '80px', left: '0' },
        },
        children: [
          {
            id: generateId('container'),
            type: 'container',
            settings: { columns: 1 },
            children: [
              {
                id: generateId('heading'),
                type: 'heading',
                content: { text: 'Your Heading Here', tag: 'h2' },
                styles: { fontSize: '36px', fontWeight: '700', color: '#1a1a1a', textAlign: 'center' },
              },
            ],
          },
        ],
      };

      setState((prev) => {
        let sections: CBNode[];
        if (afterId) {
          const idx = prev.pageData.sections.findIndex((s) => s.id === afterId);
          sections = [...prev.pageData.sections];
          sections.splice(idx + 1, 0, newSection);
        } else {
          sections = [...prev.pageData.sections, newSection];
        }
        const newData = { ...prev.pageData, sections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: newSection.id, ...mkHistory(prev, newData, 'Add Section') };
      });
    },
    []
  );

  const addElement = useCallback(
    (parentId: string, elementDef: Partial<CBNode>) => {
      const newNode: CBNode = withFreshChildIds({
        id: generateId(elementDef.type ?? 'text'),
        type: elementDef.type ?? 'text',
        ...elementDef,
      } as CBNode);

      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, parentId, (node) => ({
          ...node,
          children: [...(node.children ?? []), newNode],
        }));
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: newNode.id, ...mkHistory(prev, newData, 'Add Element') };
      });
    },
    []
  );

  // Adds an element wrapped in a new container directly to a section.
  // Special case: if the element itself IS a container, add it directly to the
  // section to avoid Section > Container > Container double-nesting.
  const addElementInContainer = useCallback(
    (sectionId: string, elementDef: Partial<CBNode>) => {
      const elementId = generateId(elementDef.type ?? 'text');

      let nodeToAdd: CBNode;
      let selectedAfter: string;

      if (elementDef.type === 'container') {
        // The element IS a container — add it straight to the section
        nodeToAdd = {
          id:       elementId,
          type:     'container',
          settings: (elementDef.settings as CBNode['settings']) ?? { columns: 1 },
          children: (elementDef.children as CBNode[]) ?? [],
          ...(elementDef.styles ? { styles: elementDef.styles } : {}),
        };
        selectedAfter = elementId;
      } else {
        // Regular element — wrap it in a new container inside the section.
        // withFreshChildIds reassigns any nested defaultNode children (e.g. a
        // Tabs (Advanced) widget's tab-panel containers) to unique ids.
        const containerId = generateId('container');
        nodeToAdd = {
          id:       containerId,
          type:     'container',
          settings: { columns: 1 },
          children: [withFreshChildIds({
            id:   elementId,
            type: elementDef.type ?? 'text',
            ...elementDef,
          } as CBNode)],
        };
        selectedAfter = elementId;
      }

      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, sectionId, (node) => ({
          ...node,
          children: [...(node.children ?? []), nodeToAdd],
        }));
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: selectedAfter, ...mkHistory(prev, newData, 'Add Element') };
      });
    },
    []
  );

  // Drop a brand-new element from the elements panel beside an existing element.
  const addElementBesideNode = useCallback(
    (def: Partial<CBNode>, targetId: string, position: 'before' | 'after') => {
      const newNode: CBNode = withFreshChildIds({
        id:   generateId(def.type ?? 'text'),
        type: (def.type ?? 'text') as ElementType,
        ...def,
      } as CBNode);
      setState((prev) => {
        const newSections = insertNodeRelativeTo(prev.pageData.sections, newNode, targetId, position);
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: newNode.id, ...mkHistory(prev, newData, 'Add Element') };
      });
    },
    []
  );

  const setContainerColumns = useCallback(
    (containerId: string, count: number) => {
      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, containerId, (node) => {
          const current       = node.children ?? [];
          const currentCols   = current.filter((c) => c.type === 'column');
          const currentElems  = current.filter((c) => c.type !== 'column');
          const inColumnMode  = currentCols.length > 0 && current.every((c) => c.type === 'column');

          if (count <= 1) {
            // → Block mode: flatten column contents back into direct children.
            // Content is preserved, not destroyed.
            if (!inColumnMode) return node; // already in block mode — nothing to do
            const flatChildren = currentCols.flatMap((col) => col.children ?? []);
            return { ...node, children: flatChildren };
          }

          // → Multi-column mode
          const pct = Math.round(100 / count);
          const columns: CBNode[] = Array.from({ length: count }, (_, i) => {
            const existingCol = currentCols[i];
            if (existingCol) {
              // Resize an existing column
              return { ...existingCol, settings: { ...(existingCol.settings ?? {}), width: `${pct}%` } };
            }
            // New column — if coming from block mode, move existing elements into col 0
            return {
              id:       generateId('column'),
              type:     'column' as const,
              settings: { width: `${pct}%` },
              children: (!inColumnMode && i === 0) ? currentElems : [],
            };
          });
          return { ...node, children: columns };
        });
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Set Columns') };
      });
    },
    []
  );

  const deleteNode = useCallback(
    (id: string) => {
      setState((prev) => {
        const newSections = deleteNodeFromTree(prev.pageData.sections, id);
        const newData = { ...prev.pageData, sections: newSections };
        return {
          ...prev,
          pageData: newData,
          isDirty: true,
          selectedId: prev.selectedId === id ? null : prev.selectedId,
          ...mkHistory(prev, newData, 'Delete'),
        };
      });
    },
    []
  );

  const duplicateNode = useCallback(
    (id: string) => {
      setState((prev) => {
        const node = findNode(prev.pageData.sections, id);
        if (!node) return prev;

        const cloned = cloneDeep(node);
        const reassignIds = (n: CBNode): CBNode => ({
          ...n,
          id: generateId(n.type),
          children: n.children?.map(reassignIds),
        });
        const duplicate = reassignIds(cloned);

        let newSections: CBNode[];
        const isTopLevel = prev.pageData.sections.some((s) => s.id === id);
        if (isTopLevel) {
          const idx = prev.pageData.sections.findIndex((s) => s.id === id);
          newSections = [...prev.pageData.sections];
          newSections.splice(idx + 1, 0, duplicate);
        } else {
          const insertAfterInParent = (sections: CBNode[]): CBNode[] =>
            sections.map((section) => {
              if (section.children) {
                const idx = section.children.findIndex((c) => c.id === id);
                if (idx !== -1) {
                  const children = [...section.children];
                  children.splice(idx + 1, 0, duplicate);
                  return { ...section, children };
                }
                return { ...section, children: insertAfterInParent(section.children) };
              }
              return section;
            });
          newSections = insertAfterInParent(prev.pageData.sections);
        }

        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: duplicate.id, ...mkHistory(prev, newData, 'Duplicate') };
      });
    },
    []
  );

  const moveSection = useCallback(
    (id: string, direction: 'up' | 'down') => {
      setState((prev) => {
        const sections = [...prev.pageData.sections];
        const idx = sections.findIndex((s) => s.id === id);
        if (idx === -1) return prev;
        if (direction === 'up' && idx === 0) return prev;
        if (direction === 'down' && idx === sections.length - 1) return prev;

        const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
        [sections[idx], sections[swapIdx]] = [sections[swapIdx], sections[idx]];
        const newData = { ...prev.pageData, sections };
        return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Move Section') };
      });
    },
    []
  );

  const moveNode = useCallback(
    (id: string, direction: 'up' | 'down') => {
      setState((prev) => {
        // Top-level section
        const secIdx = prev.pageData.sections.findIndex((s) => s.id === id);
        if (secIdx !== -1) {
          const sections = [...prev.pageData.sections];
          const swapIdx = direction === 'up' ? secIdx - 1 : secIdx + 1;
          if (swapIdx < 0 || swapIdx >= sections.length) return prev;
          [sections[secIdx], sections[swapIdx]] = [sections[swapIdx], sections[secIdx]];
          const newData = { ...prev.pageData, sections };
          return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Move Node') };
        }
        // Move within parent
        const moveInChildren = (nodes: CBNode[]): CBNode[] =>
          nodes.map((node) => {
            if (!node.children) return node;
            const idx = node.children.findIndex((c) => c.id === id);
            if (idx !== -1) {
              const children = [...node.children];
              const swap = direction === 'up' ? idx - 1 : idx + 1;
              if (swap >= 0 && swap < children.length) {
                [children[idx], children[swap]] = [children[swap], children[idx]];
              }
              return { ...node, children };
            }
            return { ...node, children: moveInChildren(node.children) };
          });
        const newSections = moveInChildren(prev.pageData.sections);
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Move Node') };
      });
    },
    []
  );

  const copyNode = useCallback((id: string) => {
    setState((prev) => {
      const node = findNode(prev.pageData.sections, id);
      if (!node) return prev;
      return { ...prev, clipboard: cloneDeep(node) };
    });
  }, []);

  const pasteNode = useCallback(
    (afterId: string) => {
      setState((prev) => {
        if (!prev.clipboard) return prev;
        const reassignIds = (n: CBNode): CBNode => ({
          ...n,
          id: generateId(n.type),
          children: n.children?.map(reassignIds),
        });
        const pasted = reassignIds(cloneDeep(prev.clipboard));
        const secIdx = prev.pageData.sections.findIndex((s) => s.id === afterId);
        if (secIdx !== -1) {
          const sections = [...prev.pageData.sections];
          sections.splice(secIdx + 1, 0, pasted);
          const newData = { ...prev.pageData, sections };
          return { ...prev, pageData: newData, isDirty: true, selectedId: pasted.id, ...mkHistory(prev, newData, 'Paste') };
        }
        const insertAfter = (nodes: CBNode[]): CBNode[] =>
          nodes.map((node) => {
            if (!node.children) return node;
            const idx = node.children.findIndex((c) => c.id === afterId);
            if (idx !== -1) {
              const children = [...node.children];
              children.splice(idx + 1, 0, pasted);
              return { ...node, children };
            }
            return { ...node, children: insertAfter(node.children) };
          });
        const newSections = insertAfter(prev.pageData.sections);
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: pasted.id, ...mkHistory(prev, newData, 'Paste') };
      });
    },
    []
  );

  const insertElementTemplate = useCallback((elementNode: CBNode) => {
    setState((prev) => {
      const reassignIds = (n: CBNode): CBNode => ({
        ...n,
        id: generateId(n.type),
        children: n.children?.map(reassignIds),
      });
      const fresh = reassignIds(cloneDeep(elementNode));

      // Insert into selected container/column, or the first available container
      const targetId = prev.selectedId;
      if (targetId) {
        const target = findNode(prev.pageData.sections, targetId);
        if (target && (target.type === 'container' || target.type === 'column')) {
          const newSections = updateNodeInTree(prev.pageData.sections, targetId, (n) => ({
            ...n,
            children: [...(n.children ?? []), fresh],
          }));
          const newData = { ...prev.pageData, sections: newSections };
          return { ...prev, pageData: newData, isDirty: true, selectedId: fresh.id, ...mkHistory(prev, newData, 'Insert Element Template') };
        }
      }

      // Fallback: find the first container or column in the page
      const findFirstContainer = (nodes: CBNode[]): CBNode | null => {
        for (const n of nodes) {
          if (n.type === 'container' || n.type === 'column') return n;
          const found = findFirstContainer(n.children ?? []);
          if (found) return found;
        }
        return null;
      };
      const fallback = findFirstContainer(prev.pageData.sections);
      if (!fallback) return prev;

      const newSections = updateNodeInTree(prev.pageData.sections, fallback.id, (n) => ({
        ...n,
        children: [...(n.children ?? []), fresh],
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, selectedId: fresh.id, ...mkHistory(prev, newData, 'Insert Element Template') };
    });
  }, []);

  const copyNodeStyle = useCallback((id: string) => {
    setState((prev) => {
      const node = findNode(prev.pageData.sections, id);
      if (!node) return prev;
      return { ...prev, styleClipboard: cloneDeep(node.styles ?? {}) };
    });
  }, []);

  const pasteNodeStyle = useCallback((id: string) => {
    setState((prev) => {
      if (!prev.styleClipboard) return prev;
      const newSections = updateNodeInTree(prev.pageData.sections, id, (node) => ({
        ...node,
        styles: { ...(node.styles ?? {}), ...prev.styleClipboard },
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Paste Style') };
    });
  }, []);

  const addSectionWithLayout = useCallback(
    (afterId: string | undefined, layout: 'block1' | 'block2' | 'block3' | 'flex' | 'grid') => {
      const makeColumns = (count: number): CBNode[] =>
        Array.from({ length: count }, () => ({
          id: generateId('column'),
          type: 'column' as const,
          settings: { width: `${Math.round(100 / count)}%` },
          children: [] as CBNode[],
        }));

      let containerSettings: Record<string, unknown>;
      let containerChildren: CBNode[];

      if (layout === 'block2') {
        containerSettings = { columns: 2 };
        containerChildren = makeColumns(2);
      } else if (layout === 'block3') {
        containerSettings = { columns: 3 };
        containerChildren = makeColumns(3);
      } else if (layout === 'flex') {
        containerSettings = { layoutMode: 'flex', flexDirection: 'row', flexWrapMode: 'wrap', flexGap: '20px', flexJustify: 'flex-start', flexAlign: 'stretch' };
        containerChildren = [];
      } else if (layout === 'grid') {
        containerSettings = { layoutMode: 'grid', gridAutoMode: 'fill', gridMinColSize: '300px', gridGap: '24px' };
        containerChildren = [];
      } else {
        containerSettings = { columns: 1 };
        containerChildren = [];
      }

      const newSection: CBNode = {
        id: generateId('section'),
        type: 'section',
        settings: { padding: { top: '60px', right: '0', bottom: '60px', left: '0' } },
        children: [{
          id: generateId('container'),
          type: 'container',
          settings: containerSettings as CBNode['settings'],
          children: containerChildren,
        }],
      };

      setState((prev) => {
        let sections: CBNode[];
        if (afterId) {
          const idx = prev.pageData.sections.findIndex((s) => s.id === afterId);
          sections = [...prev.pageData.sections];
          sections.splice(idx + 1, 0, newSection);
        } else {
          sections = [...prev.pageData.sections, newSection];
        }
        const newData = { ...prev.pageData, sections };
        const containerId = (newSection.children![0] as CBNode).id;
        return { ...prev, pageData: newData, isDirty: true, selectedId: containerId, ...mkHistory(prev, newData, 'Add Section') };
      });
    },
    []
  );

  const addElementInNewSection = useCallback(
    (afterSectionId: string | undefined, elementDef: Partial<CBNode>) => {
      const elementId = generateId(elementDef.type ?? 'text');

      // When the element being added IS a container, use it directly as the
      // section's child — avoids Section > Container > Container double-nesting.
      const sectionChildren: CBNode[] = elementDef.type === 'container'
        ? [{
            id:       elementId,
            type:     'container',
            settings: (elementDef.settings as CBNode['settings']) ?? { columns: 1 },
            children: (elementDef.children as CBNode[]) ?? [],
            ...(elementDef.styles ? { styles: elementDef.styles } : {}),
          }]
        : [{
            id:       generateId('container'),
            type:     'container',
            settings: { columns: 1 } as CBNode['settings'],
            children: [{ id: elementId, type: elementDef.type ?? 'text', ...elementDef } as CBNode],
          }];

      const newSection: CBNode = {
        id:       generateId('section'),
        type:     'section',
        settings: { padding: { top: '60px', right: '0', bottom: '60px', left: '0' } },
        children: sectionChildren,
      };

      setState((prev) => {
        let sections: CBNode[];
        if (afterSectionId) {
          const idx = prev.pageData.sections.findIndex((s) => s.id === afterSectionId);
          sections = [...prev.pageData.sections];
          sections.splice(idx + 1, 0, newSection);
        } else {
          sections = [...prev.pageData.sections, newSection];
        }
        const newData = { ...prev.pageData, sections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: elementId, ...mkHistory(prev, newData, 'Add Element') };
      });
    },
    []
  );

  const reorderSections = useCallback(
    (newSections: CBNode[]) => {
      setState((prev) => {
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Reorder') };
      });
    },
    []
  );

  const insertTemplateSection = useCallback(
    (sectionNode: CBNode, afterId?: string) => {
      const reassignIds = (n: CBNode): CBNode => ({
        ...n,
        id: generateId(n.type),
        children: n.children?.map(reassignIds),
      });
      const fresh = reassignIds(cloneDeep(sectionNode));

      setState((prev) => {
        let sections: CBNode[];
        if (afterId) {
          const idx = prev.pageData.sections.findIndex((s) => s.id === afterId);
          sections = [...prev.pageData.sections];
          sections.splice(idx + 1, 0, fresh);
        } else {
          sections = [...prev.pageData.sections, fresh];
        }
        const newData = { ...prev.pageData, sections };
        return { ...prev, pageData: newData, isDirty: true, selectedId: fresh.id, ...mkHistory(prev, newData, 'Insert Template Section') };
      });
    },
    []
  );

  const loadTemplateAsPage = useCallback(
    (sections: CBNode[]) => {
      const reassignIds = (n: CBNode): CBNode => ({
        ...n,
        id: generateId(n.type),
        children: n.children?.map(reassignIds),
      });
      const newData: PageData = { version: '1.0', sections: sections.map(reassignIds) };
      setState((prev) => ({ ...prev, pageData: newData, isDirty: true, selectedId: null, ...mkHistory(prev, newData, 'Load Page Template') }));
    },
    []
  );

  // --- UI state ---

  const updateNodeHideOn = useCallback(
    (id: string, device: 'desktop' | 'tablet' | 'mobile', hidden: boolean) => {
      setState((prev) => {
        const newSections = updateNodeInTree(prev.pageData.sections, id, (node) => ({
          ...node,
          hideOn: { ...(node.hideOn ?? {}), [device]: hidden },
        }));
        const newData = { ...prev.pageData, sections: newSections };
        return { ...prev, pageData: newData, isDirty: true };
      });
    },
    []
  );

  // --- Layer lock / visibility / reorder ---

  const lockNode = useCallback((id: string, locked: boolean) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, id, (node) => ({
        ...node,
        locked,
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const toggleNodeVisibility = useCallback((id: string) => {
    setState((prev) => {
      const node = findNode(prev.pageData.sections, id);
      const allHidden = node?.hideOn?.desktop && node?.hideOn?.tablet && node?.hideOn?.mobile;
      const hideOn = allHidden
        ? {}
        : { desktop: true as const, tablet: true as const, mobile: true as const };
      const newSections = updateNodeInTree(prev.pageData.sections, id, (n) => ({
        ...n,
        hideOn,
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const moveNodeTo = useCallback((nodeId: string, targetId: string, position: 'before' | 'after') => {
    setState((prev) => {
      const newSections = moveNodeInTree(prev.pageData.sections, nodeId, targetId, position);
      if (newSections === prev.pageData.sections) return prev;
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Move') };
    });
  }, []);

  // Move a node inside a container as its last child (drag-drop "drop inside")
  const moveNodeInside = useCallback((nodeId: string, targetParentId: string) => {
    setState((prev) => {
      const moving = findNode(prev.pageData.sections, nodeId);
      if (!moving || nodeId === targetParentId) return prev;
      const without     = deleteNodeFromTree(prev.pageData.sections, nodeId);
      const newSections = updateNodeInTree(without, targetParentId, (parent) => ({
        ...parent,
        children: [...(parent.children ?? []), cloneDeep(moving)],
      }));
      if (newSections === without) return prev;
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Move') };
    });
  }, []);

  // --- Global Styles ---

  const getGlobalStyles = () =>
    (state.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES);

  const addGlobalColor = useCallback(() => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const id = `color-${Date.now()}`;
      const newColors = [...gs.colors, { id, name: 'New Color', value: '#1a1a1a' }];
      const newData = { ...prev.pageData, globalStyles: { ...gs, colors: newColors } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const updateGlobalColor = useCallback((id: string, updates: Partial<GlobalColor>) => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const newColors = gs.colors.map((c) => (c.id === id ? { ...c, ...updates } : c));
      const newData = { ...prev.pageData, globalStyles: { ...gs, colors: newColors } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const removeGlobalColor = useCallback((id: string) => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const newColors = gs.colors.filter((c) => c.id !== id);
      const newData = { ...prev.pageData, globalStyles: { ...gs, colors: newColors } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const addTypographyPreset = useCallback(() => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const id = `typo-${Date.now()}`;
      const preset: TypographyPreset = { id, name: 'New Preset', fontSize: '16px', fontWeight: '400', lineHeight: '1.6', color: '#111827' };
      const newData = { ...prev.pageData, globalStyles: { ...gs, typography: [...gs.typography, preset] } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const updateTypographyPreset = useCallback((id: string, updates: Partial<TypographyPreset>) => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const newTypo = gs.typography.map((t) => (t.id === id ? { ...t, ...updates } : t));
      const newData = { ...prev.pageData, globalStyles: { ...gs, typography: newTypo } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const removeTypographyPreset = useCallback((id: string) => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const newTypo = gs.typography.filter((t) => t.id !== id);
      const newData = { ...prev.pageData, globalStyles: { ...gs, typography: newTypo } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const addButtonPreset = useCallback(() => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const id = `btn-${Date.now()}`;
      const preset: ButtonPreset = { id, name: 'New Button', backgroundColor: '#1a1a1a', color: '#ffffff', borderRadius: '8px', paddingTop: '12px', paddingBottom: '12px', paddingLeft: '24px', paddingRight: '24px', fontSize: '15px', fontWeight: '600', border: 'none', boxShadow: 'none' };
      const newData = { ...prev.pageData, globalStyles: { ...gs, buttons: [...gs.buttons, preset] } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const updateButtonPreset = useCallback((id: string, updates: Partial<ButtonPreset>) => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const newBtns = gs.buttons.map((b) => (b.id === id ? { ...b, ...updates } : b));
      const newData = { ...prev.pageData, globalStyles: { ...gs, buttons: newBtns } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const removeButtonPreset = useCallback((id: string) => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const newBtns = gs.buttons.filter((b) => b.id !== id);
      const newData = { ...prev.pageData, globalStyles: { ...gs, buttons: newBtns } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const updateSpacing = useCallback((key: keyof import('../types').SpacingScale, value: string) => {
    setState((prev) => {
      const gs = prev.pageData.globalStyles ?? DEFAULT_GLOBAL_STYLES;
      const newData = { ...prev.pageData, globalStyles: { ...gs, spacing: { ...gs.spacing, [key]: value } } };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const linkNodeToPreset = useCallback((nodeId: string, ref: CBNode['globalStyleRef']) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => {
        // Preserve any existing atomic token links (ref.tokens) — this call only
        // ever sets/clears the composite typographyId/buttonId fields.
        const tokens = n.globalStyleRef?.tokens;
        const hasTokens = tokens && Object.keys(tokens).length > 0;
        const merged = ref && (ref.typographyId || ref.buttonId) ? { ...ref, tokens } : (hasTokens ? { tokens } : undefined);
        return { ...n, globalStyleRef: merged };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  // --- Design System (site-wide) ---

  const dsAddToken = useCallback((category: TokenCategory) => {
    setState((prev) => {
      const list = prev.designSystem[category] as DesignToken[];
      const token: DesignToken = { id: `${category}-${Date.now()}`, name: 'New Token', value: '' };
      return { ...prev, designSystem: { ...prev.designSystem, [category]: [...list, token] }, designSystemDirty: true };
    });
  }, []);

  const dsUpdateToken = useCallback((category: TokenCategory, id: string, updates: Partial<DesignToken>) => {
    setState((prev) => {
      const list = (prev.designSystem[category] as DesignToken[]).map((t) => (t.id === id ? { ...t, ...updates } : t));
      return { ...prev, designSystem: { ...prev.designSystem, [category]: list }, designSystemDirty: true };
    });
  }, []);

  const dsRemoveToken = useCallback((category: TokenCategory, id: string) => {
    setState((prev) => {
      const list = (prev.designSystem[category] as DesignToken[]).filter((t) => t.id !== id);
      return { ...prev, designSystem: { ...prev.designSystem, [category]: list }, designSystemDirty: true };
    });
  }, []);

  const dsAddTypography = useCallback(() => {
    setState((prev) => {
      const id = `typo-${Date.now()}`;
      const preset: TypographyPreset = { id, name: 'New Preset', fontSize: '16px', fontWeight: '400', lineHeight: '1.6', color: '#111827' };
      return { ...prev, designSystem: { ...prev.designSystem, typography: [...prev.designSystem.typography, preset] }, designSystemDirty: true };
    });
  }, []);

  const dsUpdateTypography = useCallback((id: string, updates: Partial<TypographyPreset>) => {
    setState((prev) => {
      const typography = prev.designSystem.typography.map((t) => (t.id === id ? { ...t, ...updates } : t));
      return { ...prev, designSystem: { ...prev.designSystem, typography }, designSystemDirty: true };
    });
  }, []);

  const dsRemoveTypography = useCallback((id: string) => {
    setState((prev) => {
      const typography = prev.designSystem.typography.filter((t) => t.id !== id);
      return { ...prev, designSystem: { ...prev.designSystem, typography }, designSystemDirty: true };
    });
  }, []);

  const dsAddButton = useCallback(() => {
    setState((prev) => {
      const id = `btn-${Date.now()}`;
      const preset: ButtonPreset = { id, name: 'New Button', backgroundColor: '#1a1a1a', color: '#ffffff', borderRadius: '8px', paddingTop: '12px', paddingBottom: '12px', paddingLeft: '24px', paddingRight: '24px', fontSize: '15px', fontWeight: '600', border: 'none', boxShadow: 'none' };
      return { ...prev, designSystem: { ...prev.designSystem, buttons: [...prev.designSystem.buttons, preset] }, designSystemDirty: true };
    });
  }, []);

  const dsUpdateButton = useCallback((id: string, updates: Partial<ButtonPreset>) => {
    setState((prev) => {
      const buttons = prev.designSystem.buttons.map((b) => (b.id === id ? { ...b, ...updates } : b));
      return { ...prev, designSystem: { ...prev.designSystem, buttons }, designSystemDirty: true };
    });
  }, []);

  const dsRemoveButton = useCallback((id: string) => {
    setState((prev) => {
      const buttons = prev.designSystem.buttons.filter((b) => b.id !== id);
      return { ...prev, designSystem: { ...prev.designSystem, buttons }, designSystemDirty: true };
    });
  }, []);

  // Links a single style property on a node to an atomic Design System token.
  // This edits the PAGE (node.globalStyleRef lives in pageData.sections), so it
  // marks isDirty — distinct from designSystemDirty, which only a token's own
  // value edit sets. See dsAddToken/dsUpdateToken above.
  const linkNodeToken = useCallback((nodeId: string, styleProp: string, category: TokenCategory, tokenId: string) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => ({
        ...n,
        globalStyleRef: {
          ...n.globalStyleRef,
          tokens: { ...(n.globalStyleRef?.tokens ?? {}), [styleProp]: { category, id: tokenId } },
        },
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const unlinkNodeToken = useCallback((nodeId: string, styleProp: string) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => {
        if (!n.globalStyleRef?.tokens?.[styleProp]) return n;
        const tokens = { ...n.globalStyleRef.tokens };
        delete tokens[styleProp];
        const hasTokens = Object.keys(tokens).length > 0;
        const ref = { ...n.globalStyleRef, tokens: hasTokens ? tokens : undefined };
        return { ...n, globalStyleRef: (ref.typographyId || ref.buttonId || ref.tokens) ? ref : undefined };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  // --- Interaction Engine ---
  // Extends the (previously FAQ-only) Animation panel into a generic,
  // per-node rule list. Same updateNodeInTree pattern as linkNodeToken/
  // unlinkNodeToken above — this is page data (persists with the page, via
  // the normal save flow), not part of the site-wide Design System.

  const addInteraction = useCallback((nodeId: string, trigger: InteractionTrigger = 'pageLoad', action: InteractionAction = 'fade', settings: InteractionSettings = {}) => {
    setState((prev) => {
      const rule: InteractionRule = { id: generateId('ia'), trigger, action, enabled: true, settings };
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => ({
        ...n,
        interactions: [...(n.interactions ?? []), rule],
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const updateInteraction = useCallback((nodeId: string, ruleId: string, updates: Partial<InteractionRule>) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => ({
        ...n,
        interactions: (n.interactions ?? []).map((r) =>
          r.id === ruleId
            ? { ...r, ...updates, settings: updates.settings ? { ...r.settings, ...updates.settings } : r.settings }
            : r
        ),
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const removeInteraction = useCallback((nodeId: string, ruleId: string) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => ({
        ...n,
        interactions: (n.interactions ?? []).filter((r) => r.id !== ruleId),
      }));
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  const reorderInteractions = useCallback((nodeId: string, ruleIds: string[]) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => {
        const byId = new Map((n.interactions ?? []).map((r) => [r.id, r]));
        const reordered = ruleIds.map((id) => byId.get(id)).filter((r): r is InteractionRule => !!r);
        return { ...n, interactions: reordered };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true };
    });
  }, []);

  // One-time convenience: copy this page's legacy pageData.globalStyles into
  // the new site-wide store (see GlobalStylesPanel.tsx's import banner).
  // Colors/typography/buttons share an identical shape and copy verbatim; the
  // old fixed spacing object is exploded into DesignToken entries.
  const dsImportFromLegacy = useCallback((legacy: GlobalStyles) => {
    setState((prev) => {
      const spacing: DesignToken[] = Object.entries(legacy.spacing ?? {}).map(([id, value]) => ({
        id,
        name: id.toUpperCase(),
        value: value as string,
      }));
      return {
        ...prev,
        designSystem: {
          ...prev.designSystem,
          colors: legacy.colors?.length ? legacy.colors : prev.designSystem.colors,
          typography: legacy.typography?.length ? legacy.typography : prev.designSystem.typography,
          buttons: legacy.buttons?.length ? legacy.buttons : prev.designSystem.buttons,
          spacing: spacing.length ? spacing : prev.designSystem.spacing,
        },
        designSystemDirty: true,
      };
    });
  }, []);

  // Import a previously-exported Design System JSON blob (see GlobalStylesPanel.tsx's
  // Export/Import UI). Tolerant of partial files — a category absent from `raw` is
  // left untouched regardless of mode. The actual authority on validity is still
  // the server-side Jalpi_Sanitizer::sanitize_design_system() run on save; this is
  // just enough client-side normalization to merge/replace safely before that.
  const dsImportJSON = useCallback((raw: unknown, mode: 'merge' | 'replace') => {
    if (!raw || typeof raw !== 'object') return;
    const data = raw as Record<string, unknown>;
    setState((prev) => {
      const next: DesignSystem = { ...prev.designSystem };
      for (const category of TOKEN_CATEGORIES) {
        next[category] = mergeTokensById(prev.designSystem[category] as DesignToken[], data[category], mode);
      }
      next.typography = mergeTokensById(prev.designSystem.typography, data.typography, mode);
      next.buttons = mergeTokensById(prev.designSystem.buttons, data.buttons, mode);
      return { ...prev, designSystem: next, designSystemDirty: true };
    });
  }, []);

  const setDesignSystemSaving = useCallback((saving: boolean) => {
    setState((prev) => ({ ...prev, designSystemSaving: saving }));
  }, []);

  const markDesignSystemSaved = useCallback(() => {
    setState((prev) => ({ ...prev, designSystemDirty: false }));
  }, []);

  // --- Class-Based Styling System ---
  // REST calls (create/update/delete/duplicate/favorite/reorder) live in the
  // UI components via useApi() — these are local-state sync actions applied
  // after a successful API response, same separation as templates/revisions.

  const setClasses = useCallback((classes: JalpiClass[]) => {
    setState((prev) => ({
      ...prev,
      classes: [...classes].sort((a, b) => a.sortOrder - b.sortOrder),
      classesLoaded: true,
    }));
  }, []);

  const upsertClass = useCallback((cls: JalpiClass) => {
    setState((prev) => {
      const exists = prev.classes.some((c) => c.key === cls.key);
      const next = exists
        ? prev.classes.map((c) => (c.key === cls.key ? cls : c))
        : [...prev.classes, cls];
      next.sort((a, b) => a.sortOrder - b.sortOrder);
      return { ...prev, classes: next };
    });
  }, []);

  const removeClassFromStore = useCallback((key: string) => {
    setState((prev) => ({ ...prev, classes: prev.classes.filter((c) => c.key !== key) }));
  }, []);

  const setClassesOrder = useCallback((keys: string[]) => {
    setState((prev) => {
      const byKey = new Map(prev.classes.map((c) => [c.key, c]));
      const reordered = keys
        .map((k, i) => {
          const c = byKey.get(k);
          return c ? { ...c, sortOrder: i + 1 } : null;
        })
        .filter((c): c is JalpiClass => !!c);
      return { ...prev, classes: reordered };
    });
  }, []);

  const setEditingClassKey = useCallback((key: string | null) => {
    setState((prev) => ({ ...prev, editingClassKey: key }));
  }, []);

  const addClassToNode = useCallback((nodeId: string, classKey: string) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => {
        const current = ((n.settings as { cssClasses?: string[] } | undefined)?.cssClasses) ?? [];
        if (current.includes(classKey)) return n;
        return { ...n, settings: { ...n.settings, cssClasses: [...current, classKey] } };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Attach Class') };
    });
  }, []);

  const removeClassFromNode = useCallback((nodeId: string, classKey: string) => {
    setState((prev) => {
      const newSections = updateNodeInTree(prev.pageData.sections, nodeId, (n) => {
        const current = ((n.settings as { cssClasses?: string[] } | undefined)?.cssClasses) ?? [];
        return { ...n, settings: { ...n.settings, cssClasses: current.filter((k) => k !== classKey) } };
      });
      const newData = { ...prev.pageData, sections: newSections };
      return { ...prev, pageData: newData, isDirty: true, ...mkHistory(prev, newData, 'Detach Class') };
    });
  }, []);

  const setBreakpoint = useCallback((bp: Breakpoint) => {
    setState((prev) => ({ ...prev, breakpoint: bp }));
  }, []);

  const setPanelTab = useCallback((tab: EditorState['panelTab']) => {
    setState((prev) => ({ ...prev, panelTab: tab }));
  }, []);

  const toggleLayers = useCallback(() => {
    setState((prev) => ({ ...prev, showLayers: !prev.showLayers }));
  }, []);

  const setZoom = useCallback((zoom: number) => {
    setState((prev) => ({ ...prev, zoom: Math.min(150, Math.max(50, zoom)) }));
  }, []);

  const setSaving = useCallback((isSaving: boolean) => {
    setState((prev) => ({ ...prev, isSaving }));
  }, []);

  const setPublishing = useCallback((isPublishing: boolean) => {
    setState((prev) => ({ ...prev, isPublishing }));
  }, []);

  const markSaved = useCallback(() => {
    setState((prev) => ({ ...prev, isDirty: false }));
  }, []);

  // Getters
  const getSelectedNode = useCallback((): CBNode | null => {
    if (!state.selectedId) return null;
    return findNode(state.pageData.sections, state.selectedId);
  }, [state.selectedId, state.pageData.sections]);

  return {
    state,
    // actions
    setPageData,
    loadPageData,
    selectNode,
    hoverNode,
    updateNode,
    updateNodeStyles,
    updateNodeSettings,
    updateNodeContent,
    addSection,
    addElement,
    addElementInContainer,
    addElementBesideNode,
    setAdvMenuMega,
    setAdvMenuMegaCols,
    addTab,
    removeTab,
    duplicateTab,
    setContainerColumns,
    deleteNode,
    duplicateNode,
    moveSection,
    moveNode,
    copyNode,
    pasteNode,
    copyNodeStyle,
    insertElementTemplate,
    pasteNodeStyle,
    addSectionWithLayout,
    addElementInNewSection,
    reorderSections,
    insertTemplateSection,
    loadTemplateAsPage,
    updateNodeHideOn,
    lockNode,
    toggleNodeVisibility,
    moveNodeTo,
    moveNodeInside,
    getGlobalStyles,
    addGlobalColor,
    updateGlobalColor,
    removeGlobalColor,
    addTypographyPreset,
    updateTypographyPreset,
    removeTypographyPreset,
    addButtonPreset,
    updateButtonPreset,
    removeButtonPreset,
    updateSpacing,
    linkNodeToPreset,
    // Design System (site-wide)
    dsAddToken,
    dsUpdateToken,
    dsRemoveToken,
    dsAddTypography,
    dsUpdateTypography,
    dsRemoveTypography,
    dsAddButton,
    dsUpdateButton,
    dsRemoveButton,
    linkNodeToken,
    unlinkNodeToken,
    addInteraction,
    updateInteraction,
    removeInteraction,
    reorderInteractions,
    dsImportFromLegacy,
    dsImportJSON,
    setDesignSystemSaving,
    markDesignSystemSaved,
    // Class-Based Styling System
    setClasses,
    upsertClass,
    removeClassFromStore,
    setClassesOrder,
    setEditingClassKey,
    addClassToNode,
    removeClassFromNode,
    undo,
    redo,
    setBreakpoint,
    setPanelTab,
    toggleLayers,
    setZoom,
    setSaving,
    setPublishing,
    markSaved,
    getSelectedNode,
    // utils
    saveTimerRef,
  };
}

export type EditorStore = ReturnType<typeof useEditorStore>;
