// Shared WebSocket for ESPNow messages (presets + selects)
let espnowSocket = null;
let espnowSocketReady = false;
let espnowPendingMessages = [];

const getEspnowSocket = () => {
  if (espnowSocket && (espnowSocket.readyState === WebSocket.OPEN || espnowSocket.readyState === WebSocket.CONNECTING)) {
    return espnowSocket;
  }

  const wsUrl = `ws://${window.location.host}/ws`;
  espnowSocket = new WebSocket(wsUrl);
  espnowSocketReady = false;

  espnowSocket.onopen = () => {
    espnowSocketReady = true;
    // Flush any queued messages
    espnowPendingMessages.forEach((msg) => {
      try {
        espnowSocket.send(msg);
      } catch (err) {
        console.error('Failed to send queued ESPNow message:', err);
      }
    });
    espnowPendingMessages = [];
  };

  espnowSocket.onclose = () => {
    espnowSocketReady = false;
    espnowSocket = null;
  };

  espnowSocket.onerror = (err) => {
    console.error('ESPNow WebSocket error:', err);
  };

  return espnowSocket;
};

const sendEspnowMessage = (obj) => {
  const json = JSON.stringify(obj);
  const ws = getEspnowSocket();
  if (espnowSocketReady && ws.readyState === WebSocket.OPEN) {
    try {
      ws.send(json);
    } catch (err) {
      console.error('Failed to send ESPNow message:', err);
    }
  } else {
    // Queue until connection is open
    espnowPendingMessages.push(json);
  }
};

// Send a select message for a preset to all device names in the current tab.
const sendSelectForCurrentTabDevices = (presetName, sectionEl) => {
  const section = sectionEl || document.querySelector('.presets-section[data-tab-id]');
  if (!section || !presetName) {
    return;
  }
  const namesAttr = section.getAttribute('data-device-names');
  const deviceNames = namesAttr
    ? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0)
    : [];

  if (!deviceNames.length) {
    return;
  }

  const select = {};
  deviceNames.forEach((name) => {
    select[name] = [presetName];
  });

  const message = {
    v: '1',
    select,
  };

  sendEspnowMessage(message);
};

