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

const getCurrentProfileId = async () => {
  try {
    const res = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
    if (!res.ok) return currentProfileIdCache ? String(currentProfileIdCache) : null;
    const data = await res.json();
    const id = data && (data.id || (data.profile && data.profile.id));
    currentProfileIdCache = id ? String(id) : null;
    return currentProfileIdCache;
  } catch (_) {
    return currentProfileIdCache ? String(currentProfileIdCache) : null;
  }
};

const filterPresetsForCurrentProfile = async (presetsObj) => {
  const scoped = presetsObj && typeof presetsObj === 'object' ? presetsObj : {};
  const currentProfileId = await getCurrentProfileId();
  if (!currentProfileId) return scoped;
  return Object.fromEntries(
    Object.entries(scoped).filter(([, preset]) => {
      if (!preset || typeof preset !== 'object') return false;
      if (!('profile_id' in preset)) return true; // Legacy records
      return String(preset.profile_id) === String(currentProfileId);
    }),
  );
};

const getCurrentProfileData = async () => {
  try {
    const res = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
    if (!res.ok) return null;
    return await res.json();
  } catch (_) {
    return null;
  }
};

const getCurrentProfilePaletteColors = async () => {
  const profileData = await getCurrentProfileData();
  const profile = profileData && profileData.profile;
  const paletteId = profile && (profile.palette_id || profile.paletteId);
  if (!paletteId) return [];
  try {
    const res = await fetch(`/palettes/${paletteId}`, { headers: { Accept: 'application/json' } });
    if (!res.ok) return [];
    const pal = await res.json();
    return Array.isArray(pal.colors) ? pal.colors : [];
  } catch (_) {
    return [];
  }
};

const resolveColorsWithPaletteRefs = (colors, paletteRefs, paletteColors) => {
  const baseColors = Array.isArray(colors) ? colors : [];
  const refs = Array.isArray(paletteRefs) ? paletteRefs : [];
  const pal = Array.isArray(paletteColors) ? paletteColors : [];
  return baseColors.map((color, idx) => {
    const refRaw = refs[idx];
    const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
    if (Number.isInteger(ref) && ref >= 0 && ref < pal.length && pal[ref]) {
      return pal[ref];
    }
    return color;
  });
};

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

  const wsScheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  const wsUrl = `${wsScheme}//${window.location.host}/ws`;
  espnowSocket = new WebSocket(wsUrl);
  espnowSocketReady = false;

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

  espnowSocket.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      if (data && data.type === 'device_tcp' && typeof data.connected === 'boolean' && data.ip) {
        window.dispatchEvent(
          new CustomEvent('deviceTcpStatus', { detail: { ip: data.ip, connected: data.connected } }),
        );
        return;
      }
      if (data && data.type === 'device_tcp_snapshot' && Array.isArray(data.connected_ips)) {
        window.dispatchEvent(
          new CustomEvent('deviceTcpSnapshot', { detail: { connectedIps: data.connected_ips } }),
        );
        return;
      }
      if (data && data.error) {
        console.error('ESP-NOW:', data.error);
        alert('ESP-NOW send failed. ' + (data.error === 'ESP-NOW send failed' ? 'Check device WiFi/interface.' : data.error));
      }
    } catch (_) {
      // Ignore non-JSON or non-error messages
    }
  };

  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);
  }
};

function tabDeviceNamesFromSection(section) {
  if (typeof window.parseTabDeviceNames === 'function') {
    return window.parseTabDeviceNames(section);
  }
  const namesAttr = section && section.getAttribute('data-device-names');
  return namesAttr
    ? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0)
    : [];
}

