import { useEffect, useRef, useCallback, useState } from 'react';
import * as Blockly from 'blockly';
import { javascriptGenerator } from 'blockly/javascript';

import { getBlocksForFramework, getBlock } from '../../blocks/registry';
import { initializeBlocks } from '../../blocks/init';
import { BlockDefinition, BlockShape, CATEGORY_COLORS, CATEGORY_ICONS } from '../../types/blocks';
import { useProjectStore } from '../../store/projectStore';
import { useEditorStore } from '../../store/editorStore';
import { useCollaborationStore } from '../../store/collaborationStore';
import { useProjectSocket } from '../../hooks/useWebSocket';
import { getColor } from '../Collaboration/CollaboratorAvatars';
import { Maximize2, Trash2, MousePointer2 } from 'lucide-react';

// Bridge for Blockly callbacks to set React modal state
let setEditModalData: ((data: EditModalData | null) => void) | null = null;

interface EditModalData {
  blockType: string;
  block: any;
  values: Record<string, string>;
}

// Variable type options
const VAR_TYPES = [
  { label: 'Any', value: 'any' },
  { label: 'Text', value: 'string' },
  { label: 'Number', value: 'number' },
  { label: 'True/False', value: 'boolean' },
  { label: 'List', value: 'array' },
];

// Inject Blockly CSS fixes for flyout/toolbox
function injectBlocklyCSS() {
  const id = 'rb-blockly-css-fixes';
  if (document.getElementById(id)) return;
  const style = document.createElement('style');
  style.id = id;
  style.textContent = `
    .blocklyFlyout {
      min-width: 220px !important;
      background: #1e293b !important;
      border-right: 1px solid #334155 !important;
    }
    .blocklyFlyout .blocklyBlockCanvas {
      padding: 12px 8px !important;
    }
    .blocklyFlyout .blocklyBlockCanvas .blocklyDraggable {
      margin-bottom: 8px !important;
    }
    .blocklyToolboxDiv {
      background: #0f172a !important;
      border-right: 1px solid #1e293b !important;
    }
    .blocklyToolboxDiv .blocklyTreeRow {
      padding: 6px 8px !important;
      margin: 0 !important;
      border-bottom: 1px solid #1e293b !important;
      height: auto !important;
      min-height: 36px !important;
    }
    .blocklyToolboxDiv .blocklyTreeRow:hover {
      background: rgba(99, 102, 241, 0.15) !important;
    }
    .blocklyToolboxDiv .blocklyTreeRow.blocklyTreeSelected {
      background: rgba(99, 102, 241, 0.25) !important;
    }
    .blocklyTreeIcon {
      display: none !important;
    }
    .blocklyTreeLabel {
      font-size: 13px !important;
      font-weight: 500 !important;
      padding-left: 4px !important;
    }
    .blocklyTreeRow .blocklyTreeIcon + .blocklyTreeLabel {
      padding-left: 28px !important;
    }
    .blocklyScrollbarHandle {
      fill: rgba(148, 163, 184, 0.3) !important;
    }
    .blocklyScrollbarHandle:hover {
      fill: rgba(148, 163, 184, 0.5) !important;
    }
    .blocklyFlyoutButton {
      fill: #334155 !important;
    }
    .blocklyFlyoutButton:hover {
      fill: #475569 !important;
    }
  `;
  document.head.appendChild(style);
}

// Get all variable names from variable definition blocks on the workspace
function getWorkspaceVariableNames(): string[] {
  const ws = Blockly.getMainWorkspace();
  if (!ws) return [];
  const vars = new Set<string>();
  const topBlocks = ws.getTopBlocks(false);
  const queue = [...topBlocks];
  while (queue.length > 0) {
    const block = queue.shift()!;
    if (block.type === 'rb_js_var') {
      try {
        const name = block.getFieldValue('name');
        if (name) vars.add(name);
      } catch {}
    }
    (block.getChildren(false) || []).forEach((c: any) => queue.push(c));
    const next = block.getNextBlock();
    if (next) queue.push(next);
  }
  return Array.from(vars).sort();
}

// Create a dynamic variable dropdown for Blockly
function dynamicVariableOptions(): any[][] {
  const names = getWorkspaceVariableNames();
  if (names.length === 0) return [['myVar', 'myVar']];
  return names.map(n => [n, n]);
}

// Create a dynamic element selector dropdown for Blockly.
// Shows elements from:
// 1. The active file's own visual elements (when editing HTML)
// 2. Elements from HTML files linked to the active JS/CSS file
// 3. All HTML files' elements as a fallback
function dynamicElementOptions(): any[][] {
  try {
    const state = useEditorStore.getState();
    const { elementRegistry, activeFileId, fileTree } = state;
    const selectors = new Set<string>();

    const walk = (els: any[]) => {
      for (const el of els) {
        if (el.props?.id) selectors.add(`#${el.props.id}`);
        if (el.tagName) selectors.add(el.tagName);
        el.classes?.forEach((c: string) => selectors.add(`.${c}`));
        if (el.children) walk(el.children);
      }
    };

    // Walk elements from a given file, return whether any were found
    const walkFile = (fileId: string): boolean => {
      const els = elementRegistry[fileId] || [];
      if (els.length > 0) {
        walk(els);
        return true;
      }
      return false;
    };

    // 1. Active file's own elements (if it's an HTML file)
    if (activeFileId) {
      walkFile(activeFileId);
    }

    // 2. Elements from HTML files that LINK TO the active file
    // (e.g., index.html links to app.js -> when editing app.js, show index.html's elements)
    if (activeFileId) {
      const linkingHtmlFiles = fileTree.filter(
        f => f.type === 'html' && (f.linkedFiles || []).includes(activeFileId)
      );
      for (const hf of linkingHtmlFiles) {
        walkFile(hf.id);
      }
    }

    // 3. Elements from ALL HTML files (broad fallback so blocks always have options)
    const allHtmlFiles = fileTree.filter(f => f.type === 'html');
    for (const hf of allHtmlFiles) {
      walkFile(hf.id);
    }

    const sorted = Array.from(selectors).sort();
    if (sorted.length === 0) return [['body', 'body'], ['#myId', '#myId'], ['.myClass', '.myClass']];
    return sorted.map(s => [s, s]);
  } catch {
    return [['body', 'body'], ['#myId', '#myId']];
  }
}

// Find a file in the file tree by id
function findFileInTree(files: any[], id: string): any {
  for (const f of files) {
    if (f.id === id) return f;
    if (f.children) {
      const found = findFileInTree(f.children, id);
      if (found) return found;
    }
  }
  return null;
}

initializeBlocks();