document.addEventListener('DOMContentLoaded', () => {
  const presetsButton = document.getElementById('presets-btn');
  const presetsModal = document.getElementById('presets-modal');
  const presetsCloseButton = document.getElementById('presets-close-btn');
  const presetsList = document.getElementById('presets-list');
  const presetsAddButton = document.getElementById('preset-add-btn');
  const presetEditorModal = document.getElementById('preset-editor-modal');
  const presetEditorCloseButton = document.getElementById('preset-editor-close-btn');
  const presetNameInput = document.getElementById('preset-name-input');
  const presetPatternInput = document.getElementById('preset-pattern-input');
  const presetColorsContainer = document.getElementById('preset-colors-container');
  const presetNewColorInput = document.getElementById('preset-new-color');
  const presetAddColorButton = document.getElementById('preset-add-color-btn');
  const presetBrightnessInput = document.getElementById('preset-brightness-input');
  const presetDelayInput = document.getElementById('preset-delay-input');
  const presetSaveButton = document.getElementById('preset-save-btn');
  const presetClearButton = document.getElementById('preset-clear-btn');
  const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');
  const presetRemoveFromTabButton = document.getElementById('preset-remove-from-tab-btn');

  if (!presetsButton || !presetsModal || !presetsList || !presetSaveButton || !presetClearButton) {
    return;
  }

  let currentEditId = null;
  let currentEditTabId = null;
  let cachedPresets = {};
  let cachedPatterns = {};
  let currentPresetColors = []; // Track colors for the current preset

  // Function to get max colors for current pattern
  const getMaxColors = () => {
    if (!presetPatternInput || !presetPatternInput.value) {
      return Infinity; // No pattern selected, no limit
    }
    const patternName = presetPatternInput.value.trim();
    const patternConfig = cachedPatterns && cachedPatterns[patternName];
    if (patternConfig && typeof patternConfig === 'object' && patternConfig.max_colors !== undefined) {
      return patternConfig.max_colors;
    }
    return Infinity; // No limit if not specified
  };

  // Function to show/hide color section based on max_colors
  const updateColorSectionVisibility = () => {
    const maxColors = getMaxColors();
    const shouldShow = maxColors > 0;
    
    // Find the color label (the label before the container)
    if (presetColorsContainer) {
      let prev = presetColorsContainer.previousElementSibling;
      while (prev) {
        if (prev.tagName === 'LABEL' && prev.textContent.trim().toLowerCase().includes('color')) {
          prev.style.display = shouldShow ? '' : 'none';
          break;
        }
        prev = prev.previousElementSibling;
      }
      
      // Hide/show the container
      presetColorsContainer.style.display = shouldShow ? '' : 'none';
      
      // Hide/show the actions (color picker and buttons)
      const colorActions = presetColorsContainer.nextElementSibling;
      if (colorActions && (colorActions.querySelector('#preset-add-color-btn') || colorActions.querySelector('#preset-new-color'))) {
        colorActions.style.display = shouldShow ? '' : 'none';
      }
    }
  };

  const getNumberInput = (id) => {
    const input = document.getElementById(id);
    if (!input) {
      return 0;
    }
    return parseInt(input.value, 10) || 0;
  };

  const renderPresetColors = (colors) => {
    if (!presetColorsContainer) return;
    
    presetColorsContainer.innerHTML = '';
    currentPresetColors = colors || [];
    
    // Get max colors for current pattern
    const maxColors = getMaxColors();
    const maxColorsText = maxColors !== Infinity ? ` (max ${maxColors})` : '';
    
    if (currentPresetColors.length === 0) {
      const empty = document.createElement('p');
      empty.className = 'muted-text';
      empty.textContent = `No colors added. Click "Add Color" to add colors.${maxColorsText}`;
      presetColorsContainer.appendChild(empty);
      return;
    }
    
    // Show max colors info if limit exists and reached
    if (maxColors !== Infinity && currentPresetColors.length >= maxColors) {
      const info = document.createElement('p');
      info.className = 'muted-text';
      info.style.cssText = 'font-size: 0.85em; margin-bottom: 0.5rem; color: #ffa500;';
      info.textContent = `Maximum ${maxColors} color${maxColors !== 1 ? 's' : ''} reached for this pattern.`;
      presetColorsContainer.appendChild(info);
    }
    
    const swatchContainer = document.createElement('div');
    swatchContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 0.5rem;';
    swatchContainer.classList.add('color-swatches-container');
    
    currentPresetColors.forEach((color, index) => {
      const swatchWrapper = document.createElement('div');
      swatchWrapper.style.cssText = 'position: relative; display: inline-block;';
      swatchWrapper.draggable = true;
      swatchWrapper.dataset.colorIndex = index;
      swatchWrapper.classList.add('draggable-color-swatch');
      
      const swatch = document.createElement('div');
      swatch.style.cssText = `
        width: 64px;
        height: 64px;
        border-radius: 8px;
        background-color: ${color};
        border: 2px solid #4a4a4a;
        cursor: move;
        box-shadow: 0 2px 4px rgba(0,0,0,0.3);
        transition: opacity 0.2s, transform 0.2s;
      `;
      swatch.title = `${color} - Drag to reorder`;
      
      // Color picker overlay
      const colorPicker = document.createElement('input');
      colorPicker.type = 'color';
      colorPicker.value = color;
      colorPicker.style.cssText = `
        position: absolute;
        top: 0;
        left: 0;
        width: 64px;
        height: 64px;
        opacity: 0;
        cursor: pointer;
        z-index: 5;
      `;
      colorPicker.addEventListener('change', (e) => {
        currentPresetColors[index] = e.target.value;
        renderPresetColors(currentPresetColors);
      });
      // Prevent color picker from interfering with drag
      colorPicker.addEventListener('mousedown', (e) => {
        e.stopPropagation();
      });
      
      // Remove button
      const removeBtn = document.createElement('button');
      removeBtn.textContent = '×';
      removeBtn.style.cssText = `
        position: absolute;
        top: -8px;
        right: -8px;
        width: 24px;
        height: 24px;
        border-radius: 50%;
        background-color: #ff4444;
        color: white;
        border: none;
        cursor: pointer;
        font-size: 18px;
        line-height: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 10;
        padding: 0;
      `;
      removeBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        currentPresetColors.splice(index, 1);
        renderPresetColors(currentPresetColors);
      });
      // Prevent remove button from interfering with drag
      removeBtn.addEventListener('mousedown', (e) => {
        e.stopPropagation();
      });
      
      // Drag event handlers for reordering
      swatchWrapper.addEventListener('dragstart', (e) => {
        swatchWrapper.classList.add('dragging-color');
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/plain', index.toString());
        swatch.style.opacity = '0.5';
      });
      
      swatchWrapper.addEventListener('dragend', (e) => {
        swatchWrapper.classList.remove('dragging-color');
        swatch.style.opacity = '1';
        // Remove drag-over classes from siblings
        document.querySelectorAll('.draggable-color-swatch').forEach(el => {
          el.classList.remove('drag-over-color');
        });
      });
      
      swatchWrapper.appendChild(swatch);
      swatchWrapper.appendChild(colorPicker);
      swatchWrapper.appendChild(removeBtn);
      swatchContainer.appendChild(swatchWrapper);
    });
    
    // Add drag and drop handlers to the container
    swatchContainer.addEventListener('dragover', (e) => {
      e.preventDefault();
      const dragging = swatchContainer.querySelector('.dragging-color');
      if (!dragging) return;
      
      const afterElement = getDragAfterElementForColors(swatchContainer, e.clientX);
      if (afterElement == null) {
        swatchContainer.appendChild(dragging);
      } else {
        swatchContainer.insertBefore(dragging, afterElement);
      }
    });
    
    swatchContainer.addEventListener('drop', (e) => {
      e.preventDefault();
      const dragging = swatchContainer.querySelector('.dragging-color');
      if (!dragging) return;
      
      // Get new order of colors from DOM
      const colorElements = [...swatchContainer.querySelectorAll('.draggable-color-swatch')];
      const newColorOrder = colorElements.map(el => {
        const colorPicker = el.querySelector('input[type="color"]');
        return colorPicker ? colorPicker.value : null;
      }).filter(color => color !== null);
      
      // Update current colors array
      currentPresetColors = newColorOrder;
      
      // Re-render to update indices
      renderPresetColors(currentPresetColors);
    });
    
    presetColorsContainer.appendChild(swatchContainer);
  };
  
  // Function to get drag after element for colors (horizontal layout)
  const getDragAfterElementForColors = (container, x) => {
    const draggableElements = [...container.querySelectorAll('.draggable-color-swatch:not(.dragging-color)')];
    
    return draggableElements.reduce((closest, child) => {
      const box = child.getBoundingClientRect();
      const offset = x - box.left - box.width / 2;
      
      if (offset < 0 && offset > closest.offset) {
        return { offset: offset, element: child };
      } else {
        return closest;
      }
    }, { offset: Number.NEGATIVE_INFINITY }).element;
  };

  const setFormValues = (preset) => {
    if (!presetNameInput || !presetPatternInput || !presetBrightnessInput || !presetDelayInput) {
      return;
    }
    presetNameInput.value = preset.name || '';
    const patternName = preset.pattern || '';
    presetPatternInput.value = patternName;
    const colors = Array.isArray(preset.colors) ? preset.colors : [];
    renderPresetColors(colors);
    presetBrightnessInput.value = preset.brightness || 0;
    presetDelayInput.value = preset.delay || 0;
    
    // Update color section visibility based on pattern
    updateColorSectionVisibility();
    
    // Make name and pattern read-only when editing (not when creating new)
    const isEditing = currentEditId !== null;
    presetNameInput.disabled = isEditing;
    presetPatternInput.disabled = isEditing;
    if (isEditing) {
      presetNameInput.style.backgroundColor = '#2a2a2a';
      presetNameInput.style.cursor = 'not-allowed';
      presetPatternInput.style.backgroundColor = '#2a2a2a';
      presetPatternInput.style.cursor = 'not-allowed';
    } else {
      presetNameInput.style.backgroundColor = '';
      presetNameInput.style.cursor = '';
      presetPatternInput.style.backgroundColor = '';
      presetPatternInput.style.cursor = '';
    }
    
    // Update labels and visibility based on pattern
    updatePresetNLabels(patternName);
    
    // Get pattern config to map descriptive names back to n keys
    const patternConfig = cachedPatterns && cachedPatterns[patternName];
    const nToLabel = {};
    if (patternConfig && typeof patternConfig === 'object') {
      // Now n keys are keys, labels are values
      Object.entries(patternConfig).forEach(([nKey, label]) => {
        if (typeof nKey === 'string' && nKey.startsWith('n') && typeof label === 'string') {
          nToLabel[nKey] = label;
        }
      });
    }
    
    // Set n values, checking both n keys and descriptive names
    for (let i = 1; i <= 8; i++) {
      const nKey = `n${i}`;
      const inputEl = document.getElementById(`preset-${nKey}-input`);
      if (inputEl) {
        // First check if preset has n key directly
        if (preset[nKey] !== undefined) {
          inputEl.value = preset[nKey] || 0;
        } else {
          // Check if preset has descriptive name (from pattern.json mapping)
          const label = nToLabel[nKey];
          if (label && preset[label] !== undefined) {
            inputEl.value = preset[label] || 0;
          } else {
            inputEl.value = 0;
          }
        }
      }
    }
  };

  const clearForm = () => {
    currentEditId = null;
    currentEditTabId = null;
    currentPresetColors = [];
    setFormValues({
      name: '',
      pattern: '',
      colors: [],
      brightness: 0,
      delay: 0,
      n1: 0,
      n2: 0,
      n3: 0,
      n4: 0,
      n5: 0,
      n6: 0,
      n7: 0,
      n8: 0,
    });
    // Re-enable name and pattern when clearing (for new preset)
    if (presetNameInput) {
      presetNameInput.disabled = false;
      presetNameInput.style.backgroundColor = '';
      presetNameInput.style.cursor = '';
    }
    if (presetPatternInput) {
      presetPatternInput.disabled = false;
      presetPatternInput.style.backgroundColor = '';
      presetPatternInput.style.cursor = '';
    }
  };

  const openEditor = () => {
    if (presetEditorModal) {
      presetEditorModal.classList.add('active');
    }
    loadPatterns().then(() => {
      updatePresetNLabels(presetPatternInput ? presetPatternInput.value : '');
      updateColorSectionVisibility();
    });
  };

  const closeEditor = () => {
    if (presetEditorModal) {
      presetEditorModal.classList.remove('active');
    }
  };

  const buildPresetPayload = () => {
    const payload = {
      name: presetNameInput ? presetNameInput.value.trim() : '',
      pattern: presetPatternInput ? presetPatternInput.value.trim() : '',
      colors: currentPresetColors || [],
      // Use canonical field names expected by the device / API
      brightness: presetBrightnessInput ? parseInt(presetBrightnessInput.value, 10) || 0 : 0,
      delay: presetDelayInput ? parseInt(presetDelayInput.value, 10) || 0 : 0,
    };

    // Always store numeric parameters as n1..n8.
    for (let i = 1; i <= 8; i++) {
      const nKey = `n${i}`;
      payload[nKey] = getNumberInput(`preset-${nKey}-input`);
    }

    return payload;
  };

  const loadPatterns = async () => {
    if (!presetPatternInput) {
      return;
    }
    try {
      // Load pattern definitions from pattern.json
      const response = await fetch('/patterns/definitions', {
        headers: { Accept: 'application/json' },
      });
      if (!response.ok) {
        return;
      }
      const patterns = await response.json();
      cachedPatterns = patterns || {};
      const entries = Object.keys(cachedPatterns);
      const desiredPattern = presetPatternInput.value;
      presetPatternInput.innerHTML = '<option value="">Pattern</option>';
      entries.forEach((patternName) => {
        const option = document.createElement('option');
        option.value = patternName;
        option.textContent = patternName;
        presetPatternInput.appendChild(option);
      });
      if (desiredPattern && cachedPatterns[desiredPattern]) {
        presetPatternInput.value = desiredPattern;
      } else if (entries.length > 0) {
        let defaultPattern = entries[0];
        for (const patternName of entries) {
          const config = cachedPatterns[patternName];
          const hasMapping = config && Object.keys(config).some((key) => {
            return typeof key === 'string' && key.startsWith('n');
          });
          if (hasMapping) {
            defaultPattern = patternName;
            break;
          }
        }
        presetPatternInput.value = defaultPattern;
      }
      updatePresetNLabels(presetPatternInput.value);
    } catch (error) {
      console.warn('Failed to load patterns:', error);
    }
  };

  const updatePresetNLabels = (patternName) => {
    const labels = {};
    const visibleNKeys = new Set();
    
    // Initialize all labels with default n1:, n2:, etc.
    for (let i = 1; i <= 8; i++) {
      labels[`n${i}`] = `n${i}:`;
    }
    
    const patternConfig = cachedPatterns && cachedPatterns[patternName];
    if (patternConfig && typeof patternConfig === 'object') {
      // Now n values are keys and descriptive names are values
      Object.entries(patternConfig).forEach(([key, label]) => {
        if (typeof key === 'string' && key.startsWith('n') && typeof label === 'string') {
          labels[key] = `${label}:`;
          visibleNKeys.add(key); // Mark this n key as visible
        }
      });
    }
    
    // Update labels and show/hide input groups
    for (let i = 1; i <= 8; i++) {
      const nKey = `n${i}`;
      const labelEl = document.getElementById(`preset-${nKey}-label`);
      const inputEl = document.getElementById(`preset-${nKey}-input`);
      const groupEl = labelEl ? labelEl.closest('.n-param-group') : null;
      
      if (labelEl) {
        labelEl.textContent = labels[nKey];
      }
      
      // Show or hide the entire group based on whether it has a mapping
      if (groupEl) {
        if (visibleNKeys.has(nKey)) {
          groupEl.style.display = ''; // Show
        } else {
          groupEl.style.display = 'none'; // Hide
        }
      }
    }
  };

  const renderPresets = (presets) => {
    presetsList.innerHTML = '';
    cachedPresets = presets || {};
    const entries = Object.entries(cachedPresets);
    if (!entries.length) {
      const empty = document.createElement('p');
      empty.className = 'muted-text';
      empty.textContent = 'No presets found.';
      presetsList.appendChild(empty);
      return;
    }
    entries.forEach(([presetId, preset]) => {
      const row = document.createElement('div');
      row.className = 'profiles-row';

      const label = document.createElement('span');
      label.textContent = (preset && preset.name) || presetId;

      const details = document.createElement('span');
      const pattern = preset && preset.pattern ? preset.pattern : '-';
      details.textContent = pattern;
      details.style.color = '#aaa';
      details.style.fontSize = '0.85em';

      const editButton = document.createElement('button');
      editButton.className = 'btn btn-secondary btn-small';
      editButton.textContent = 'Edit';
      editButton.addEventListener('click', () => {
        currentEditId = presetId;
        setFormValues(preset || {});
        openEditor();
      });

      const sendButton = document.createElement('button');
      sendButton.className = 'btn btn-primary btn-small';
      sendButton.textContent = 'Send';
      sendButton.title = 'Send this preset via ESPNow';
      sendButton.addEventListener('click', () => {
        // Just send the definition; selection happens when user clicks the preset.
        sendPresetViaEspNow(presetId, preset || {});
      });

      const deleteButton = document.createElement('button');
      deleteButton.className = 'btn btn-danger btn-small';
      deleteButton.textContent = 'Delete';
      deleteButton.addEventListener('click', async () => {
        const confirmed = confirm(`Delete preset "${label.textContent}"?`);
        if (!confirmed) {
          return;
        }
        try {
          const response = await fetch(`/presets/${presetId}`, {
            method: 'DELETE',
            headers: { Accept: 'application/json' },
          });
          if (!response.ok) {
            throw new Error('Failed to delete preset');
          }
          await loadPresets();
          if (currentEditId === presetId) {
            clearForm();
          }
        } catch (error) {
          console.error('Delete preset failed:', error);
          alert('Failed to delete preset.');
        }
      });

      row.appendChild(label);
      row.appendChild(details);
      row.appendChild(editButton);
      row.appendChild(sendButton);
      row.appendChild(deleteButton);
      presetsList.appendChild(row);
    });
  };

  const loadPresets = async () => {
    presetsList.innerHTML = '';
    const loading = document.createElement('p');
    loading.className = 'muted-text';
    loading.textContent = 'Loading presets...';
    presetsList.appendChild(loading);

    try {
      const response = await fetch('/presets', {
        headers: { Accept: 'application/json' },
      });
      if (!response.ok) {
        throw new Error('Failed to load presets');
      }
      const presets = await response.json();
      renderPresets(presets);
    } catch (error) {
      console.error('Load presets failed:', error);
      presetsList.innerHTML = '';
      const errorMessage = document.createElement('p');
      errorMessage.className = 'muted-text';
      errorMessage.textContent = 'Failed to load presets.';
      presetsList.appendChild(errorMessage);
    }
  };

  const openModal = () => {
    presetsModal.classList.add('active');
    loadPresets();
  };

  const closeModal = () => {
    presetsModal.classList.remove('active');
  };

  presetsButton.addEventListener('click', openModal);
  if (presetsCloseButton) {
    presetsCloseButton.addEventListener('click', closeModal);
  }
  if (presetsAddButton) {
    presetsAddButton.addEventListener('click', () => {
      clearForm();
      openEditor();
    });
  }
  
  // Handle "Add Preset" button in tab area (dynamically loaded)
  document.addEventListener('click', async (e) => {
    if (e.target && e.target.id === 'preset-add-btn-tab') {
      await showAddPresetToTabModal();
    }
  });

  const showAddPresetToTabModal = async () => {
    // Get current tab ID from the left-panel
    const leftPanel = document.querySelector('.presets-section[data-tab-id]');
    let tabId = leftPanel ? leftPanel.dataset.tabId : null;
    
    if (!tabId) {
      // Fallback: try to get from URL
      const pathParts = window.location.pathname.split('/');
      const tabIndex = pathParts.indexOf('tabs');
      if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
        tabId = pathParts[tabIndex + 1];
      }
    }
    
    if (!tabId) {
      alert('Could not determine current tab.');
      return;
    }
    
    // Load all presets
    try {
      const response = await fetch('/presets', {
        headers: { Accept: 'application/json' },
      });
      if (!response.ok) {
        throw new Error('Failed to load presets');
      }
      const allPresets = await response.json();
      
      // Get current tab's presets to exclude already added ones
      let currentTabPresets = [];
      if (tabId) {
        try {
          const tabResponse = await fetch(`/tabs/${tabId}`, {
            headers: { Accept: 'application/json' },
          });
          if (tabResponse.ok) {
            const tabData = await tabResponse.json();
            currentTabPresets = tabData.presets || [];
          }
        } catch (e) {
          console.warn('Could not load current tab presets:', e);
        }
      }
      
      // Create modal
      const modal = document.createElement('div');
      modal.className = 'modal active';
      modal.id = 'add-preset-to-tab-modal';
      modal.innerHTML = `
        <div class="modal-content">
          <h2>Add Preset to Tab</h2>
          <div id="add-preset-list" class="profiles-list" style="max-height: 400px; overflow-y: auto;"></div>
          <div class="modal-actions">
            <button class="btn btn-secondary" id="add-preset-to-tab-close-btn">Close</button>
          </div>
        </div>
      `;
      
      document.body.appendChild(modal);
      
      const listContainer = document.getElementById('add-preset-list');
      const presetNames = Object.keys(allPresets);
      
      if (presetNames.length === 0) {
        listContainer.innerHTML = '<p class="muted-text">No presets available. Create a preset first.</p>';
      } else {
        presetNames.forEach(presetId => {
          const preset = allPresets[presetId];
          const isAlreadyAdded = currentTabPresets.includes(presetId);
          
          const row = document.createElement('div');
          row.className = 'profiles-row';
          
          const label = document.createElement('span');
          label.textContent = preset.name || presetId;
          
          const details = document.createElement('span');
          details.style.color = '#aaa';
          details.style.fontSize = '0.85em';
          details.textContent = preset.pattern || '-';
          
          const actionButton = document.createElement('button');
          if (isAlreadyAdded) {
            actionButton.className = 'btn btn-danger btn-small';
            actionButton.textContent = 'Remove';
            actionButton.addEventListener('click', async (e) => {
              e.stopPropagation();
              if (confirm(`Remove preset "${preset.name || presetId}" from this tab?`)) {
                await removePresetFromTab(presetId, tabId);
                modal.remove();
              }
            });
          } else {
            actionButton.className = 'btn btn-primary btn-small';
            actionButton.textContent = 'Add';
            actionButton.addEventListener('click', async () => {
              await addPresetToTab(presetId, tabId);
              modal.remove();
            });
          }
          
          row.appendChild(label);
          row.appendChild(details);
          row.appendChild(actionButton);
          listContainer.appendChild(row);
        });
      }
      
      // Close button handler
      document.getElementById('add-preset-to-tab-close-btn').addEventListener('click', () => {
        modal.remove();
      });
      
      // Close on outside click
      modal.addEventListener('click', (e) => {
        if (e.target === modal) {
          modal.remove();
        }
      });
    } catch (error) {
      console.error('Failed to show add preset modal:', error);
      alert('Failed to load presets.');
    }
  };

  const addPresetToTab = async (presetId, tabId) => {
    if (!tabId) {
      // Try to get tab ID from the left-panel
      const leftPanel = document.querySelector('.presets-section[data-tab-id]');
      tabId = leftPanel ? leftPanel.dataset.tabId : null;
      
      if (!tabId) {
        // Fallback: try to get from URL
        const pathParts = window.location.pathname.split('/');
        const tabIndex = pathParts.indexOf('tabs');
        if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
          tabId = pathParts[tabIndex + 1];
        }
      }
    }
    
    if (!tabId) {
      alert('Could not determine current tab.');
      return;
    }
    
    try {
      // Get current tab data
      const tabResponse = await fetch(`/tabs/${tabId}`, {
        headers: { Accept: 'application/json' },
      });
      if (!tabResponse.ok) {
        throw new Error('Failed to load tab');
      }
      const tabData = await tabResponse.json();
      
      // Add preset to tab's presets array if not already present
      const presets = tabData.presets || [];
      if (!presets.includes(presetId)) {
        presets.push(presetId);
        
        // Update tab
        const updateResponse = await fetch(`/tabs/${tabId}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ...tabData, presets }),
        });
        
        if (!updateResponse.ok) {
          throw new Error('Failed to update tab');
        }
        
        // Reload the tab content to show the new preset
        if (window.htmx) {
          htmx.ajax('GET', `/tabs/${tabId}/content-fragment`, {
            target: '#tab-content',
            swap: 'innerHTML'
          });
          // The htmx:afterSwap event listener will call renderTabPresets
        } else {
          // Fallback: reload the page
          window.location.reload();
        }
      } else {
        alert('Preset is already added to this tab.');
      }
    } catch (error) {
      console.error('Failed to add preset to tab:', error);
      alert('Failed to add preset to tab.');
    }
  };
  if (presetEditorCloseButton) {
    presetEditorCloseButton.addEventListener('click', closeEditor);
  }
  presetClearButton.addEventListener('click', clearForm);
  if (presetPatternInput) {
    presetPatternInput.addEventListener('change', () => {
      updatePresetNLabels(presetPatternInput.value);
      // Update color section visibility
      updateColorSectionVisibility();
      // Re-render colors to show updated max colors limit
      renderPresetColors(currentPresetColors);
    });
  }
  // Add Color button handler
  if (presetAddColorButton && presetNewColorInput) {
  presetAddColorButton.addEventListener('click', () => {
      const color = presetNewColorInput.value;
      if (!color) return;
      
      if (currentPresetColors.includes(color)) {
        alert('This color is already in the list.');
        return;
      }
      
      const maxColors = getMaxColors();
      if (currentPresetColors.length >= maxColors) {
        alert(`This pattern allows a maximum of ${maxColors} color${maxColors !== 1 ? 's' : ''}.`);
        return;
      }
      
      currentPresetColors.push(color);
      renderPresetColors(currentPresetColors);
    });
  }

  // Add from Palette button handler
  if (presetAddFromPaletteButton) {
    presetAddFromPaletteButton.addEventListener('click', () => {
      const openButton = document.getElementById('color-palette-btn');
      if (openButton) {
        openButton.click();
      }
      const modal = document.getElementById('color-palette-modal');
      const modalList = document.getElementById('palette-container');
      if (modal) {
        modal.classList.add('active');
      }
      if (!modalList) {
        return;
      }

      const handlePick = (event) => {
        const row = event.target.closest('[data-color]');
        if (!row) {
          return;
        }
        const picked = row.dataset.color;
        if (!picked) {
          return;
        }
        
        if (currentPresetColors.includes(picked)) {
          alert('This color is already in the list.');
          return;
        }
        
        const maxColors = getMaxColors();
        if (currentPresetColors.length >= maxColors) {
          alert(`This pattern allows a maximum of ${maxColors} color${maxColors !== 1 ? 's' : ''}.`);
          if (modal) {
            modal.classList.remove('active');
          }
          modalList.removeEventListener('click', handlePick);
          return;
        }
        
        currentPresetColors.push(picked);
        renderPresetColors(currentPresetColors);
        if (modal) {
          modal.classList.remove('active');
        }
        modalList.removeEventListener('click', handlePick);
      };

      modalList.addEventListener('click', handlePick);
    });
  }
  const presetSendButton = document.getElementById('preset-send-btn');

  if (presetSendButton) {
    presetSendButton.addEventListener('click', () => {
      const payload = buildPresetPayload();
      if (!payload.name) {
        alert('Preset name is required to send.');
        return;
      }
      // Send current editor values and select on all devices in the current tab (if any)
      const section = document.querySelector('.presets-section[data-tab-id]');
      const namesAttr = section && section.getAttribute('data-device-names');
      const deviceNames = namesAttr
        ? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0)
        : [];

      sendPresetViaEspNow(payload.name, payload, deviceNames);
    });
  }

  presetSaveButton.addEventListener('click', async () => {
    const payload = buildPresetPayload();
    if (!payload.name) {
      alert('Preset name is required.');
      return;
    }
    try {
      const url = currentEditId ? `/presets/${currentEditId}` : '/presets';
      const method = currentEditId ? 'PUT' : 'POST';
      const response = await fetch(url, {
        method,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!response.ok) {
        throw new Error('Failed to save preset');
      }

      // Determine device names from current tab (if any)
      let deviceNames = [];
      const section = document.querySelector('.presets-section[data-tab-id]');
      if (section) {
        const namesAttr = section.getAttribute('data-device-names');
        deviceNames = namesAttr
          ? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0)
          : [];
      }

      // Use saved preset from server response for sending
      const saved = await response.json().catch(() => null);
      if (saved && typeof saved === 'object') {
        if (currentEditId) {
          // PUT returns the preset object directly
          sendPresetViaEspNow(payload.name, saved, deviceNames);
        } else {
          // POST returns { id: preset }
          const entries = Object.entries(saved);
          if (entries.length > 0) {
            const [newId, presetData] = entries[0];
            sendPresetViaEspNow(newId, presetData, deviceNames);
          }
        }
      } else {
        // Fallback: send what we just built
        sendPresetViaEspNow(payload.name, payload, deviceNames);
      }

      await loadPresets();
      clearForm();
      closeEditor();
      
      // Reload tab presets if we're in a tab view
      const leftPanel = document.querySelector('.presets-section[data-tab-id]');
      if (leftPanel) {
        const tabId = leftPanel.dataset.tabId;
        if (tabId && typeof renderTabPresets !== 'undefined') {
          renderTabPresets(tabId);
        }
      }
    } catch (error) {
      console.error('Save preset failed:', error);
      alert('Failed to save preset.');
    }
  });

  // Listen for edit preset events from tab preset buttons
  document.addEventListener('editPreset', async (event) => {
    const { presetId, preset, tabId } = event.detail;
    currentEditId = presetId;
    currentEditTabId = tabId || null;
    await loadPatterns();
    setFormValues(preset);
    openEditor();
  });

  if (presetRemoveFromTabButton) {
    presetRemoveFromTabButton.addEventListener('click', async () => {
      if (!currentEditId) {
        alert('No preset loaded to remove.');
        return;
      }
      try {
        await removePresetFromTab(currentEditTabId, currentEditId);
        closeEditor();
      } catch (e) {
        // removePresetFromTab already logs and alerts on error
      }
    });
  }

  presetsModal.addEventListener('click', (event) => {
    if (event.target === presetsModal) {
      closeModal();
    }
  });

  if (presetEditorModal) {
    presetEditorModal.addEventListener('click', (event) => {
      if (event.target === presetEditorModal) {
        closeEditor();
      }
    });
  }

  clearForm();
});

// Build an ESPNow preset message for a single preset and optionally include a select
// for the given device names, then send it via WebSocket.
const sendPresetViaEspNow = (presetId, preset, deviceNames) => {
  try {
    const presetName = preset.name || presetId;
    if (!presetName) {
      alert('Preset has no name and cannot be sent.');
      return;
    }

    const colors = Array.isArray(preset.colors) && preset.colors.length
      ? preset.colors
      : ['#FFFFFF'];

    const message = {
      v: '1',
      presets: {
        [presetName]: {
          pattern: preset.pattern || 'off',
          colors,
          delay: typeof preset.delay === 'number' ? preset.delay : 100,
          brightness: typeof preset.brightness === 'number'
            ? preset.brightness
            : (typeof preset.br === 'number' ? preset.br : 127),
          auto: typeof preset.auto === 'boolean' ? preset.auto : true,
          n1: typeof preset.n1 === 'number' ? preset.n1 : 0,
          n2: typeof preset.n2 === 'number' ? preset.n2 : 0,
          n3: typeof preset.n3 === 'number' ? preset.n3 : 0,
          n4: typeof preset.n4 === 'number' ? preset.n4 : 0,
          n5: typeof preset.n5 === 'number' ? preset.n5 : 0,
          n6: typeof preset.n6 === 'number' ? preset.n6 : 0,
        },
      },
    };

    // Optionally include a select section for specific devices
    if (Array.isArray(deviceNames) && deviceNames.length > 0) {
      const select = {};
      deviceNames.forEach((name) => {
        if (name) {
          select[name] = [presetName];
        }
      });
      if (Object.keys(select).length > 0) {
        message.select = select;
      }
    }

    sendEspnowMessage(message);
  } catch (error) {
    console.error('Failed to send preset via ESPNow:', error);
    alert('Failed to send preset via ESPNow.');
  }
};

// Expose for other scripts (tabs.js) so they can reuse the shared WebSocket.
try {
  window.sendPresetViaEspNow = sendPresetViaEspNow;
} catch (e) {
  // window may not exist in some environments; ignore.
}

// Store selected preset per tab
const selectedPresets = {};
// Track if we're currently dragging a preset
let isDraggingPreset = false;
// Context menu for tab presets
let presetContextMenu = null;
let presetContextTarget = null;

const ensurePresetContextMenu = () => {
  if (presetContextMenu) {
    return presetContextMenu;
  }
  const menu = document.createElement('div');
  menu.id = 'preset-context-menu';
  menu.style.cssText = `
    position: fixed;
    z-index: 2000;
    background: #2e2e2e;
    border: 1px solid #4a4a4a;
    border-radius: 4px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.6);
    padding: 0.25rem 0;
    min-width: 160px;
    display: none;
  `;

  const addItem = (label, action) => {
    const item = document.createElement('button');
    item.type = 'button';
    item.textContent = label;
    item.dataset.action = action;
    item.style.cssText = `
      display: block;
      width: 100%;
      padding: 0.4rem 0.75rem;
      background: transparent;
      color: #eee;
      border: none;
      text-align: left;
      cursor: pointer;
      font-size: 0.9rem;
    `;
    if (action === 'remove') {
      // Visually emphasize and align remove to the right
      item.style.textAlign = 'right';
      item.style.color = '#ff8080';
    }
    item.addEventListener('mouseover', () => {
      item.style.backgroundColor = '#3a3a3a';
    });
    item.addEventListener('mouseout', () => {
      item.style.backgroundColor = 'transparent';
    });
    menu.appendChild(item);
  };

  addItem('Edit preset…', 'edit');
  addItem('Remove', 'remove');

  menu.addEventListener('click', async (e) => {
    const btn = e.target.closest('button[data-action]');
    if (!btn || !presetContextTarget) {
      return;
    }
    const { tabId, presetId } = presetContextTarget;
    const action = btn.dataset.action;
    hidePresetContextMenu();
    if (action === 'edit') {
      await editPresetFromTab(presetId);
    } else if (action === 'remove') {
      await removePresetFromTab(tabId, presetId);
    }
  });

  document.body.appendChild(menu);
  presetContextMenu = menu;

  // Hide on outside click
  document.addEventListener('click', (e) => {
    if (!presetContextMenu) return;
    if (e.target.closest('#preset-context-menu')) return;
    hidePresetContextMenu();
  });

  return menu;
};

const showPresetContextMenu = (x, y, tabId, presetId, preset) => {
  const menu = ensurePresetContextMenu();
  presetContextTarget = { tabId, presetId, preset };
  menu.style.left = `${x}px`;
  menu.style.top = `${y}px`;
  menu.style.display = 'block';
};

const hidePresetContextMenu = () => {
  if (presetContextMenu) {
    presetContextMenu.style.display = 'none';
  }
  presetContextTarget = null;
};

// Function to convert 2D grid to flat array (for backward compatibility)
const gridToArray = (presetsGrid) => {
  if (!presetsGrid || !Array.isArray(presetsGrid)) {
    return [];
  }
  // If it's already a flat array (old format), return it
  if (presetsGrid.length > 0 && typeof presetsGrid[0] === 'string') {
    return presetsGrid;
  }
  // If it's a 2D grid, flatten it
  if (Array.isArray(presetsGrid[0])) {
    return presetsGrid.flat();
  }
  // If it's an array of objects with positions, convert to grid then flatten
  if (presetsGrid.length > 0 && typeof presetsGrid[0] === 'object' && presetsGrid[0].id) {
    // Find max row and col
    let maxRow = 0, maxCol = 0;
    presetsGrid.forEach(p => {
      if (p.row > maxRow) maxRow = p.row;
      if (p.col > maxCol) maxCol = p.col;
    });
    // Create grid
    const grid = Array(maxRow + 1).fill(null).map(() => Array(maxCol + 1).fill(null));
    presetsGrid.forEach(p => {
      grid[p.row][p.col] = p.id;
    });
    return grid.flat().filter(id => id !== null);
  }
  return [];
};

// Function to convert flat array to 2D grid
const arrayToGrid = (presetIds, columns = 3) => {
  if (!presetIds || !Array.isArray(presetIds)) {
    return [];
  }
  const grid = [];
  for (let i = 0; i < presetIds.length; i += columns) {
    grid.push(presetIds.slice(i, i + columns));
  }
  return grid;
};

// Function to save preset grid for a tab
const savePresetGrid = async (tabId, presetGrid) => {
  try {
    // Get current tab data
    const tabResponse = await fetch(`/tabs/${tabId}`, {
      headers: { Accept: 'application/json' },
    });
    if (!tabResponse.ok) {
      throw new Error('Failed to load tab');
    }
    const tabData = await tabResponse.json();
    
    // Store as 2D grid
    tabData.presets = presetGrid;
    // Also store as flat array for backward compatibility
    tabData.presets_flat = presetGrid.flat();
    
    // Save updated tab
    const updateResponse = await fetch(`/tabs/${tabId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(tabData),
    });
    
    if (!updateResponse.ok) {
      throw new Error('Failed to save preset grid');
    }
  } catch (error) {
    console.error('Failed to save preset grid:', error);
    throw error;
  }
};