async function postDriverSequence(sequence, targetMacs, delayS) {
  const body = {
    sequence,
    targets: Array.isArray(targetMacs) && targetMacs.length ? [...new Set(targetMacs)] : undefined,
  };
  if (delayS != null && delayS >= 0) {
    body.delay_s = delayS;
  }
  const res = await fetch('/presets/push', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    credentials: 'same-origin',
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error((err && err.error) || res.statusText || 'Send failed');
  }
  return res.json().catch(() => ({}));
}

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 presetClearDeviceButton = document.getElementById('preset-clear-device-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 presetBrightnessInput = document.getElementById('preset-brightness-input');
  const presetDelayInput = document.getElementById('preset-delay-input');
  const presetDelayField = presetDelayInput ? presetDelayInput.closest('.preset-editor-field') : null;
  const presetBackgroundInput = document.getElementById('preset-background-input');
  const presetBackgroundButton = document.getElementById('preset-background-btn');
  const presetManualModeInput = document.getElementById('preset-manual-mode-input');
  const presetManualModeHint = document.getElementById('preset-manual-mode-hint');
  const presetManualModeLabel = document.getElementById('preset-manual-mode-label');
  const presetManualBeatNWrap = document.getElementById('preset-manual-beat-n-wrap');
  const presetManualBeatNInput = document.getElementById('preset-manual-beat-n-input');
  const presetDefaultButton = document.getElementById('preset-default-btn');
  const presetRemoveFromTabButton = document.getElementById('preset-remove-from-zone-btn');
  const presetSaveButton = document.getElementById('preset-save-btn');
  const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');

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

  let currentEditId = null;
  let currentEditTabId = null;
  let cachedPresets = {};
  let cachedPatterns = {};
  let currentPresetColors = []; // Track colors for the current preset
  let currentPresetPaletteRefs = []; // Palette index refs per color (null for direct colors)

  // 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
  };

  const resolvePatternConfig = (patternName) => {
    const rawPatternName = String(patternName || '').trim();
    const normalizedPatternName = rawPatternName.endsWith('.py')
      ? rawPatternName.slice(0, -3)
      : rawPatternName;
    let patternConfig =
      (cachedPatterns && cachedPatterns[rawPatternName]) ||
      (cachedPatterns && cachedPatterns[normalizedPatternName]) ||
      null;
    if (!patternConfig && cachedPatterns && typeof cachedPatterns === 'object') {
      const lower = normalizedPatternName.toLowerCase();
      const matchedKey = Object.keys(cachedPatterns).find(
        (k) => String(k).toLowerCase() === lower,
      );
      if (matchedKey) {
        patternConfig = cachedPatterns[matchedKey];
      }
    }
    if (patternConfig && typeof patternConfig === 'object' && patternConfig.data && typeof patternConfig.data === 'object') {
      patternConfig = patternConfig.data;
    }
    if (
      patternConfig &&
      typeof patternConfig === 'object' &&
      patternConfig.parameter_mappings &&
      typeof patternConfig.parameter_mappings === 'object'
    ) {
      patternConfig = patternConfig.parameter_mappings;
    }
    return patternConfig && typeof patternConfig === 'object' ? patternConfig : null;
  };

  /** From db/pattern.json; missing key means pattern allows manual / beat (backward compatible). */
  const patternSupportsManual = (patternName) => {
    const cfg = resolvePatternConfig(patternName);
    if (!cfg) {
      return true;
    }
    return cfg.supports_manual !== false;
  };

  const updateManualBeatNVisibility = () => {
    if (!presetManualBeatNWrap) {
      return;
    }
    const manualOn = presetManualModeInput && presetManualModeInput.checked;
    const patternName = presetPatternInput ? presetPatternInput.value.trim() : '';
    const ok = !patternName || patternSupportsManual(patternName);
    presetManualBeatNWrap.style.display = manualOn && ok ? '' : 'none';
  };

  const updatePresetBackgroundButton = () => {
    if (!presetBackgroundButton || !presetBackgroundInput) return;
    const color = coercePresetBackground({ background: presetBackgroundInput.value });
    presetBackgroundInput.value = color;
    presetBackgroundButton.textContent = color;
    presetBackgroundButton.style.backgroundColor = color;
    presetBackgroundButton.style.color = '#fff';
    presetBackgroundButton.style.borderColor = 'rgba(255, 255, 255, 0.6)';
  };

  const updateDelayVisibilityForManualMode = () => {
    if (!presetDelayField) return;
    const manualOn = presetManualModeInput && presetManualModeInput.checked;
    presetDelayField.style.display = manualOn ? 'none' : '';
  };

  const updateManualModeAvailability = () => {
    if (!presetManualModeInput) {
      return;
    }
    const patternName = presetPatternInput ? presetPatternInput.value.trim() : '';
    const ok = !patternName || patternSupportsManual(patternName);
    presetManualModeInput.disabled = !ok;
    if (presetManualModeLabel) {
      presetManualModeLabel.style.opacity = ok ? '' : '0.55';
    }
    if (presetManualModeHint) {
      if (!patternName || ok) {
        presetManualModeHint.style.display = 'none';
        presetManualModeHint.textContent = '';
      } else {
        presetManualModeHint.style.display = '';
        presetManualModeHint.textContent =
          'This pattern is a poor fit for manual mode or audio beat triggers; use auto mode for best results.';
      }
    }
    if (!ok) {
      presetManualModeInput.checked = false;
    }
    updateManualBeatNVisibility();
    updateDelayVisibilityForManualMode();
  };

  // 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-new-color')) {
        colorActions.style.display = shouldShow ? '' : 'none';
      }
    }
  };

  const getNumberInput = (id) => {
    const input = document.getElementById(id);
    if (!input) {
      return 0;
    }
    const n = parseInt(String(input.value).trim(), 10);
    return Number.isFinite(n) ? n : 0;
  };

  const renderPresetColors = (colors, paletteRefs) => {
    if (!presetColorsContainer) return;
    
    presetColorsContainer.innerHTML = '';
    currentPresetColors = Array.isArray(colors) ? colors.slice() : [];
    if (Array.isArray(paletteRefs)) {
      currentPresetPaletteRefs = currentPresetColors.map((_, i) => {
        const refRaw = paletteRefs[i];
        const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
        return Number.isInteger(ref) ? ref : null;
      });
    } else {
      currentPresetPaletteRefs = currentPresetColors.map((_, i) => {
        const refRaw = currentPresetPaletteRefs[i];
        const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
        return Number.isInteger(ref) ? ref : null;
      });
    }
    
    // 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. Use the color picker 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: nowrap; gap: 0.5rem; align-items: flex-start; overflow-x: auto;';
    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;
      const refAtIndex = currentPresetPaletteRefs[index];
      swatchWrapper.dataset.paletteRef = Number.isInteger(refAtIndex) ? String(refAtIndex) : '';
      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`;

      if (Number.isInteger(refAtIndex)) {
        const linkedBadge = document.createElement('span');
        linkedBadge.textContent = 'P';
        linkedBadge.title = `Linked to palette color #${refAtIndex + 1}`;
        linkedBadge.style.cssText = `
          position: absolute;
          left: -6px;
          top: -6px;
          min-width: 18px;
          height: 18px;
          border-radius: 9px;
          background: #3f51b5;
          color: #fff;
          font-size: 11px;
          font-weight: 700;
          display: flex;
          align-items: center;
          justify-content: center;
          z-index: 11;
          border: 1px solid rgba(255,255,255,0.35);
          box-shadow: 0 1px 3px rgba(0,0,0,0.35);
        `;
        swatchWrapper.appendChild(linkedBadge);
      }
      
      // 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;
        // Manual picker edit breaks palette linkage for this slot.
        currentPresetPaletteRefs[index] = null;
        renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
      });
      // 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);
        currentPresetPaletteRefs.splice(index, 1);
        renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
      });
      // 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);
      const newRefOrder = colorElements.map((el) => {
        const refRaw = el.dataset.paletteRef;
        const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
        return Number.isInteger(ref) ? ref : null;
      });
      
      // Update current colors array
      currentPresetColors = newColorOrder;
      currentPresetPaletteRefs = newRefOrder;
      
      // Re-render to update indices
      renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
    });
    
    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.slice() : [];
    const paletteRefs = Array.isArray(preset.palette_refs) ? preset.palette_refs.slice() : [];
    renderPresetColors(colors, paletteRefs);
    presetBrightnessInput.value = preset.brightness || 0;
    presetDelayInput.value = preset.delay || 0;
    if (presetBackgroundInput) {
      presetBackgroundInput.value = coercePresetBackground(preset);
    }
    updatePresetBackgroundButton();
    if (presetManualModeInput) {
      const autoVal = typeof preset.auto === 'boolean' ? preset.auto : true;
      presetManualModeInput.checked = !autoVal;
    }
    if (presetManualBeatNInput) {
      const raw = preset.manual_beat_n;
      let n = typeof raw === 'number' ? raw : parseInt(String(raw != null ? raw : '1'), 10);
      if (!Number.isFinite(n)) n = 1;
      n = Math.max(1, Math.min(64, n));
      presetManualBeatNInput.value = String(n);
    }

    // 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 = '';
    }

    // Get pattern config to map descriptive names back to n keys
    const patternConfig = cachedPatterns && cachedPatterns[patternName];
    const nToLabel = {};
    if (patternConfig && typeof patternConfig === 'object') {
      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) {
        if (preset[nKey] !== undefined && preset[nKey] !== null) {
          const raw = preset[nKey];
          const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10);
          inputEl.value = String(Number.isFinite(n) ? n : 0);
        } else {
          const label = nToLabel[nKey];
          if (label && preset[label] !== undefined && preset[label] !== null) {
            const rawL = preset[label];
            const nL = typeof rawL === 'number' ? rawL : parseInt(String(rawL), 10);
            inputEl.value = String(Number.isFinite(nL) ? nL : 0);
          } else {
            inputEl.value = '0';
          }
        }
      }
    }

    // After values: show only mapped n params with labels from pattern.json; clear hidden inputs
    updatePresetNLabels(patternName);
    updateManualModeAvailability();
    updatePresetEditorTabActionsVisibility();
  };

  const clearForm = () => {
    currentEditId = null;
    currentEditTabId = null;
    currentPresetColors = [];
    currentPresetPaletteRefs = [];
    setFormValues({
      name: '',
      pattern: '',
      colors: [],
      brightness: 0,
      delay: 0,
      n1: 0,
      n2: 0,
      n3: 0,
      n4: 0,
      n5: 0,
      n6: 0,
      n7: 0,
      n8: 0,
      background: '#000000',
      auto: true,
      manual_beat_n: 1,
    });
    if (presetManualModeInput) {
      presetManualModeInput.checked = false;
    }
    if (presetManualBeatNInput) {
      presetManualBeatNInput.value = '1';
    }
    if (presetBackgroundInput) {
      presetBackgroundInput.value = '#000000';
    }
    updatePresetBackgroundButton();
    updateManualModeAvailability();
    // 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 = '';
    }
    updatePresetEditorTabActionsVisibility();
  };

  const getActiveTabId = () => {
    if (currentEditTabId) {
      return currentEditTabId;
    }
    const section = document.querySelector('.presets-section[data-zone-id]');
    return section ? section.dataset.zoneId : null;
  };

  const updatePresetEditorTabActionsVisibility = () => {
    if (!presetRemoveFromTabButton) return;
    const show = Boolean(currentEditTabId && currentEditId);
    presetRemoveFromTabButton.hidden = !show;
  };

  const updateTabDefaultPreset = async (presetId) => {
    const zoneId = getActiveTabId();
    if (!zoneId) {
      return;
    }
    try {
      const tabResponse = await fetch(`/zones/${zoneId}`, {
        headers: { Accept: 'application/json' },
      });
      if (!tabResponse.ok) {
        return;
      }
      const tabData = await tabResponse.json();
      tabData.default_preset = presetId;
      await fetch(`/zones/${zoneId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(tabData),
      });
    } catch (error) {
      console.warn('Failed to save zone default preset:', error);
    }
  };

  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 || [],
      palette_refs: currentPresetPaletteRefs || [],
      // 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,
      background: presetBackgroundInput ? presetBackgroundInput.value : '#000000',
      auto: presetManualModeInput ? !presetManualModeInput.checked : true,
      manual_beat_n: (() => {
        if (!presetManualBeatNInput) return 1;
        let n = parseInt(presetManualBeatNInput.value, 10);
        if (!Number.isFinite(n)) n = 1;
        return Math.max(1, Math.min(64, n));
      })(),
    };

    // 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 normalizePatternMap = (raw) => {
    if (!raw) return {};
    if (typeof raw === 'object' && !Array.isArray(raw)) {
      // Support wrapped payloads like { patterns: {...} }.
      if (raw.patterns && typeof raw.patterns === 'object' && !Array.isArray(raw.patterns)) {
        return raw.patterns;
      }
      return raw;
    }
    if (Array.isArray(raw)) {
      // Support list payloads like [{name: "blink", ...}, ...].
      return raw.reduce((acc, item, idx) => {
        if (item && typeof item === 'object') {
          const name = item.name || item.id || String(idx);
          acc[String(name)] = item;
        }
        return acc;
      }, {});
    }
    return {};
  };

  const loadPatterns = async () => {
    if (!presetPatternInput) {
      return;
    }
    try {
      // Load pattern definitions from pattern.json
      let patternsPayload = null;
      let response = await fetch('/patterns/definitions', {
        cache: 'no-store',
        headers: { Accept: 'application/json' },
      });
      if (response.ok) {
        patternsPayload = await response.json();
      }

      let normalized = normalizePatternMap(patternsPayload);
      if (!Object.keys(normalized).length) {
        // Fallback when definitions route is unavailable or returns an empty map.
        response = await fetch('/patterns', {
          cache: 'no-store',
          headers: { Accept: 'application/json' },
        });
        if (!response.ok) {
          return;
        }
        patternsPayload = await response.json();
        normalized = normalizePatternMap(patternsPayload);
      }
      cachedPatterns = normalized;
      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 rawPatternName = String(patternName || '').trim();
    const normalizedPatternName = rawPatternName.endsWith('.py')
      ? rawPatternName.slice(0, -3)
      : rawPatternName;
    let patternConfig =
      (cachedPatterns && cachedPatterns[rawPatternName]) ||
      (cachedPatterns && cachedPatterns[normalizedPatternName]) ||
      null;
    if (!patternConfig && cachedPatterns && typeof cachedPatterns === 'object') {
      const lower = normalizedPatternName.toLowerCase();
      const matchedKey = Object.keys(cachedPatterns).find(
        (k) => String(k).toLowerCase() === lower,
      );
      if (matchedKey) {
        patternConfig = cachedPatterns[matchedKey];
      }
    }
    if (patternConfig && typeof patternConfig === 'object' && patternConfig.data && typeof patternConfig.data === 'object') {
      patternConfig = patternConfig.data;
    }
    if (patternConfig && typeof patternConfig === 'object' && patternConfig.parameter_mappings && typeof patternConfig.parameter_mappings === 'object') {
      patternConfig = patternConfig.parameter_mappings;
    }
    const labels = {};
    const visibleNKeys = new Set();

    if (patternConfig && typeof patternConfig === 'object') {
      Object.entries(patternConfig).forEach(([key, label]) => {
        if (typeof key === 'string' && key.startsWith('n') && typeof label === 'string') {
          const text = label.trim();
          if (text) {
            labels[key] = `${text}:`;
            visibleNKeys.add(key);
          }
        }
      });
    }

    const hasPatternMeta =
      patternConfig && typeof patternConfig === 'object' && Object.keys(patternConfig).length > 0;
    const hasAnyNLabel = visibleNKeys.size > 0;

    for (let i = 1; i <= 8; i++) {
      const nKey = `n${i}`;
      const labelEl = document.getElementById(`preset-${nKey}-label`);
      const groupEl = labelEl ? labelEl.closest('.n-param-group') : null;
      const show = visibleNKeys.has(nKey);
      const inputEl = document.getElementById(`preset-${nKey}-input`);

      if (labelEl) {
        labelEl.textContent = show ? labels[nKey] : '';
      }
      if (groupEl) {
        groupEl.style.display = show ? '' : 'none';
      }
      // Only clear hidden n inputs when we know this pattern's metadata (avoids wiping n3..n4
      // while definitions are still loading, or when twinkle exists only as a driver file).
      if (inputEl && !show && (hasAnyNLabel || hasPatternMeta)) {
        inputEl.value = '0';
      }
    }

    const nGrid = presetEditorModal && presetEditorModal.querySelector('.n-params-grid');
    if (nGrid) {
      nGrid.style.display = visibleNKeys.size > 0 ? '' : 'none';
    }
    updateManualModeAvailability();
  };

  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', async () => {
        currentEditId = presetId;
        currentEditTabId = null;
        await loadPatterns();
        const paletteColors = await getCurrentProfilePaletteColors();
        const presetForEditor = {
          ...(preset || {}),
          colors: resolveColorsWithPaletteRefs(
            (preset && preset.colors) || [],
            (preset && preset.palette_refs) || [],
            paletteColors,
          ),
        };
        setFormValues(presetForEditor);
        openEditor();
      });

      const sendButton = document.createElement('button');
      sendButton.className = 'btn btn-primary btn-small';
      sendButton.textContent = 'Send';
      sendButton.title = 'Send this preset to drivers';
      sendButton.addEventListener('click', () => {
        // Just send the definition; selection happens when user clicks the preset.
        void 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();
      const filtered = await filterPresetsForCurrentProfile(presets);
      renderPresets(filtered);
    } 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();
    });
  }
  if (presetClearDeviceButton) {
    presetClearDeviceButton.addEventListener('click', async () => {
      const section = document.querySelector('.presets-section[data-zone-id]');
      const deviceNames = tabDeviceNamesFromSection(section);
      if (!deviceNames.length) {
        alert('No devices found in the current zone.');
        return;
      }
      if (!window.confirm('Clear all presets on current zone devices?')) {
        return;
      }
      try {
        const targetMacs =
          typeof window.tabsManager !== 'undefined' &&
          typeof window.tabsManager.resolveTabDeviceMacs === 'function'
            ? await window.tabsManager.resolveTabDeviceMacs(deviceNames)
            : [];
        await postDriverSequence([{ v: '1', clear_presets: true, save: true }], targetMacs);
      } catch (error) {
        console.error('Clear device presets failed:', error);
        alert('Failed to clear presets on devices.');
      }
    });
  }
  
  const showAddPresetToTabModal = async (optionalTabId) => {
    let zoneId = optionalTabId;
    if (!zoneId) {
      // Get current zone ID from the presets section
      const leftPanel = document.querySelector('.presets-section[data-zone-id]');
      zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
    }
    if (!zoneId) {
      // Fallback: try to get from URL
      const pathParts = window.location.pathname.split('/');
      const tabIndex = pathParts.indexOf('zones');
      if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
        zoneId = pathParts[tabIndex + 1];
      }
    }
    if (!zoneId) {
      alert('Could not determine current zone.');
      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 allPresetsRaw = await response.json();
      const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
      
      // Load only the current zone's presets so we can avoid duplicates within this zone.
      let currentTabPresets = [];
      try {
        const tabResponse = await fetch(`/zones/${zoneId}`, {
          headers: { Accept: 'application/json' },
        });
        if (tabResponse.ok) {
          const tabData = await tabResponse.json();
          if (Array.isArray(tabData.presets_flat)) {
            currentTabPresets = tabData.presets_flat.slice();
          } else if (Array.isArray(tabData.presets)) {
            if (tabData.presets.length && typeof tabData.presets[0] === 'string') {
              currentTabPresets = tabData.presets.slice();
            } else if (Array.isArray(tabData.presets[0])) {
              currentTabPresets = tabData.presets.flat();
            }
          }
        }
      } catch (e) {
        console.warn('Could not load current zone presets:', e);
      }
      
      // Create modal
      const modal = document.createElement('div');
      modal.className = 'modal active';
      modal.id = 'add-preset-to-zone-modal';
      modal.innerHTML = `
        <div class="modal-content">
          <h2>Add Preset to Zone</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-zone-close-btn">Close</button>
          </div>
        </div>
      `;
      
      document.body.appendChild(modal);
      
      const listContainer = document.getElementById('add-preset-list');
      const presetNames = Object.keys(allPresets);
      
      const availableToAdd = presetNames.filter(presetId => !currentTabPresets.includes(presetId));
      if (availableToAdd.length === 0) {
        listContainer.innerHTML = '<p class="muted-text">No presets to add. All presets are already in this zone, or create a preset first.</p>';
      } else {
        availableToAdd.forEach(presetId => {
          const preset = allPresets[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 addButton = document.createElement('button');
          addButton.className = 'btn btn-primary btn-small';
          addButton.textContent = 'Add';
          addButton.addEventListener('click', async () => {
            await addPresetToTab(presetId, zoneId);
            modal.remove();
          });
          
          row.appendChild(label);
          row.appendChild(details);
          row.appendChild(addButton);
          listContainer.appendChild(row);
        });
      }
      
      // Close button handler
      document.getElementById('add-preset-to-zone-close-btn').addEventListener('click', () => {
        modal.remove();
      });
      
    } catch (error) {
      console.error('Failed to show add preset modal:', error);
      alert('Failed to load presets.');
    }
  };
  try {
    window.showAddPresetToTabModal = showAddPresetToTabModal;
  } catch (e) {}

  const addPresetToTab = async (presetId, zoneId) => {
    if (!zoneId) {
      // Try to get zone ID from the left-panel
      const leftPanel = document.querySelector('.presets-section[data-zone-id]');
      zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
      
      if (!zoneId) {
        // Fallback: try to get from URL
        const pathParts = window.location.pathname.split('/');
        const tabIndex = pathParts.indexOf('zones');
        if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
          zoneId = pathParts[tabIndex + 1];
        }
      }
    }
    
    if (!zoneId) {
      alert('Could not determine current zone.');
      return;
    }
    
    try {
      // Get current zone data
      const tabResponse = await fetch(`/zones/${zoneId}`, {
        headers: { Accept: 'application/json' },
      });
      if (!tabResponse.ok) {
        throw new Error('Failed to load zone');
      }
      const tabData = await tabResponse.json();
      
      // Normalize to flat array to check and update usage
      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();
        }
      }

      if (flat.includes(presetId)) {
        alert('Preset is already added to this zone.');
        return;
      }

      flat.push(presetId);
      const newGrid = arrayToGrid(flat, 3);
      tabData.presets = newGrid;
      tabData.presets_flat = flat;
      
      // Update zone
      const updateResponse = await fetch(`/zones/${zoneId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(tabData),
      });
      
      if (!updateResponse.ok) {
        throw new Error('Failed to update zone');
      }
      
      // Reload the zone content to show the new preset
      if (typeof renderTabPresets === 'function') {
        await renderTabPresets(zoneId);
      } else if (window.htmx) {
        htmx.ajax('GET', `/zones/${zoneId}/content-fragment`, {
          target: '#zone-content',
          swap: 'innerHTML'
        });
      } else {
        // Fallback: reload the page
        window.location.reload();
      }
    } catch (error) {
      console.error('Failed to add preset to zone:', error);
      alert('Failed to add preset to zone.');
    }
  };
  try {
    window.addPresetToTab = addPresetToTab;
  } catch (e) {}
  if (presetEditorCloseButton) {
    presetEditorCloseButton.addEventListener('click', closeEditor);
  }
  if (presetPatternInput) {
    presetPatternInput.addEventListener('change', () => {
      updatePresetNLabels(presetPatternInput.value);
      // Update color section visibility
      updateColorSectionVisibility();
      // Re-render colors to show updated max colors limit
      renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
      updateManualModeAvailability();
    });
  }
  if (presetManualModeInput) {
    presetManualModeInput.addEventListener('change', () => {
      updateManualBeatNVisibility();
      updateDelayVisibilityForManualMode();
    });
  }
  if (presetBackgroundButton && presetBackgroundInput) {
    presetBackgroundButton.addEventListener('click', () => {
      presetBackgroundInput.click();
    });
    presetBackgroundInput.addEventListener('input', () => {
      updatePresetBackgroundButton();
    });
  }
  // Color picker auto-add handler
  if (presetNewColorInput) {
    const tryAddSelectedColor = () => {
      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);
      currentPresetPaletteRefs.push(null);
      renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
    };
    // Add when the picker closes (user confirms selection).
    presetNewColorInput.addEventListener('change', tryAddSelectedColor);
  }

  if (presetAddFromPaletteButton) {
    presetAddFromPaletteButton.addEventListener('click', async () => {
      try {
        const paletteColors = await getCurrentProfilePaletteColors();
        if (!Array.isArray(paletteColors) || paletteColors.length === 0) {
          alert('No profile palette colors available.');
          return;
        }

        const modal = document.createElement('div');
        modal.className = 'modal active';
        modal.innerHTML = `
          <div class="modal-content">
            <h2>Pick Palette Color</h2>
            <div id="pick-palette-list" class="profiles-list" style="max-height: 300px; overflow-y: auto;"></div>
            <div class="modal-actions">
              <button class="btn btn-secondary" id="pick-palette-close-btn">Close</button>
            </div>
          </div>
        `;
        document.body.appendChild(modal);

        const list = modal.querySelector('#pick-palette-list');
        paletteColors.forEach((color, idx) => {
          const row = document.createElement('div');
          row.className = 'profiles-row';
          row.style.display = 'flex';
          row.style.alignItems = 'center';
          row.style.gap = '0.75rem';
          row.dataset.paletteIndex = String(idx);
          row.dataset.paletteColor = color;
          row.innerHTML = `
            <div style="width:28px;height:28px;border-radius:4px;border:1px solid #555;background:${color};"></div>
            <span style="flex:1">${color}</span>
            <button class="btn btn-primary btn-small" type="button">Use</button>
          `;
          list.appendChild(row);
        });

        const close = () => modal.remove();
        modal.querySelector('#pick-palette-close-btn').addEventListener('click', close);

        list.addEventListener('click', (e) => {
          const btn = e.target.closest('button');
          if (!btn) return;
          const row = e.target.closest('[data-palette-index]');
          if (!row) return;
          const color = row.dataset.paletteColor;
          const ref = parseInt(row.dataset.paletteIndex, 10);
          if (!color || !Number.isInteger(ref)) return;

          if (currentPresetColors.includes(color) && currentPresetPaletteRefs.includes(ref)) {
            alert('That palette color is already linked.');
            return;
          }
          const maxColors = getMaxColors();
          if (currentPresetColors.length >= maxColors) {
            alert(`This pattern allows a maximum of ${maxColors} color${maxColors !== 1 ? 's' : ''}.`);
            return;
          }

          currentPresetColors.push(color);
          currentPresetPaletteRefs.push(ref);
          renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
          close();
        });
      } catch (err) {
        console.error('Failed to add from palette:', err);
        alert('Failed to load palette colors.');
      }
    });
  }
  const presetSendButton = document.getElementById('preset-send-btn');

  if (presetSendButton) {
    presetSendButton.addEventListener('click', async () => {
      const payload = buildPresetPayload();
      if (!payload.name) {
        alert('Preset name is required to send.');
        return;
      }
      // Send current editor values and then select on all devices in the current zone (if any)
      const section = document.querySelector('.presets-section[data-zone-id]');
      const deviceNames = tabDeviceNamesFromSection(section);
      // Work out the preset ID: for existing presets use currentEditId, otherwise fall back to name
      const presetId = currentEditId || payload.name;
      // Try sends preset first, then select; never persist on device.
      await sendPresetViaEspNow(presetId, payload, deviceNames, false, false, '2');
    });
  }

  if (presetDefaultButton) {
    presetDefaultButton.addEventListener('click', async () => {
      const payload = buildPresetPayload();
      if (!payload.name) {
        alert('Preset name is required.');
        return;
      }
      const section = document.querySelector('.presets-section[data-zone-id]');
      const deviceNames = tabDeviceNamesFromSection(section);
      const presetId = currentEditId || payload.name;
      await sendPresetViaEspNow(presetId, payload, deviceNames, true, true, '1');
      await updateTabDefaultPreset(presetId);
      await sendDefaultPreset('1', deviceNames);
    });
  }

  if (presetRemoveFromTabButton) {
    presetRemoveFromTabButton.addEventListener('click', async () => {
      if (!currentEditTabId || !currentEditId) return;
      if (!window.confirm('Remove this preset from this zone?')) return;
      await removePresetFromTab(currentEditTabId, currentEditId);
      clearForm();
      closeEditor();
    });
  }

  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');
      }

      // Same device targeting as Try: zone tab supplies names and selection without persistence.
      const section = document.querySelector('.presets-section[data-zone-id]');
      const deviceNames = tabDeviceNamesFromSection(section);

      // 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; use the existing ID
          await sendPresetViaEspNow(currentEditId, saved, deviceNames, false, false, '2');
        } else {
          // POST returns { id: preset }
          const entries = Object.entries(saved);
          if (entries.length > 0) {
            const [newId, presetData] = entries[0];
            await sendPresetViaEspNow(newId, presetData, deviceNames, false, false, '2');
          }
        }
      } else {
        // Fallback: send what we just built
        await sendPresetViaEspNow(currentEditId || payload.name, payload, deviceNames, false, false, '2');
      }

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

  // Listen for edit preset events from zone preset buttons
  document.addEventListener('editPreset', async (event) => {
    const { presetId, preset, zoneId } = event.detail;
    currentEditId = presetId;
    currentEditTabId = zoneId || null;
    await loadPatterns();
    const paletteColors = await getCurrentProfilePaletteColors();
    setFormValues({
      ...(preset || {}),
      colors: resolveColorsWithPaletteRefs(
        (preset && preset.colors) || [],
        (preset && preset.palette_refs) || [],
        paletteColors,
      ),
    });
    openEditor();
  });

  clearForm();
});