// Category label mapping (child-friendly names)
const CATEGORY_LABELS: Record<string, string> = {
  events: 'Events',
  variables: 'Variables',
  logic: 'Logic',
  loops: 'Loops',
  math: 'Math',
  text: 'Text',
  lists: 'Lists',
  date: 'Date & Time',
  functions: 'Functions',
  dom: 'Elements',
  css: 'Styles',
  data: 'Data',
  html: 'HTML',
  types: 'Types',
  electron: 'Electron',
  ipc: 'IPC',
  dialog: 'Dialogs',
  clipboard: 'Clipboard',
  screen: 'Screen',
  preload: 'Preload',
  xaml: 'XAML',
  csharp: 'C#',
  maui: 'Commands',
  node_fs: 'File System',
  node_http: 'HTTP',
  node_express: 'Express',
  node_db: 'Database',
  node_modules: 'Modules',
  node_utils: 'Utilities',
  node_async: 'Async',
  node_websocket: 'WebSocket',
  node_jwt: 'JWT Auth',
  browser: 'Browser',
};

// Map our shape names to Blockly's connection types
function getBlocklyConnectionType(shape: BlockShape): {
  prev: string | null;
  next: string | null;
  output?: string;
} {
  switch (shape) {
    case 'hat':
      return { prev: null, next: null }; // no top, has bottom (handled separately)
    case 'cap':
      return { prev: 'stack', next: null as string | null };
    case 'boolean':
      return { prev: null as string | null, next: null as string | null, output: 'Boolean' };
    case 'reporter':
      return { prev: null as string | null, next: null as string | null, output: undefined }; // any output
    case 'c-block':
      return { prev: 'stack', next: 'stack' };
    case 'stack':
    default:
      return { prev: 'stack', next: 'stack' };
  }
}

// Map our field types to Blockly field types
function buildBlocklyFields(config: any[], blockId: string): any {
  const args: any[] = [];
  if (!config) return args;

  for (const field of config) {
    const fieldId = field.id;

    switch (field.type) {
      case 'text':
        args.push({
          type: 'field_input',
          name: fieldId,
          text: String(field.default ?? ''),
        });
        break;
      case 'number':
        args.push({
          type: 'field_number',
          name: fieldId,
          value: Number(field.default ?? 0),
        });
        break;
      case 'select':
        args.push({
          type: 'field_dropdown',
          name: fieldId,
          options: (field.options || []).map((o: any) => [
            o.label,
            String(o.value),
          ]),
        });
        break;
      case 'variable':
        args.push({
          type: 'field_dropdown',
          name: fieldId,
          options: dynamicVariableOptions,
        });
        break;
      case 'element':
        args.push({
          type: 'field_dropdown',
          name: fieldId,
          options: dynamicElementOptions,
        });
        break;
      case 'boolean':
        args.push({
          type: 'field_checkbox',
          name: fieldId,
          checked: Boolean(field.default),
        });
        break;
      case 'color':
        args.push({
          type: 'field_colour',
          name: fieldId,
          colour: String(field.default ?? '#000000'),
        });
        break;
      case 'textarea':
      case 'code':
        args.push({
          type: 'field_input',
          name: fieldId,
          text: String(field.default ?? '').slice(0, 30),
        });
        break;
      default:
        args.push({
          type: 'field_input',
          name: fieldId,
          text: String(field.default ?? ''),
        });
    }
  }
  return args;
}