// Function to get drop target in 2D grid
const getDropTarget = (container, x, y) => {
  const draggableElements = [...container.querySelectorAll('.draggable-preset:not(.dragging)')];
  
  return draggableElements.reduce((closest, child) => {
    const box = child.getBoundingClientRect();
    const centerX = box.left + box.width / 2;
    const centerY = box.top + box.height / 2;
    const distanceX = Math.abs(x - centerX);
    const distanceY = Math.abs(y - centerY);
    const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
    
    if (distance < closest.distance) {
      return { distance: distance, element: child };
    } else {
      return closest;
    }
  }, { distance: Infinity }).element;
};

// Function to render presets for a specific tab in 2D grid
const renderTabPresets = async (tabId) => {
  const presetsList = document.getElementById('presets-list-tab');
  if (!presetsList) return;
  
  try {
    // Get tab data to see which presets are associated
    const tabResponse = await fetch(`/tabs/${tabId}`, {
      headers: { Accept: 'application/json' },
    });
    if (!tabResponse.ok) {
      throw new Error('Failed to load tab');
    }
    const tabData = await tabResponse.json();
    
    // Get presets - support both 2D grid and flat array (for backward compatibility)
    let presetGrid = tabData.presets;
    if (!presetGrid || !Array.isArray(presetGrid)) {
      // Try to get from flat array or convert old format
      const flatArray = tabData.presets_flat || tabData.presets || [];
      presetGrid = arrayToGrid(flatArray, 3); // Default to 3 columns
    } else if (presetGrid.length > 0 && typeof presetGrid[0] === 'string') {
      // It's a flat array, convert to grid
      presetGrid = arrayToGrid(presetGrid, 3);
    }
    
    // Get all presets
    const presetsResponse = await fetch('/presets', {
      headers: { Accept: 'application/json' },
    });
    if (!presetsResponse.ok) {
      throw new Error('Failed to load presets');
    }
    const allPresets = await presetsResponse.json();
    
    presetsList.innerHTML = '';
    
    // Add drag and drop handlers to the container
    presetsList.addEventListener('dragover', (e) => {
      e.preventDefault();
      const dragging = presetsList.querySelector('.dragging');
      if (!dragging) return;
      
      const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY);
      if (dropTarget && dropTarget !== dragging) {
        // Insert before or after based on position
        const rect = dropTarget.getBoundingClientRect();
        const draggingRect = dragging.getBoundingClientRect();
        
        if (e.clientX < rect.left + rect.width / 2) {
          // Insert before
          presetsList.insertBefore(dragging, dropTarget);
        } else {
          // Insert after
          presetsList.insertBefore(dragging, dropTarget.nextSibling);
        }
      }
    });
    
    presetsList.addEventListener('drop', async (e) => {
      e.preventDefault();
      const dragging = presetsList.querySelector('.dragging');
      if (!dragging) return;
      
      // Get new grid layout from DOM
      const presetElements = [...presetsList.querySelectorAll('.draggable-preset')];
      const presetIds = presetElements.map(el => el.dataset.presetId);
      
      // Convert to 2D grid (3 columns)
      const newGrid = arrayToGrid(presetIds, 3);
      
      // Save new grid
      try {
        await savePresetGrid(tabId, newGrid);
        // Re-render to ensure consistency
        await renderTabPresets(tabId);
      } catch (error) {
        console.error('Failed to save preset grid:', error);
        alert('Failed to save preset order. Please try again.');
        // Re-render to restore original order
        await renderTabPresets(tabId);
      }
    });
    
    // Get the currently selected preset for this tab
    const selectedPresetId = selectedPresets[tabId];
    
    // Render presets in grid layout
    // Flatten the grid and render all presets (grid CSS will handle layout)
    const flatPresets = presetGrid.flat().filter(id => id);
    
    if (flatPresets.length === 0) {
      // Show empty message if this tab has no presets
      const empty = document.createElement('p');
      empty.className = 'muted-text';
      empty.style.gridColumn = '1 / -1'; // Span all columns
      empty.textContent = 'No presets added to this tab. Click "Add Preset" to add one.';
      presetsList.appendChild(empty);
    } else {
      flatPresets.forEach((presetId) => {
        const preset = allPresets[presetId];
        if (preset) {
          const isSelected = presetId === selectedPresetId;
          const wrapper = createPresetButton(presetId, preset, tabId, isSelected);
          presetsList.appendChild(wrapper);
        }
      });
    }
  } catch (error) {
    console.error('Failed to render tab presets:', error);
    presetsList.innerHTML = '<p class="muted-text">Failed to load presets.</p>';
  }
};