const coercePresetInt = (v, def = 0) => {
  if (typeof v === 'number' && Number.isFinite(v)) {
    return v;
  }
  const t = parseInt(String(v), 10);
  return Number.isFinite(t) ? t : def;
};

/** Device field ``a`` / API ``auto``; missing → auto-run (matches server build_preset_dict). */
const coercePresetAuto = (preset) => {
  if (!preset || typeof preset !== 'object') {
    return true;
  }
  const v =
    preset.auto !== undefined && preset.auto !== null ? preset.auto : preset.a;
  if (typeof v === 'boolean') {
    return v;
  }
  if (v === 0 || v === '0') {
    return false;
  }
  if (v === 1 || v === '1') {
    return true;
  }
  if (typeof v === 'string') {
    const l = v.trim().toLowerCase();
    if (['false', '0', 'no', 'off'].includes(l)) {
      return false;
    }
    if (['true', '1', 'yes', 'on'].includes(l)) {
      return true;
    }
  }
  return true;
};

/** Preset background colour; accepts #RRGGBB or [r,g,b]. */
const coercePresetBackground = (preset) => {
  if (!preset || typeof preset !== 'object') {
    return '#000000';
  }
  const raw = preset.background !== undefined && preset.background !== null ? preset.background : preset.bg;
  if (typeof raw === 'string') {
    const s = raw.trim();
    if (/^#[0-9a-fA-F]{6}$/.test(s)) {
      return s.toUpperCase();
    }
  }
  if (Array.isArray(raw) && raw.length === 3) {
    const r = coercePresetInt(raw[0], 0);
    const g = coercePresetInt(raw[1], 0);
    const b = coercePresetInt(raw[2], 0);
    const clamp = (n) => Math.max(0, Math.min(255, n));
    return `#${clamp(r).toString(16).padStart(2, '0')}${clamp(g).toString(16).padStart(2, '0')}${clamp(b).toString(16).padStart(2, '0')}`.toUpperCase();
  }
  return '#000000';
};