// Escape special regex characters in a string
function escapeRegex(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Register a single block with Blockly
function registerBlock(block: BlockDefinition, framework: string): void {
  const connType = getBlocklyConnectionType(block.shape);

  // Build all config fields into Blockly field descriptors
  const allFields: any[] = [];
  for (const field of block.config || []) {
    const built = buildBlocklyFields([field], block.id);
    allFields.push(built[0]);
  }

  // Build message: always append config fields and value inputs after the label
  // We NEVER try to replace words inline to avoid regex collision bugs.
  let message = block.label || '';
  const args0: any[] = [];

  // Append all config fields at the end of the message
  for (let i = 0; i < allFields.length; i++) {
    message += ` %${i + 1}`;
    args0.push(allFields[i]);
  }

  // Separate inputs into value inputs (inline) and statement inputs (separate messages)
  const valueInputs: any[] = [];
  const statementInputs: any[] = [];
  for (const input of block.inputs || []) {
    if (input.type === 'code') {
      statementInputs.push(input);
    } else {
      valueInputs.push(input);
    }
  }

  // Append value inputs inline to message0 (continue from config fields)
  let nextIdx = allFields.length + 1;
  for (const vi of valueInputs) {
    message += ` %${nextIdx}`;
    args0.push({
      type: 'input_value',
      name: vi.id,
      check: vi.type === 'boolean' ? 'Boolean' : null,
    });
    nextIdx++;
  }

  const blockJson: any = {
    type: `rb_${block.id}`,
    message0: message,
    args0: args0,
    colour: hexToBlocklyColor(block.color || CATEGORY_COLORS[block.category] || '#6366f1'),
    tooltip: block.description,
    helpUrl: '',
  };

  if (block.shape === 'hat') {
    // Hat blocks have a "next" connection (they start stacks) but no "previous"
    blockJson.nextStatement = 'stack';
  } else if (block.shape === 'c-block') {
    // C-block: has prev, next, and a statement input for child blocks
    blockJson.previousStatement = 'stack';
    blockJson.nextStatement = 'stack';
  } else if (block.shape === 'boolean' || block.shape === 'reporter') {
    // Value blocks have output
    blockJson.output = block.shape === 'boolean' ? 'Boolean' : null;
    // For reporter, allow any connection
    if (block.shape === 'reporter') {
      blockJson.output = null; // Blockly allows any type
    }
  } else {
    // stack or cap
    if (connType.prev) blockJson.previousStatement = connType.prev;
    if (connType.next) blockJson.nextStatement = connType.next;
  }

  // Add custom context menu for variable and function definition blocks
  if (block.id === 'js_var' || block.id === 'js_function') {
    blockJson.customContextMenu = function (options: any[]) {
      options.push({
        text: block.id.startsWith('js_') ? 'Edit...' : 'Edit variable...',
        enabled: true,
        callback: function () {
          if (!setEditModalData) return;
          const vals: Record<string, string> = {};
          (block.config || []).forEach((f: any) => {
            try { vals[f.id] = this.getFieldValue(f.id) || f.default || ''; } catch { vals[f.id] = f.default || ''; }
          });
          setEditModalData({ blockType: `rb_${block.id}`, block: this, values: vals });
        },
      });
    };
  }

  // Statement inputs (code type) get their own message blocks for C-shape rendering
  if (statementInputs.length > 0) {
    statementInputs.forEach((input, idx) => {
      blockJson[`message${idx + 1}`] = `${input.label || ''} %1`;
      blockJson[`args${idx + 1}`] = [{
        type: 'input_statement',
        name: input.id,
      }];
    });
  }

  try {
    Blockly.Blocks[`rb_${block.id}`] = {
      init: function () {
        this.jsonInit(blockJson);
      },
    };
  } catch (e) {
    // Block already registered - skip
  }

  // Register the code generator
  const blockDef = block;
  javascriptGenerator.forBlock[`rb_${block.id}`] = function (runtimeBlock: any) {
    const configValues: Record<string, any> = {};
    if (blockDef.config) {
      blockDef.config.forEach((field: any) => {
        try {
          const valueBlock = runtimeBlock.getFieldValue(field.id);
          configValues[field.id] = valueBlock;
        } catch (e) {
          configValues[field.id] = field.default;
        }
      });
    }

    // Get input values (from connected blocks)
    const inputValues: Record<string, string> = {};
    if (blockDef.inputs) {
      blockDef.inputs.forEach((input: any) => {
        try {
          const valueCode = javascriptGenerator.valueToCode(
            runtimeBlock,
            input.id,
            javascriptGenerator.ORDER_ATOMIC
          );
          inputValues[input.id] = valueCode || '';
        } catch (e) {
          inputValues[input.id] = '';
        }
      });
    }

    // Call our existing compile function
    const code = blockDef.compile ? blockDef.compile(configValues, inputValues) : '';
    return [code, javascriptGenerator.ORDER_ATOMIC];
  };
}

// Convert hex color to Blockly's expected color number (HSV hue)
function hexToBlocklyColor(hex: string): number {
  // Blockly uses hue values 0-360
  // We'll just return the hex as a string in modern Blockly
  return hex as any;
}

// Generate code from a single Blockly block by using our own compile functions
// This bypasses javascriptGenerator.workspaceToCode() to avoid compatibility issues
function generateCodeFromBlock(block: any, visited: Set<string>): string {
  const type: string = block.type || '';
  const id = type.startsWith('rb_') ? type.slice(3) : '';
  if (!id) return '';
  const def = getBlock(id);
  if (!def) return '';

  const fieldValues: Record<string, any> = {};
  if (def.config) {
    for (const field of def.config) {
      try {
        const val = block.getFieldValue(field.id);
        fieldValues[field.id] = val != null ? val : field.default;
      } catch {
        fieldValues[field.id] = field.default;
      }
    }
  }

  const inputValues: Record<string, string> = {};
  if (def.inputs) {
    for (const input of def.inputs) {
      try {
        const targetBlock = block.getInputTargetBlock(input.id);
        if (targetBlock && !visited.has(targetBlock.id)) {
          visited.add(targetBlock.id);
          if (input.type === 'code') {
            // Statement input: walk the connected block stack via nextConnection
            const stmts: string[] = [];
            let current: any = targetBlock;
            while (current) {
              stmts.push(generateCodeFromBlock(current, visited));
              current = current.getNextBlock ? current.getNextBlock() : null;
              if (current && visited.has(current.id)) break;
              if (current) visited.add(current.id);
            }
            inputValues[input.id] = stmts.join('\n');
          } else {
            // Value input: single expression block
            inputValues[input.id] = generateCodeFromBlock(targetBlock, visited);
          }
        } else {
          inputValues[input.id] = '';
        }
      } catch {
        inputValues[input.id] = '';
      }
    }
  }

  return def.compile ? def.compile(fieldValues, inputValues) : '';
}

function generateCodeFromWorkspace(ws: any): string {
  const topBlocks = ws.getTopBlocks(false);
  const visited = new Set<string>();
  const code: string[] = [];
  for (const block of topBlocks) {
    if (!visited.has(block.id)) {
      visited.add(block.id);
      code.push(generateCodeFromBlock(block, visited));
    }
  }
  return code.filter(Boolean).join('\n');
}

export default function BlockEditor() {
  const blocklyDiv = useRef<HTMLDivElement>(null);
  const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
  const { currentProject } = useProjectStore();
  const framework = currentProject?.framework || 'web';
  const activeFileId = useEditorStore((s) => s.activeFileId);
  const getBlocksXml = useEditorStore((s) => s.getBlocksXml);
  const setBlocksXml = useEditorStore((s) => s.setBlocksXml);
  const setBlockCode = useEditorStore((s) => s.setBlockCode);
  const saveGeneratedCodeToFile = useEditorStore((s) => s.saveGeneratedCodeToFile);
  const { emitBlockStateSync, emitBlockDragStart, emitBlockDragMove, emitBlockDragEnd, emitBlockConnect, emitBlockChange, emitBlockCreateEvent, emitPointerMove } = useProjectSocket(currentProject?.id);
  const remotePointers = useCollaborationStore((s) => s.remotePointers);
  const remoteBlockDrags = useCollaborationStore((s) => s.remoteBlockDrags);
  const pendingBlockConnects = useCollaborationStore((s) => s.pendingBlockConnects);
  const pendingBlockChanges = useCollaborationStore((s) => s.pendingBlockChanges);
  const pendingBlockCreates = useCollaborationStore((s) => s.pendingBlockCreates);
  const remoteUpdateRef = useRef(false);
  const isDraggingRef = useRef(false);
  const pendingSyncXmlRef = useRef<string | null>(null);
  const blocksCreatedDuringDragRef = useRef(false);

  // Modal state for editing variable/function blocks
  const [editingModal, setEditingModal] = useState<EditModalData | null>(null);
  const [editFormValues, setEditFormValues] = useState<Record<string, string>>({});

  // Expose setter to module-level variable for Blockly callbacks
  useEffect(() => {
    setEditModalData = (data) => {
      setEditingModal(data);
      if (data) {
        setEditFormValues({ ...data.values });
      }
    };
  }, []);

  // Initialize Blockly workspace
  useEffect(() => {
    if (!blocklyDiv.current) return;
    if (!activeFileId) return;

    // Inject Blockly CSS fixes for toolbox/flyout spacing
    injectBlocklyCSS();

    // Register all blocks for the current framework
    const blocks = getBlocksForFramework(framework as any);
    blocks.forEach((block) => registerBlock(block, framework));

    // Build the toolbox
    const blocksByCategory = new Map<string, BlockDefinition[]>();
    blocks.forEach((b) => {
      if (!blocksByCategory.has(b.category)) {
        blocksByCategory.set(b.category, []);
      }
      blocksByCategory.get(b.category)!.push(b);
    });

    const toolbox = buildToolbox(blocksByCategory);

    // Create the workspace
    const ws = Blockly.inject(blocklyDiv.current, {
      toolbox,
      grid: {
        spacing: 20,
        length: 3,
        colour: '#1e293b',
        snap: false,
      },
      move: {
        scrollbars: true,
        drag: true,
        wheel: true,
      },
      zoom: {
        controls: true,
        wheel: true,
        startScale: 0.9,
        maxScale: 2,
        minScale: 0.4,
        scaleSpeed: 1.1,
      },
      trashcan: true,

      renderer: 'zelos',
      theme: Blockly.Themes.Zelos,
      sounds: false,
    });

    workspaceRef.current = ws;

    // Register toolbar button callbacks for custom buttons in the toolbox
    ws.registerButtonCallback('ADD_VARIABLE', () => {
      const btn = document.querySelector('[data-rb-add-var]');
      if (btn) (btn as HTMLButtonElement).click();
    });
    ws.registerButtonCallback('ADD_FUNCTION', () => {
      const btn = document.querySelector('[data-rb-add-func]');
      if (btn) (btn as HTMLButtonElement).click();
    });

    // Hide flyout scrollbar when flyout closes, show when it opens
    ws.addChangeListener((e: any) => {
      if (e.type === 'flyout_open') {
        if (e.isOpen === false) {
          setTimeout(() => {
            Blockly.svgResize(ws);
            try { ws.scroll(0, 0); } catch {}
            // After resize/scroll, force-hide the flyout's scrollbar.
            // Blockly appends flyout scrollbar SVG elements directly to the main
            // workspace SVG (not inside the flyout's SVG group), so they remain
            // visible even after the flyout is hidden.
            const flyout = ws.getFlyout();
            if (flyout) {
              const flyoutWs = flyout.getWorkspace();
              if (flyoutWs && flyoutWs.scrollbar) {
                flyoutWs.scrollbar.setVisible(false);
              }
              // DOM fallback: directly hide any scrollbar handles on the left side
              // (flyout area) that Blockly's API may leave visible.
              const svgEl = ws.getParentSvg();
              if (svgEl) {
                const svgWidth = svgEl.clientWidth;
                svgEl.querySelectorAll('.blocklyScrollbarHandle').forEach((h: Element) => {
                  const scrollbarEl = h.closest('.blocklyScrollbar') as HTMLElement;
                  if (scrollbarEl && scrollbarEl.style.display !== 'none') {
                    const rect = scrollbarEl.getBoundingClientRect();
                    const svgRect = svgEl.getBoundingClientRect();
                    const relX = rect.left - svgRect.left;
                    if (relX < svgWidth * 0.4) {
                      scrollbarEl.style.display = 'none';
                    }
                  }
                });
              }
            }
          }, 200);
        }
      }
    });

    // Track block selection for collaboration
    ws.addChangeListener((e: any) => {
      if (e.type === Blockly.Events.SELECTED) {
        const wsHook = (window as any).__wsBlockSelection;
        if (wsHook) wsHook(e.newElementId || null);
      }
    });

    // Track pointer position in workspace coordinates (viewport → ws, delivers absolute coords)
    let pointerEmitTime = 0;
    const svg = ws.getParentSvg();
    if (svg) {
      const handleMouseMove = (e: MouseEvent) => {
        const now = Date.now();
        if (now - pointerEmitTime < 50) return;
        pointerEmitTime = now;
        const containerRect = blocklyDiv.current!.getBoundingClientRect();
        const relX = e.clientX - containerRect.left;
        const relY = e.clientY - containerRect.top;
        const wsCoord = {
          x: (relX - ws.scrollX) / ws.scale,
          y: (relY - ws.scrollY) / ws.scale,
        };
        emitPointerMove(wsCoord.x, wsCoord.y);
      };
      svg.addEventListener('mousemove', handleMouseMove);
      // Store for cleanup
      blocklyDiv.current!.dataset.pointerHandler = 'true';
      (blocklyDiv.current as any).__pointerHandler = handleMouseMove;
      (blocklyDiv.current as any).__pointerSvg = svg;
    }

    // Track block drag events + poll position during drag
    let draggedBlockId: string | null = null;
    let dragPoll: ReturnType<typeof setInterval> | null = null;
    let dragEndId = 0;

    ws.addChangeListener((e: any) => {
      if (e.type === Blockly.Events.BLOCK_DRAG) {
        const block = ws.getBlockById(e.blockId);
        if (block) {
          const xy = block.getRelativeToSurfaceXY();
          if (e.isStart) {
            dragEndId++;
            isDraggingRef.current = true;
            blocksCreatedDuringDragRef.current = false;
            draggedBlockId = e.blockId;
            emitBlockDragStart(e.blockId, xy.x, xy.y);
            // Poll block position during drag (BLOCK_MOVE may not fire)
            dragPoll = setInterval(() => {
              if (remoteUpdateRef.current || !isDraggingRef.current) {
                if (dragPoll) { clearInterval(dragPoll); dragPoll = null; }
                return;
              }
              const b = ws.getBlockById(draggedBlockId!);
              if (b) {
                const pos = b.getRelativeToSurfaceXY();
                emitBlockDragMove(draggedBlockId!, pos.x, pos.y);
              }
            }, 100);
          } else if (e.isEnd) {
            const endedBlockId = e.blockId;
            draggedBlockId = null;
            if (dragPoll) { clearInterval(dragPoll); dragPoll = null; }
            emitBlockDragEnd(endedBlockId);
            // Defer isDraggingRef=false + saveBlocks until after Blockly
            // applies the connection (which fires BLOCK_MOVE next)
            const thisId = ++dragEndId;
            requestAnimationFrame(() => {
              if (thisId !== dragEndId) return;
              isDraggingRef.current = false;
              saveBlocks();
              // Apply any deferred XML sync that arrived during drag
              const pendingXml = pendingSyncXmlRef.current;
              if (pendingXml) {
                pendingSyncXmlRef.current = null;
                const ws2 = workspaceRef.current;
                if (ws2) {
                  // If the user created any blocks locally during drag, skip the
                  // deferred XML entirely — local changes take priority over the
                  // collaborator's XML that was captured mid-drag.
                  if (blocksCreatedDuringDragRef.current) {
                    console.log('[WS Reload] drag-end SKIPPING deferred XML: user created blocks locally during drag');
                    return;
                  }
                  // Staleness check: if the workspace has >= blocks than the incoming
                  // XML, the XML is stale (user added/rearranged blocks during drag).
                  try {
                    const xmlEl = Blockly.utils.xml.textToDom(pendingXml);
                    let incomingTopBlockCount = 0;
                    if (xmlEl) {
                      for (const child of xmlEl.children) {
                        if (child.nodeName === 'block') incomingTopBlockCount++;
                      }
                    }
                    const currentTopBlockCount = ws2.getTopBlocks(false).length;
                    console.log('[WS Reload] drag-end: incoming top blocks:', incomingTopBlockCount, 'current workspace top blocks:', currentTopBlockCount);
                    if (currentTopBlockCount >= incomingTopBlockCount) {
                      console.log('[WS Reload] drag-end SKIPPING deferred XML: workspace has >= blocks than incoming XML (stale)');
                      return;
                    }
                  } catch (e) {
                    console.warn('[WS Reload] drag-end staleness check failed:', e);
                  }
                  console.log('[WS Reload] applying deferred XML after drag end');
                  remoteUpdateRef.current = true;
                  try {
                    ws2.clear();
                    const dom = Blockly.utils.xml.textToDom(pendingXml);
                    Blockly.Xml.domToWorkspace(dom, ws2);
                    try { (ws2 as any).clearSelection(); } catch {}
                    try { ws2.zoomToFit(); } catch {}
                  } catch (e) {
                    console.error('[WS Reload] FAILED to apply deferred XML:', e);
                  }
                  requestAnimationFrame(() => { setTimeout(() => { remoteUpdateRef.current = false; }, 0); });
                }
              }
            });
          }
        }
      }
      // When a block snaps to a parent during drag end, Connection.connect()
      // fires BLOCK_MOVE synchronously with newParentId/newInputName.
      // isDraggingRef.current ensures we only emit for user-initiated drags.
      if (e.type === Blockly.Events.BLOCK_MOVE && e.newParentId && isDraggingRef.current) {
        emitBlockConnect(e.blockId, e.newParentId, e.newInputName || undefined);
      }
      // Broadcast field value changes (dropdowns, text inputs, etc.)
      // Only emit for user-initiated changes, not during remote updates or workspace load.
      if (e.type === Blockly.Events.BLOCK_CHANGE && e.element === 'field' && !remoteUpdateRef.current) {
        emitBlockChange(e.blockId, e.name, e.newValue, e.element);
      }
      // Broadcast block creation (toolbox drag-out, paste, duplicate) so collaborator
      // screens can create the block immediately instead of waiting for XML sync.
      if (e.type === Blockly.Events.BLOCK_CREATE && !remoteUpdateRef.current) {
        if (isDraggingRef.current) {
          blocksCreatedDuringDragRef.current = true;
        }
        const block = ws.getBlockById(e.blockId);
        console.log('[BLOCK_CREATE] received:', { blockId: e.blockId, hasBlock: !!block, blockType: block?.type, isShadow: block?.isShadow?.(), hasParent: !!block?.getParent?.(), isDragging: isDraggingRef.current });
        if (block && !block.getParent()) {
          try {
            const dom = Blockly.Xml.blockToDom(block, { xy: true } as any);
            const xml = Blockly.Xml.domToText(dom);
            console.log('[BLOCK_CREATE] emitting block_create_event:', { blockId: e.blockId, activeFileId, xmlLen: xml.length });
            if (activeFileId) emitBlockCreateEvent(e.blockId, activeFileId, xml);
          } catch (err) {
            console.warn('[BLOCK_CREATE] failed to emit block_create_event:', err);
          }
        } else if (block && block.getParent()) {
          console.log('[BLOCK_CREATE] skipping: block has parent');
        }
      }
    });

    // Restore saved blocks for this specific file
    const savedXml = getBlocksXml(activeFileId);
    if (savedXml) {
      try {
        remoteUpdateRef.current = true;
        const dom = Blockly.utils.xml.textToDom(savedXml);
        Blockly.Xml.domToWorkspace(dom, ws);
        requestAnimationFrame(() => { setTimeout(() => { remoteUpdateRef.current = false; }, 0); });
      } catch (e) {
        requestAnimationFrame(() => { setTimeout(() => { remoteUpdateRef.current = false; }, 0); });
        // saved XML is invalid, ignore
      }
    }

    // Auto-save blocks on change - per-file
    const saveBlocks = () => {
      if (remoteUpdateRef.current || isDraggingRef.current) {
        console.log('[saveBlocks] SKIPPING (remote:', remoteUpdateRef.current, 'dragging:', isDraggingRef.current, ') topBlocks:', ws.getTopBlocks(false).length);
        return;
      }
      const dom = Blockly.Xml.workspaceToDom(ws);
      const xml = Blockly.Xml.domToText(dom);
      const currentFileId = useEditorStore.getState().activeFileId;
      if (!currentFileId) return;
      const savedXml = getBlocksXml(currentFileId);
      if (savedXml === xml) {
        console.log('[saveBlocks] SKIPPING: XML unchanged for file:', currentFileId);
        return;
      }
      console.log('[saveBlocks] SAVING blocks for file:', currentFileId, 'xml length:', xml.length, 'topBlocks:', ws.getTopBlocks(false).length, '(old:', savedXml?.length, ')');
      setBlocksXml(currentFileId, xml);
      emitBlockStateSync(currentFileId, xml);
      let finalCode = '';
      try {
        // Try manual code generation first (uses our compile functions directly)
        finalCode = generateCodeFromWorkspace(ws);
        // Fall back to Blockly's generator if manual output is empty but blocks exist
        if (!finalCode && ws.getTopBlocks(false).length > 0) {
          finalCode = javascriptGenerator.workspaceToCode(ws) || '';
        }
      } catch (e) {
        // code generation failed - file content stays empty so compileWeb defaults
      }
      setBlockCode(currentFileId, finalCode);
      saveGeneratedCodeToFile(currentFileId, finalCode);
    };
    ws.addChangeListener(saveBlocks);

    return () => {
      if (dragPoll) { clearInterval(dragPoll); dragPoll = null; }
      // Save current blocks for the file before workspace is disposed
      if (activeFileId) {
        try {
          const dom = Blockly.Xml.workspaceToDom(ws);
          const xml = Blockly.Xml.domToText(dom);
          setBlocksXml(activeFileId, xml);
        } catch (e) {}
      }
      // Cleanup pointer move listener
      const div = blocklyDiv.current;
      if (div && (div as any).__pointerHandler && (div as any).__pointerSvg) {
        (div as any).__pointerSvg.removeEventListener('mousemove', (div as any).__pointerHandler);
        delete (div as any).__pointerHandler;
        delete (div as any).__pointerSvg;
      }
      isDraggingRef.current = false;
      ws.removeChangeListener(saveBlocks);
      ws.dispose();
      workspaceRef.current = null;
    };
  }, [framework, activeFileId]);

  // Apply remote block state changes (skip if workspace already matches)
  const blocksXmlForFile = useEditorStore((s) => s.blocksXml[activeFileId || '']);
  useEffect(() => {
    if (!activeFileId || !workspaceRef.current || remoteUpdateRef.current) return;
    const ws = workspaceRef.current;
    if (!blocksXmlForFile) return;
    // If the workspace already matches this XML, skip reload (avoid disrupting drag/edit)
    try {
      const currentDom = Blockly.Xml.workspaceToDom(ws);
      const currentXml = Blockly.Xml.domToText(currentDom);
      if (currentXml === blocksXmlForFile) return;
    } catch {}
    // If the user is currently dragging, defer the reload to avoid disrupting the drag.
    // Store the pending XML so the drag-end handler can apply it.
    if (isDraggingRef.current) {
      console.log('[WS Reload] DEFERRING: user is dragging');
      pendingSyncXmlRef.current = blocksXmlForFile;
      return;
    }
    // Staleness check: if the current workspace has more top-level blocks than the
    // incoming XML, the XML is stale (blocks were added via individual events like
    // block_create_event before the XML sync arrived). Don't clear the workspace
    // to avoid losing blocks that aren't yet reflected in the XML.
    try {
      const xmlEl = Blockly.utils.xml.textToDom(blocksXmlForFile);
      let incomingTopBlockCount = 0;
      if (xmlEl) {
        for (const child of xmlEl.children) {
          if (child.nodeName === 'block') incomingTopBlockCount++;
        }
      }
      const currentTopBlockCount = ws.getTopBlocks(false).length;
      console.log('[WS Reload] incoming top blocks:', incomingTopBlockCount, 'current workspace top blocks:', currentTopBlockCount);
      if (currentTopBlockCount > incomingTopBlockCount) {
        console.log('[WS Reload] SKIPPING: workspace has more blocks than incoming XML (stale)');
        return;
      }
    } catch (e) {
      console.warn('[WS Reload] staleness check failed:', e);
    }
    console.log('[WS Reload] reloading workspace from XML (top blocks:', ws.getTopBlocks(false).length, ')');
    pendingSyncXmlRef.current = null;
    remoteUpdateRef.current = true;
    try {
      ws.clear();
      const dom = Blockly.utils.xml.textToDom(blocksXmlForFile);
      Blockly.Xml.domToWorkspace(dom, ws);
      try { (ws as any).clearSelection(); } catch {}
      try { ws.zoomToFit(); } catch {}
    } catch (e) {
      console.error('[WS Reload] FAILED to load blocks from XML:', e);
    }
    // Blockly may fire events asynchronously (microtasks) after domToWorkspace.
    // Defer resetting the flag so those events see remoteUpdateRef=true.
    requestAnimationFrame(() => { setTimeout(() => { remoteUpdateRef.current = false; }, 0); });
  }, [activeFileId, blocksXmlForFile]);

  // Move remote collaborator blocks to absolute positions during drag
  useEffect(() => {
    const ws = workspaceRef.current;
    if (!ws || !activeFileId || remoteUpdateRef.current) return;
    remoteUpdateRef.current = true;
    for (const [userId, drag] of Object.entries(remoteBlockDrags)) {
      const block = ws.getBlockById(drag.blockId);
      if (!block) continue;
      // Remote user already unplugged this block from its parent; match on our end
      if (block.getParent()) {
        try { block.unplug(false); } catch {}
      }
      const currentPos = block.getRelativeToSurfaceXY();
      const dx = drag.x - currentPos.x;
      const dy = drag.y - currentPos.y;
      if (dx !== 0 || dy !== 0) {
        block.moveBy(dx, dy);
      }
    }
    remoteUpdateRef.current = false;
  }, [remoteBlockDrags, activeFileId]);

  // Connect blocks on receiver when remote user snaps a block into a parent
  useEffect(() => {
    if (!pendingBlockConnects.length) return;
    remoteUpdateRef.current = true;
    const ws = workspaceRef.current;
    if (ws) {
      const unprocessed: Array<{ childId: string; parentId: string; inputName?: string }> = [];
      for (const connect of pendingBlockConnects) {
        const child = ws.getBlockById(connect.childId);
        const parent = ws.getBlockById(connect.parentId);
        if (!child || !parent) {
          // Blocks not yet loaded (connect arrived before XML sync); keep in queue
          unprocessed.push(connect);
          continue;
        }
        if (connect.inputName) {
          // Statement or value connection: child connects to a named input on the parent
          const input = parent.getInput(connect.inputName);
          if (input?.connection && !input.connection.isConnected()) {
            try {
              if (child.previousConnection && !child.previousConnection.isConnected()) {
                input.connection.connect(child.previousConnection);
              } else if (child.outputConnection && !child.outputConnection.isConnected()) {
                input.connection.connect(child.outputConnection);
              }
              try { (parent as any).render(); } catch {}
              try { (child as any).render(); } catch {}
            } catch {}
          }
        } else {
          // Stack connection: child below parent (child.previous → parent.next)
          if (child.previousConnection && parent.nextConnection &&
              !child.previousConnection.isConnected() && !parent.nextConnection.isConnected()) {
            try {
              parent.nextConnection.connect(child.previousConnection);
              try { (parent as any).render(); } catch {}
              try { (child as any).render(); } catch {}
            } catch {}
          }
        }
      }
      // Only update store when items were actually consumed (blocks existed)
      if (unprocessed.length < pendingBlockConnects.length) {
        useCollaborationStore.setState({ pendingBlockConnects: unprocessed });
      }
      ws.render();
    }
    remoteUpdateRef.current = false;
  }, [pendingBlockConnects, blocksXmlForFile]);

  // Apply field value changes from remote collaborators (dropdowns, text fields, etc.)
  useEffect(() => {
    if (!pendingBlockChanges.length) return;
    remoteUpdateRef.current = true;
    const ws = workspaceRef.current;
    if (ws) {
      let change;
      while ((change = useCollaborationStore.getState().shiftPendingBlockChange())) {
        const block = ws.getBlockById(change.blockId);
        if (!block) continue;
        try {
          block.setFieldValue(change.value, change.name);
          try { (block as any).render(); } catch {}
        } catch {}
      }
    }
    remoteUpdateRef.current = false;
  }, [pendingBlockChanges]);

  // Create blocks on receiver when remote user drags a new block from the toolbox
  useEffect(() => {
    if (!pendingBlockCreates.length) {
      console.log('[PendingCreate] no pending creates, skipping');
      return;
    }
    console.log('[PendingCreate] processing', pendingBlockCreates.length, 'pending creates');
    remoteUpdateRef.current = true;
    const ws = workspaceRef.current;
    if (ws && activeFileId) {
      let created = false;
      while (useCollaborationStore.getState().pendingBlockCreates.length > 0) {
        const create = useCollaborationStore.getState().shiftPendingBlockCreate();
        if (!create) continue;
        // Skip if block already exists (e.g. already created by XML sync)
        if (ws.getBlockById(create.blockId)) {
          console.log('[PendingCreate] block already exists:', create.blockId);
          continue;
        }
        try {
          console.log('[PendingCreate] creating block:', create.blockId);
          const dom = Blockly.utils.xml.textToDom(create.xml);
          Blockly.Xml.domToWorkspace(dom, ws);
          created = true;
          console.log('[PendingCreate] block created successfully:', create.blockId);
        } catch (e) {
          console.warn('[PendingCreate] failed to create block:', create.blockId, e);
        }
      }
      // Update blocksXml in the store so the blocksXmlForFile comparison
      // in the workspace-reload effect matches the current workspace,
      // preventing a destructive clear+reload that could conflict.
      if (created) {
        const dom = Blockly.Xml.workspaceToDom(ws);
        const xml = Blockly.Xml.domToText(dom);
        console.log('[PendingCreate] updating blocksXml after creating blocks');
        useEditorStore.getState().setBlocksXml(activeFileId, xml);
      }
      useCollaborationStore.setState({ pendingBlockCreates: [] });
    }
    requestAnimationFrame(() => { setTimeout(() => { remoteUpdateRef.current = false; }, 0); });
  }, [pendingBlockCreates, blocksXmlForFile]);

  // Collaborator block selection overlay
  const collaborators = useCollaborationStore((s) => s.collaborators);
  useEffect(() => {
    const ws = workspaceRef.current;
    if (!ws) return;
    try {
      ws.getAllBlocks(false).forEach((block: any) => block.setHighlighted(false));
      collaborators.forEach((c) => {
        if (c.selectedBlockId && c.selectedBlockId !== '') {
          const block = ws.getBlockById(c.selectedBlockId);
          if (block) block.setHighlighted(true);
        }
      });
    } catch {} // workspace may be disposed
  }, [collaborators, blocksXmlForFile]);

  const fitView = useCallback(() => {
    if (workspaceRef.current) {
      Blockly.svgResize(workspaceRef.current);
      workspaceRef.current.zoomToFit();
    }
  }, []);

  const clearAll = useCallback(() => {
    if (workspaceRef.current) {
      workspaceRef.current.clear();
    }
  }, []);

  // Save edit modal and update block
  const saveEditModal = useCallback(() => {
    if (!editingModal) return;
    const { block } = editingModal;
    try {
      Object.entries(editFormValues).forEach(([key, value]) => {
        block.setFieldValue(value, key);
      });
    } catch {}
    setEditingModal(null);
  }, [editingModal, editFormValues]);

  // Add a new variable stack to the workspace
  const addNewVariable = useCallback((name: string, type: string) => {
    const ws = workspaceRef.current;
    if (!ws) return;
    Blockly.Xml.domToWorkspace(
      Blockly.utils.xml.textToDom(
        `<xml><block type="rb_js_var"><field name="name">${name}</field><field name="varType">${type}</field></block></xml>`
      ),
      ws
    );
  }, []);

  const addNewFunction = useCallback((name: string, params: string, asyncFn: boolean) => {
    const ws = workspaceRef.current;
    if (!ws) return;
    Blockly.Xml.domToWorkspace(
      Blockly.utils.xml.textToDom(
        `<xml><block type="rb_js_function"><field name="name">${name}</field><field name="params">${params}</field><field name="asyncFn">${asyncFn ? 'TRUE' : 'FALSE'}</field></block></xml>`
      ),
      ws
    );
  }, []);

  const openNewVariableModal = useCallback(() => {
    setEditingModal({ blockType: 'new_variable', block: null, values: { name: '', varType: 'any' } });
    setEditFormValues({ name: '', varType: 'any' });
  }, []);

  const openNewFunctionModal = useCallback(() => {
    setEditingModal({ blockType: 'new_function', block: null, values: { name: '', params: '', asyncFn: 'FALSE' } });
    setEditFormValues({ name: '', params: '', asyncFn: 'FALSE' });
  }, []);

  const saveNewItemFromModal = useCallback(() => {
    if (!editingModal) return;
    if (editingModal.blockType === 'new_variable') {
      const name = editFormValues.name?.trim() || 'myVar';
      const varType = editFormValues.varType || 'any';
      addNewVariable(name, varType);
    } else if (editingModal.blockType === 'new_function') {
      const name = editFormValues.name?.trim() || 'myFunction';
      const params = editFormValues.params?.trim() || '';
      const asyncFn = editFormValues.asyncFn === 'TRUE';
      addNewFunction(name, params, asyncFn);
    }
    setEditingModal(null);
  }, [editingModal, editFormValues, addNewVariable, addNewFunction]);

  const isNewItem = editingModal?.blockType === 'new_variable' || editingModal?.blockType === 'new_function';

  return (
    <div className="flex flex-col h-full bg-[#1a2e2e]">
      {/* Toolbar */}
      <div className="flex items-center gap-2 px-4 py-2 bg-surface-900 border-b border-surface-700">
        <div className="flex-1 text-xs text-surface-400">
          Draggable block editor
        </div>
        <button
          onClick={openNewVariableModal}
          className="hidden"
          data-rb-add-var
        />
        <button
          onClick={openNewFunctionModal}
          className="hidden"
          data-rb-add-func
        />
        <button
          onClick={fitView}
          className="btn-ghost p-1.5"
          title="Fit View"
        >
          <Maximize2 className="w-4 h-4" />
        </button>
        <button
          onClick={clearAll}
          className="btn-ghost p-1.5"
          title="Clear All"
        >
          <Trash2 className="w-4 h-4" />
        </button>
      </div>

      {/* Blockly workspace */}
      <div
        ref={blocklyDiv}
        className="flex-1 w-full relative"
        style={{ minHeight: '400px' }}
      >
        {/* Remote collaborator pointers (workspace coords → viewport px) */}
        {Object.entries(remotePointers).map(([userId, pointer]) => {
          const w = workspaceRef.current;
          const cr = blocklyDiv.current?.getBoundingClientRect();
          if (!w || !cr) return null;
          const vx = cr.left + (pointer.x + w.scrollX) * w.scale;
          const vy = cr.top + (pointer.y + w.scrollY) * w.scale;
          // Hide pointer if outside the blockly canvas bounds
          if (vx < cr.left || vx > cr.right || vy < cr.top || vy > cr.bottom) return null;
          return (
            <div
              key={userId}
              className="fixed pointer-events-none z-40"
              style={{
                left: vx,
                top: vy,
                transform: 'translate(-50%, -50%)',
              }}
            >
              <div
                className="flex flex-col items-center"
                style={{ color: getColor(userId) }}
              >
                <MousePointer2 className="w-6 h-6 drop-shadow-lg" />
                <span className="text-[9px] font-medium bg-surface-900 px-1 rounded">
                  {pointer.username}
                </span>
              </div>
            </div>
          );
        })}

        {/* Remote collaborator block drags (ghost blocks, workspace coords → viewport) */}
        {Object.entries(remoteBlockDrags).map(([userId, drag]) => {
          const w = workspaceRef.current;
          let vx = drag.x, vy = drag.y;
          if (w) {
            const cr = blocklyDiv.current?.getBoundingClientRect();
            if (cr) {
              vx = cr.left + (drag.x + w.scrollX) * w.scale;
              vy = cr.top + (drag.y + w.scrollY) * w.scale;
            }
          }
          const collab = collaborators.find(c => c.userId === userId);
          const label = collab ? `${collab.username} dragging` : 'Dragging...';
          return (
            <div
              key={userId}
              className="fixed pointer-events-none z-40 opacity-60"
              style={{
                left: vx,
                top: vy,
                transform: 'translate(-50%, -50%)',
                color: getColor(userId),
              }}
            >
              <div className="flex items-center gap-1 bg-surface-900/80 px-2 py-1 rounded border"
                   style={{ borderColor: getColor(userId) }}>
                <MousePointer2 className="w-3 h-3" />
                <span className="text-[10px] whitespace-nowrap">{label}</span>
              </div>
            </div>
          );
        })}
      </div>

      {/* Edit modal for variable/function blocks */}
      {editingModal && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
             onClick={() => setEditingModal(null)}>
          <div className="bg-surface-900 border border-surface-700 rounded-lg shadow-xl p-5 w-full max-w-sm"
               onClick={(e) => e.stopPropagation()}>
            <h3 className="text-sm font-semibold text-surface-200 mb-4">
              {editingModal.blockType === 'new_variable' ? 'New Variable' :
               editingModal.blockType === 'new_function' ? 'New Function' :
               editingModal.blockType === 'rb_js_var' ? 'Edit Variable' :
               editingModal.blockType === 'rb_js_function' ? 'Edit Function' : 'Edit Block'}
            </h3>

            <div className="space-y-3">
              <div>
                <label className="block text-xs text-surface-400 mb-1">Name</label>
                <input
                  type="text"
                  className="w-full bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-sm text-surface-200"
                  value={editFormValues.name ?? ''}
                  onChange={(e) => setEditFormValues(v => ({ ...v, name: e.target.value }))}
                  placeholder="myVar"
                />
              </div>

              {(editingModal.blockType === 'new_variable' || editingModal.blockType === 'rb_js_var') && (
                <div>
                  <label className="block text-xs text-surface-400 mb-1">Type</label>
                  <select
                    className="w-full bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-sm text-surface-200"
                    value={editFormValues.varType ?? 'any'}
                    onChange={(e) => setEditFormValues(v => ({ ...v, varType: e.target.value }))}
                  >
                    {VAR_TYPES.map(t => (
                      <option key={t.value} value={t.value}>{t.label}</option>
                    ))}
                  </select>
                </div>
              )}

              {(editingModal.blockType === 'new_function' || editingModal.blockType === 'rb_js_function') && (
                <div>
                  <label className="block text-xs text-surface-400 mb-1">Parameters (comma-separated)</label>
                  <input
                    type="text"
                    className="w-full bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-sm text-surface-200"
                    value={editFormValues.params ?? ''}
                    onChange={(e) => setEditFormValues(v => ({ ...v, params: e.target.value }))}
                    placeholder="a, b"
                  />
                </div>
              )}

              {(editingModal.blockType === 'new_function' || editingModal.blockType === 'rb_js_function') && (
                <div className="flex items-center gap-2">
                  <input
                    type="checkbox"
                    id="asyncFn"
                    className="rounded"
                    checked={editFormValues.asyncFn === 'TRUE' || editFormValues.asyncFn === 'true'}
                    onChange={(e) => setEditFormValues(v => ({ ...v, asyncFn: e.target.checked ? 'TRUE' : 'FALSE' }))}
                  />
                  <label htmlFor="asyncFn" className="text-xs text-surface-400">Async function (use await inside)</label>
                </div>
              )}
            </div>

            <div className="flex justify-end gap-2 mt-5">
              <button
                className="px-3 py-1.5 text-xs text-surface-400 bg-surface-800 rounded hover:bg-surface-700"
                onClick={() => setEditingModal(null)}
              >
                Cancel
              </button>
              <button
                className="px-3 py-1.5 text-xs text-white bg-indigo-600 rounded hover:bg-indigo-500"
                onClick={isNewItem ? saveNewItemFromModal : saveEditModal}
              >
                {isNewItem ? 'Create' : 'Save'}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// Build the Blockly toolbox from our category structure
function buildToolbox(blocksByCategory: Map<string, BlockDefinition[]>): Blockly.utils.toolbox.ToolboxDefinition {
  const categories: any[] = [];

  // Sort categories: events first, then others
  const sortedCategories = Array.from(blocksByCategory.keys()).sort((a, b) => {
    if (a === 'events') return -1;
    if (b === 'events') return 1;
    return a.localeCompare(b);
  });

  for (const cat of sortedCategories) {
    const blocks = blocksByCategory.get(cat) || [];
    const color = CATEGORY_COLORS[cat] || '#6366f1';
    const icon = CATEGORY_ICONS[cat] || '■';
    const label = CATEGORY_LABELS[cat] || cat;

    const contents: any[] = [];

    // Add custom buttons in variable/function categories
    if (cat === 'variable') {
      contents.push({ kind: 'button', text: '+ New Variable', callbackKey: 'ADD_VARIABLE' });
    }
    if (cat === 'function') {
      contents.push({ kind: 'button', text: '+ New Function', callbackKey: 'ADD_FUNCTION' });
    }

    contents.push(...blocks.map((b) => ({
      kind: 'block',
      type: `rb_${b.id}`,
      ...(b.config ? {
        fields: Object.fromEntries(
          b.config.filter((f) => f.type !== 'variable' && f.type !== 'element').map((f) => [f.id, f.default ?? ''])
        ),
      } : {}),
    })));

    categories.push({
      kind: 'category',
      name: label,
      colour: color,
      icon: icon,
      contents,
    });
  }

  return {
    kind: 'categoryToolbox',
    contents: categories,
  };
}