const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
  // Create wrapper div for button and edit button
  const wrapper = document.createElement('div');
  wrapper.style.cssText = 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;';
  wrapper.draggable = true;
  wrapper.dataset.presetId = presetId;
  wrapper.classList.add('draggable-preset');
  
  // Create preset button
  const button = document.createElement('button');
  button.className = 'pattern-button';
  button.style.flex = '1';
  if (isSelected) {
    button.classList.add('active');
  }
  button.dataset.presetId = presetId;
  
  const presetInfo = document.createElement('div');
  presetInfo.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start; width: 100%;';
  
  const presetNameLabel = document.createElement('span');
  presetNameLabel.textContent = preset.name || presetId;
  presetNameLabel.style.fontWeight = 'bold';
  presetNameLabel.style.marginBottom = '0.25rem';
  
  const presetDetails = document.createElement('span');
  presetDetails.style.fontSize = '0.85em';
  presetDetails.style.color = '#aaa';
  const colors = Array.isArray(preset.colors) ? preset.colors : [];
  presetDetails.textContent = `${preset.pattern || '-'} • ${colors.length} color${colors.length !== 1 ? 's' : ''}`;
  
  presetInfo.appendChild(presetNameLabel);
  presetInfo.appendChild(presetDetails);
  button.appendChild(presetInfo);
  
  // Left-click selects preset, right-click opens editor
  button.addEventListener('click', (e) => {
    if (isDraggingPreset) {
      return;
    }

    // Remove active class from all presets in this tab
    const presetsList = document.getElementById('presets-list-tab');
    if (presetsList) {
      presetsList.querySelectorAll('.pattern-button').forEach(btn => {
        btn.classList.remove('active');
      });
    }

    // Add active class to clicked preset
    button.classList.add('active');

    // Store selected preset for this tab
    selectedPresets[tabId] = presetId;

    // Build and send a select message via WebSocket for all device names in this tab.
    const presetName = preset.name || presetId;
    const section = button.closest('.presets-section');
    sendSelectForCurrentTabDevices(presetName, section);
  });

  button.addEventListener('contextmenu', async (e) => {
    e.preventDefault();
    if (isDraggingPreset) {
      return;
    }
    // Right-click: directly open the preset editor using data we already have
    await editPresetFromTab(presetId, tabId, preset);
  });
  
  wrapper.appendChild(button);
  
  // Add drag event handlers
  wrapper.addEventListener('dragstart', (e) => {
    isDraggingPreset = true;
    wrapper.classList.add('dragging');
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', presetId);
  });
  
  wrapper.addEventListener('dragend', (e) => {
    wrapper.classList.remove('dragging');
    // Remove any drag-over classes from siblings
    document.querySelectorAll('.draggable-preset').forEach(el => {
      el.classList.remove('drag-over');
    });
    // Reset dragging flag after a short delay to allow click event to check it
    setTimeout(() => {
      isDraggingPreset = false;
    }, 100);
  });
  
  return wrapper;
};