/** Audio beat stride for manual presets (led-controller only; firmware ignores this key). */
const coerceManualBeatN = (preset) => {
  if (!preset || typeof preset !== 'object') return 1;
  const raw = preset.manual_beat_n;
  let n = typeof raw === 'number' ? raw : parseInt(String(raw != null ? raw : '1'), 10);
  if (!Number.isFinite(n)) n = 1;
  return Math.max(1, Math.min(64, n));
};

// Build driver messages for a single preset; deliver via /presets/push (ESP-NOW + TCP).
// Send order:
// 1) preset payload (optionally with save)
// 2) optional select for device names (never with save)
// saveToDevice defaults to true.
const sendPresetViaEspNow = async (
  presetId,
  preset,
  deviceNames,
  saveToDevice = true,
  setDefault = false,
  devicePresetId = null,
) => {
  try {
    const baseColors = Array.isArray(preset.colors) && preset.colors.length
      ? preset.colors
      : ['#FFFFFF'];
    const paletteColors = await getCurrentProfilePaletteColors();
    const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors);

    const wirePresetId = devicePresetId != null ? String(devicePresetId) : String(presetId);
    const presetAuto = coercePresetAuto(preset);
    const presetBackground = coercePresetBackground(preset);
    const presetMessage = {
      v: '1',
      presets: {
        [wirePresetId]: {
          pattern: preset.pattern || 'off',
          colors,
          bg: presetBackground,
          delay: typeof preset.delay === 'number' ? preset.delay : 100,
          brightness: typeof preset.brightness === 'number'
            ? preset.brightness
            : (typeof preset.br === 'number' ? preset.br : 127),
          auto: presetAuto,
          a: presetAuto,
          n1: coercePresetInt(preset.n1),
          n2: coercePresetInt(preset.n2),
          n3: coercePresetInt(preset.n3),
          n4: coercePresetInt(preset.n4),
          n5: coercePresetInt(preset.n5),
          n6: coercePresetInt(preset.n6),
          manual_beat_n: coerceManualBeatN(preset),
        },
      },
    };
    if (saveToDevice) {
      presetMessage.save = true;
    }
    if (setDefault) {
      presetMessage.default = wirePresetId;
    }

    const names = Array.isArray(deviceNames) ? deviceNames : [];
    const targetMacs =
      names.length > 0 &&
      typeof window.tabsManager !== 'undefined' &&
      typeof window.tabsManager.resolveTabDeviceMacs === 'function'
        ? await window.tabsManager.resolveTabDeviceMacs(names)
        : [];

    const sequence = [presetMessage];
    if (names.length > 0) {
      const select = {};
      names.forEach((name) => {
        if (name) {
          select[name] = [wirePresetId];
        }
      });
      if (Object.keys(select).length > 0) {
        sequence.push({ v: '1', select });
      }
    }

    await postDriverSequence(sequence, targetMacs, 0.05);
  } catch (error) {
    console.error('Failed to send preset to devices:', error);
    alert('Failed to send preset to devices.');
  }
};