const editPresetFromTab = async (presetId, tabId, existingPreset) => {
  try {
    let preset = existingPreset;
    if (!preset) {
      // Fallback: load the preset data from the server if we weren't given it
      const response = await fetch(`/presets/${presetId}`, {
        headers: { Accept: 'application/json' },
      });
      if (!response.ok) {
        throw new Error('Failed to load preset');
      }
      preset = await response.json();
    }
    
    // Dispatch a custom event to trigger the edit in the DOMContentLoaded scope
    const editEvent = new CustomEvent('editPreset', {
      detail: { presetId, preset, tabId }
    });
    document.dispatchEvent(editEvent);
  } catch (error) {
    console.error('Failed to load preset for editing:', error);
    alert('Failed to load preset for editing.');
  }
};

// Remove a preset from a specific tab (does not delete the preset itself)
// Expected call style: removePresetFromTab(tabId, presetId)
const removePresetFromTab = async (tabId, presetId) => {
  if (!tabId) {
    // Try to get tab ID from the left-panel
    const leftPanel = document.querySelector('.presets-section[data-tab-id]');
    tabId = leftPanel ? leftPanel.dataset.tabId : null;
    
    if (!tabId) {
      // Fallback: try to get from URL
      const pathParts = window.location.pathname.split('/');
      const tabIndex = pathParts.indexOf('tabs');
      if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
        tabId = pathParts[tabIndex + 1];
      }
    }
  }
  
  if (!tabId) {
    alert('Could not determine current tab.');
    return;
  }
  
  try {
    // Get current tab data
    const tabResponse = await fetch(`/tabs/${tabId}`, {
      headers: { Accept: 'application/json' },
    });
    if (!tabResponse.ok) {
      throw new Error('Failed to load tab');
    }
    const tabData = await tabResponse.json();
    
    // Normalize to flat array
    let flat = [];
    if (Array.isArray(tabData.presets_flat)) {
      flat = tabData.presets_flat.slice();
    } else if (Array.isArray(tabData.presets)) {
      if (tabData.presets.length && typeof tabData.presets[0] === 'string') {
        flat = tabData.presets.slice();
      } else if (Array.isArray(tabData.presets[0])) {
        flat = tabData.presets.flat();
      }
    }

    const beforeLen = flat.length;
    flat = flat.filter(id => String(id) !== String(presetId));
    if (flat.length === beforeLen) {
      alert('Preset is not in this tab.');
      return;
    }

    const newGrid = arrayToGrid(flat, 3);
    tabData.presets = newGrid;
    tabData.presets_flat = flat;

    const updateResponse = await fetch(`/tabs/${tabId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(tabData),
    });
    if (!updateResponse.ok) {
      throw new Error('Failed to update tab presets');
    }

    await renderTabPresets(tabId);
  } catch (error) {
    console.error('Failed to remove preset from tab:', error);
    alert('Failed to remove preset from tab.');
  }
};

// Listen for HTMX swaps to render presets
document.body.addEventListener('htmx:afterSwap', (event) => {
  if (event.target && event.target.id === 'tab-content') {
    // Get tab ID from the left-panel
    const leftPanel = document.querySelector('.presets-section[data-tab-id]');
    if (leftPanel) {
      const tabId = leftPanel.dataset.tabId;
      if (tabId) {
        renderTabPresets(tabId);
      }
    }
  }
});