const sendDefaultPreset = async (presetId, deviceNames) => {
  if (!presetId) {
    alert('Select a preset to set as default.');
    return;
  }
  const nameTargets = Array.isArray(deviceNames)
    ? deviceNames.map((n) => (n || '').trim()).filter((n) => n.length > 0)
    : [];
  const message = { v: '1', default: presetId };
  message.save = true;
  if (nameTargets.length > 0) {
    message.targets = nameTargets;
  }
  const macTargets =
    nameTargets.length > 0 &&
    typeof window.tabsManager !== 'undefined' &&
    typeof window.tabsManager.resolveTabDeviceMacs === 'function'
      ? await window.tabsManager.resolveTabDeviceMacs(nameTargets)
      : [];
  try {
    await postDriverSequence([message], macTargets);
  } catch (e) {
    console.error('sendDefaultPreset:', e);
    alert('Failed to send default preset to devices.');
  }
};

const sendPresetSelectViaEspNow = async (presetId, deviceNames) => {
  if (!presetId) {
    return;
  }
  const nameTargets = Array.isArray(deviceNames)
    ? deviceNames.map((n) => (n || '').trim()).filter((n) => n.length > 0)
    : [];
  if (!nameTargets.length) {
    return;
  }
  const select = {};
  nameTargets.forEach((name) => {
    select[name] = [String(presetId)];
  });
  const macTargets =
    nameTargets.length > 0 &&
    typeof window.tabsManager !== 'undefined' &&
    typeof window.tabsManager.resolveTabDeviceMacs === 'function'
      ? await window.tabsManager.resolveTabDeviceMacs(nameTargets)
      : [];
  await postDriverSequence([{ v: '1', select }], macTargets);
};

// Expose for other scripts (zones.js) so they can reuse the shared WebSocket.
try {
  window.sendPresetViaEspNow = sendPresetViaEspNow;
  window.postDriverSequence = postDriverSequence;
  // Expose a generic ESPNow sender so other scripts (zones.js) can send
  // non-preset messages such as global brightness.
  window.sendEspnowRaw = sendEspnowMessage;
  window.getEspnowSocket = getEspnowSocket;
} catch (e) {
  // window may not exist in some environments; ignore.
}

// Store selected preset per zone
const selectedPresets = {};
// Store selected preset payload per zone for beat-trigger reliability.
const selectedPresetPayloads = {};
// Run vs Edit for zone preset strip (in-memory only — each full page load starts in run mode)
let presetUiMode = 'run';

const getPresetUiMode = () => (presetUiMode === 'edit' ? 'edit' : 'run');

const setPresetUiMode = (mode) => {
  presetUiMode = mode === 'edit' ? 'edit' : 'run';
};

const updateUiModeToggleButtons = () => {
  const mode = getPresetUiMode();
  // Label is the mode you switch *to* (opposite of current)
  const label = mode === 'edit' ? 'Run mode' : 'Edit mode';
  document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
    btn.textContent = label;
    btn.setAttribute('aria-pressed', mode === 'edit' ? 'true' : 'false');
    btn.classList.toggle('ui-mode-toggle--edit', mode === 'edit');
  });
  document.body.classList.toggle('preset-ui-edit', mode === 'edit');
  document.body.classList.toggle('preset-ui-run', mode === 'run');
};
// Track if we're currently dragging a preset
let isDraggingPreset = false;

// 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 zone
const savePresetGrid = async (zoneId, presetGrid) => {
  try {
    // Get current zone data
    const tabResponse = await fetch(`/zones/${zoneId}`, {
      headers: { Accept: 'application/json' },
    });
    if (!tabResponse.ok) {
      throw new Error('Failed to load zone');
    }
    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 zone
    const updateResponse = await fetch(`/zones/${zoneId}`, {
      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: the cell that contains the cursor (or closest if in a gap)
const getDropTarget = (container, x, y) => {
  const draggableElements = [...container.querySelectorAll('.draggable-preset:not(.dragging)')];
  // First try: find the element whose rect contains the cursor
  const containing = draggableElements.find((child) => {
    const box = child.getBoundingClientRect();
    return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
  });
  if (containing) return containing;
  // Fallback: closest element by distance to center
  const closest = draggableElements.reduce((best, child) => {
    const box = child.getBoundingClientRect();
    const cx = box.left + box.width / 2;
    const cy = box.top + box.height / 2;
    const d = Math.hypot(x - cx, y - cy);
    return d < best.distance ? { distance: d, element: child } : best;
  }, { distance: Infinity });
  return closest.element;
};

/**
 * Move dragged tile onto the drop target's slot.
 * When moving down the list (fromIdx < toIdx), insertBefore(dragging, dropTarget) lands one index
 * too early; use the next element sibling so the item occupies the target slot.
 */
const insertDraggingOntoTarget = (presetsList, dragging, dropTarget) => {
  const siblings = [...presetsList.querySelectorAll('.draggable-preset')];
  const fromIdx = siblings.indexOf(dragging);
  const toIdx = siblings.indexOf(dropTarget);
  if (fromIdx === -1 || toIdx === -1) return;

  if (fromIdx < toIdx) {
    const next = dropTarget.nextElementSibling;
    presetsList.insertBefore(dragging, next);
  } else {
    presetsList.insertBefore(dragging, dropTarget);
  }
};

// Function to render presets for a specific zone in 2D grid
const renderTabPresets = async (zoneId) => {
  const presetsList = document.getElementById('presets-list-zone');
  if (!presetsList) return;
  
  try {
    // Get zone data to see which presets are associated
    const tabResponse = await fetch(`/zones/${zoneId}`, {
      headers: { Accept: 'application/json' },
    });
    if (!tabResponse.ok) {
      throw new Error('Failed to load zone');
    }
    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 allPresetsRaw = await presetsResponse.json();
    const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
    const paletteColors = await getCurrentProfilePaletteColors();
    
    presetsList.innerHTML = '';
    presetsList.dataset.reorderTabId = zoneId;

    // Drag-and-drop on the list (wire once — re-render would duplicate listeners otherwise)
    if (!presetsList.dataset.dragWired) {
      presetsList.dataset.dragWired = '1';
      // dragenter + dropEffect tell the browser this zone accepts a move (avoids ⊘ cursor)
      presetsList.addEventListener('dragenter', (e) => {
        if (getPresetUiMode() !== 'edit') return;
        e.preventDefault();
      });
      presetsList.addEventListener('dragover', (e) => {
        if (getPresetUiMode() !== 'edit') return;
        e.preventDefault();
        try {
          e.dataTransfer.dropEffect = 'move';
        } catch (_) {}
        const dragging = presetsList.querySelector('.dragging');
        if (!dragging) return;

        const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY);
        // Keep dragover side-effect free; commit placement only on drop.
        if (!dropTarget || dropTarget === dragging) {
          delete presetsList.dataset.dropTargetId;
          return;
        }
        presetsList.dataset.dropTargetId = dropTarget.dataset.presetId || '';
      });

      presetsList.addEventListener('drop', async (e) => {
        if (getPresetUiMode() !== 'edit') return;
        e.preventDefault();
        const dragging = presetsList.querySelector('.dragging');
        if (!dragging) return;
        const targetId = presetsList.dataset.dropTargetId;
        if (targetId) {
          const dropTarget = presetsList.querySelector(`.draggable-preset[data-preset-id="${targetId}"]:not(.dragging)`);
          if (dropTarget) {
            insertDraggingOntoTarget(presetsList, dragging, dropTarget);
          }
        }
        delete presetsList.dataset.dropTargetId;

        const saveId = presetsList.dataset.reorderTabId;
        const presetElements = [...presetsList.querySelectorAll('.draggable-preset')];
        const presetIds = presetElements.map((el) => el.dataset.presetId);

        const newGrid = arrayToGrid(presetIds, 3);

        try {
          if (!saveId) {
            console.warn('No zone id for preset reorder save');
            return;
          }
          await savePresetGrid(saveId, newGrid);
          await renderTabPresets(saveId);
        } catch (error) {
          console.error('Failed to save preset grid:', error);
          alert('Failed to save preset order. Please try again.');
          const fallbackId = presetsList.dataset.reorderTabId;
          if (fallbackId) await renderTabPresets(fallbackId);
        }
      });
    }
    
    // Get the currently selected preset for this zone
    const selectedPresetId = selectedPresets[zoneId];
    
    // 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 zone 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 zone. Open the zone\'s Edit menu and click "Add Preset" to add one.';
      presetsList.appendChild(empty);
    } else {
      flatPresets.forEach((presetId) => {
        const preset = allPresets[presetId];
        if (preset) {
          const isSelected = presetId === selectedPresetId;
          const displayPreset = {
            ...preset,
            colors: resolveColorsWithPaletteRefs(preset.colors, preset.palette_refs, paletteColors),
          };
          const wrapper = createPresetButton(presetId, displayPreset, zoneId, isSelected);
          presetsList.appendChild(wrapper);
        }
      });
    }
  } catch (error) {
    console.error('Failed to render zone presets:', error);
    presetsList.innerHTML = '<p class="muted-text">Failed to load presets.</p>';
  }
};

const createPresetButton = (presetId, preset, zoneId, isSelected = false) => {
  const uiMode = getPresetUiMode();

  const row = document.createElement('div');
  const canDrag = uiMode === 'edit';
  row.className = `preset-tile-row preset-tile-row--${uiMode}${canDrag ? ' draggable-preset' : ''}`;
  row.draggable = canDrag;
  row.dataset.presetId = presetId;

  const button = document.createElement('button');
  button.type = 'button';
  button.className = 'pattern-button preset-tile-main';
  if (isSelected) {
    button.classList.add('active');
    selectedPresetPayloads[zoneId] = preset;
  }

  const colors = Array.isArray(preset.colors) ? preset.colors.filter((c) => c) : [];
  const isRainbow = (preset.pattern || '').toLowerCase() === 'rainbow';
  const barColors = isRainbow
    ? ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8F00FF']
    : colors;
  if (barColors.length > 0) {
    const n = barColors.length;
    const stops = barColors.flatMap((c, i) => {
      const start = (100 * i / n).toFixed(2);
      const end = (100 * (i + 1) / n).toFixed(2);
      return [`${c} ${start}%`, `${c} ${end}%`];
    }).join(', ');
    button.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), linear-gradient(to right, ${stops})`;
  }

  const presetNameLabel = document.createElement('span');
  presetNameLabel.textContent = preset.name || presetId;
  presetNameLabel.style.fontWeight = 'bold';
  presetNameLabel.className = 'pattern-button-label';
  button.appendChild(presetNameLabel);

  const bgSwatch = document.createElement('span');
  const bgColor = coercePresetBackground(preset);
  bgSwatch.title = `Background: ${bgColor}`;
  bgSwatch.style.cssText = `
    position: absolute;
    left: 4px;
    bottom: 4px;
    width: 12px;
    height: 12px;
    border-radius: 2px;
    background: ${bgColor};
    border: 1px solid rgba(255, 255, 255, 0.7);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
    pointer-events: none;
    z-index: 2;
  `;
  button.appendChild(bgSwatch);

  const isManualPreset = preset && typeof preset.auto === 'boolean' ? !preset.auto : false;
  if (isManualPreset) {
    const manualBadge = document.createElement('span');
    manualBadge.textContent = '1';
    manualBadge.title = 'Manual preset';
    manualBadge.style.cssText = `
      position: absolute;
      right: 4px;
      bottom: 4px;
      min-width: 16px;
      height: 16px;
      border-radius: 8px;
      background: rgba(0, 0, 0, 0.72);
      color: #fff;
      border: 1px solid rgba(255, 255, 255, 0.5);
      font-size: 11px;
      font-weight: 700;
      line-height: 14px;
      text-align: center;
      pointer-events: none;
      z-index: 2;
    `;
    button.appendChild(manualBadge);
  }

  button.addEventListener('click', () => {
    if (isDraggingPreset) return;
    const presetsListEl = document.getElementById('presets-list-zone');
    if (presetsListEl) {
      presetsListEl.querySelectorAll('.pattern-button').forEach((btn) => btn.classList.remove('active'));
    }
    button.classList.add('active');
    selectedPresets[zoneId] = presetId;
    selectedPresetPayloads[zoneId] = preset;
    const section = row.closest('.presets-section');
    const deviceNames = tabDeviceNamesFromSection(section);
    sendPresetViaEspNow(presetId, preset, deviceNames, false, false, '2').catch((err) => {
      console.error(err);
    });
  });

  if (canDrag) {
    row.addEventListener('dragstart', (e) => {
      isDraggingPreset = true;
      row.classList.add('dragging');
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text/plain', presetId);
    });

    row.addEventListener('dragend', () => {
      row.classList.remove('dragging');
      const presetsListEl = document.getElementById('presets-list-zone');
      if (presetsListEl) {
        delete presetsListEl.dataset.dropTargetId;
      }
      document.querySelectorAll('.draggable-preset').forEach((el) => el.classList.remove('drag-over'));
      setTimeout(() => {
        isDraggingPreset = false;
      }, 100);
    });
  }

  row.appendChild(button);

  if (uiMode === 'edit') {
    const actions = document.createElement('div');
    actions.className = 'preset-tile-actions';

    const editBtn = document.createElement('button');
    editBtn.type = 'button';
    editBtn.className = 'btn btn-secondary btn-small';
    editBtn.textContent = 'Edit';
    editBtn.title = 'Edit preset';
    editBtn.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      if (isDraggingPreset) return;
      editPresetFromTab(presetId, zoneId, preset);
    });

    actions.appendChild(editBtn);
    row.appendChild(actions);
  }

  return row;
};

const editPresetFromTab = async (presetId, zoneId, 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, zoneId }
    });
    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 zone (does not delete the preset itself)
// Expected call style: removePresetFromTab(zoneId, presetId)
const removePresetFromTab = async (zoneId, presetId) => {
  if (!zoneId) {
    // Try to get zone ID from the left-panel
    const leftPanel = document.querySelector('.presets-section[data-zone-id]');
    zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
    
    if (!zoneId) {
      // Fallback: try to get from URL
      const pathParts = window.location.pathname.split('/');
      const tabIndex = pathParts.indexOf('zones');
      if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
        zoneId = pathParts[tabIndex + 1];
      }
    }
  }
  
  if (!zoneId) {
    alert('Could not determine current zone.');
    return;
  }
  
  try {
    // Get current zone data
    const tabResponse = await fetch(`/zones/${zoneId}`, {
      headers: { Accept: 'application/json' },
    });
    if (!tabResponse.ok) {
      throw new Error('Failed to load zone');
    }
    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 zone.');
      return;
    }

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

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

    await renderTabPresets(zoneId);
  } catch (error) {
    console.error('Failed to remove preset from zone:', error);
    alert('Failed to remove preset from zone.');
  }
};
try {
  window.removePresetFromTab = removePresetFromTab;
} catch (e) {}

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

document.addEventListener('DOMContentLoaded', () => {
  updateUiModeToggleButtons();
  document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
    btn.addEventListener('click', () => {
      const next = getPresetUiMode() === 'edit' ? 'run' : 'edit';
      setPresetUiMode(next);
      updateUiModeToggleButtons();
      if (next === 'run') {
        ['devices-modal', 'edit-device-modal'].forEach((id) => {
          const el = document.getElementById(id);
          if (el) el.classList.remove('active');
        });
      }
      const mainMenu = document.getElementById('main-menu-dropdown');
      if (mainMenu) mainMenu.classList.remove('open');
      const leftPanel = document.querySelector('.presets-section[data-zone-id]');
      if (leftPanel) {
        renderTabPresets(leftPanel.dataset.zoneId);
      }
    });
  });
});
