/**
 * @file
 * Visual queue editor with drag-and-drop reordering.
 */

(function (Drupal, drupalSettings, once) {
  'use strict';

  Drupal.ffmpegMedia = Drupal.ffmpegMedia || {};

  /**
   * Queue editor class.
   */
  Drupal.ffmpegMedia.QueueEditor = class QueueEditor {
    /**
     * Creates a queue editor instance.
     *
     * @param {HTMLElement} container - Container element.
     * @param {Object} options - Configuration options.
     */
    constructor(container, options = {}) {
      this.container = container;
      this.options = {
        listUrl: options.listUrl || null,
        listLimit: Number.isFinite(Number(options.listLimit)) ? Number(options.listLimit) : 250,
        reorderUrl: options.reorderUrl || null,
        actionUrl: options.actionUrl || null,
        onReorder: options.onReorder || (() => {}),
        onItemAction: options.onItemAction || (() => {}),
        refreshInterval: options.refreshInterval || 5000,
        ...options
      };

      this.items = [];
      this.sseClient = null;
      this._csrfTokenPromise = null;
      this.filterValue = '';
      this.sortBy = 'position';
      this.searchDebounceTimeout = null;
      this._onSearchInput = null;
      this._onSortChange = null;
      this._onRefreshClick = null;
      this._onQueueClick = null;
      this._onQueueChange = null;

      this._init();
    }

    /**
     * Initializes the queue editor.
     *
     * @private
     */
    _init() {
      this._createUI();
      this._bindEvents();
      this._loadQueue();
      this._setupSSE();
    }

    /**
     * Creates the queue editor UI.
     *
     * @private
     */
    _createUI() {
      this.container.classList.add('ffmpeg-queue-editor');
      this.container.innerHTML = `
        <div class="ffmpeg-queue-header">
          <div class="ffmpeg-queue-title">
            <h3>${Drupal.t('Conversion Queue')}</h3>
            <span class="ffmpeg-queue-count"></span>
          </div>
          <div class="ffmpeg-queue-controls">
            <div class="ffmpeg-queue-search">
              <input type="text"
                     class="ffmpeg-queue-search__input"
                     placeholder="${Drupal.t('Search...')}"
                     aria-label="${Drupal.t('Search queue')}">
              <svg class="ffmpeg-queue-search__icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <circle cx="11" cy="11" r="8"/>
                <line x1="21" y1="21" x2="16.65" y2="16.65"/>
              </svg>
            </div>
            <select class="ffmpeg-queue-sort" aria-label="${Drupal.t('Sort by')}">
              <option value="position">${Drupal.t('Position')}</option>
              <option value="created">${Drupal.t('Date added')}</option>
              <option value="priority">${Drupal.t('Priority')}</option>
              <option value="size">${Drupal.t('File size')}</option>
            </select>
            <button type="button" class="ffmpeg-queue-refresh" aria-label="${Drupal.t('Refresh')}">
              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <polyline points="1 4 1 10 7 10"/>
                <polyline points="23 20 23 14 17 14"/>
                <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
              </svg>
            </button>
            <button type="button" class="ffmpeg-queue-keyboard-toggle"
                    aria-expanded="false"
                    aria-controls="ffmpeg-queue-keyboard-help"
                    title="${Drupal.t('Keyboard shortcuts')}">
              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <rect x="2" y="4" width="20" height="16" rx="2" ry="2"/>
                <line x1="6" y1="8" x2="6" y2="8"/>
                <line x1="10" y1="8" x2="10" y2="8"/>
                <line x1="14" y1="8" x2="14" y2="8"/>
                <line x1="18" y1="8" x2="18" y2="8"/>
                <line x1="6" y1="12" x2="18" y2="12"/>
                <line x1="8" y1="16" x2="16" y2="16"/>
              </svg>
              <span class="visually-hidden">${Drupal.t('Keyboard shortcuts')}</span>
            </button>
          </div>
        </div>
        <div class="ffmpeg-queue-notice" hidden></div>
        <div id="ffmpeg-queue-keyboard-help" class="ffmpeg-queue-keyboard-help" hidden>
          ${Drupal.t('Use Alt plus Arrow Up or Alt plus Arrow Down to reorder items.')}
        </div>
        <div id="ffmpeg-queue-search-status" class="visually-hidden" aria-live="polite" role="status"></div>
        <div class="ffmpeg-queue-list" role="listbox" aria-label="${Drupal.t('Queue items')}">
          <div class="ffmpeg-queue-loading">${Drupal.t('Loading queue...')}</div>
        </div>
        <div class="ffmpeg-queue-empty" style="display: none;">
          <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
            <line x1="9" y1="9" x2="15" y2="15"/>
            <line x1="15" y1="9" x2="9" y2="15"/>
          </svg>
          <p>${Drupal.t('Queue is empty')}</p>
        </div>
      `;

      this.queueList = this.container.querySelector('.ffmpeg-queue-list');
      this.queueCount = this.container.querySelector('.ffmpeg-queue-count');
      this.queueEmpty = this.container.querySelector('.ffmpeg-queue-empty');
      this.queueNotice = this.container.querySelector('.ffmpeg-queue-notice');
      this.searchInput = this.container.querySelector('.ffmpeg-queue-search__input');
      this.sortSelect = this.container.querySelector('.ffmpeg-queue-sort');
      this.refreshButton = this.container.querySelector('.ffmpeg-queue-refresh');
    }

    /**
     * Binds event handlers.
     *
     * @private
     */
    _bindEvents() {
      // Search.
      this._onSearchInput = (e) => {
        clearTimeout(this.searchDebounceTimeout);
        this.searchDebounceTimeout = setTimeout(() => {
          this.filterValue = e.target.value.toLowerCase();
          this._renderQueue();
        }, 300);
      };
      this.searchInput.addEventListener('input', this._onSearchInput);

      // Sort.
      this._onSortChange = (e) => {
        this.sortBy = e.target.value;
        this._renderQueue();
      };
      this.sortSelect.addEventListener('change', this._onSortChange);

      // Refresh.
      this._onRefreshClick = () => {
        this._loadQueue();
      };
      if (this.refreshButton) {
        this.refreshButton.addEventListener('click', this._onRefreshClick);
      }

      // Keyboard shortcuts toggle.
      this._keyboardToggle = this.container.querySelector('.ffmpeg-queue-keyboard-toggle');
      this._keyboardHelp = this.container.querySelector('#ffmpeg-queue-keyboard-help');
      if (this._keyboardToggle && this._keyboardHelp) {
        this._onKeyboardToggleClick = () => {
          const expanded = this._keyboardToggle.getAttribute('aria-expanded') === 'true';
          this._keyboardToggle.setAttribute('aria-expanded', String(!expanded));
          this._keyboardHelp.hidden = expanded;
        };
        this._keyboardToggle.addEventListener('click', this._onKeyboardToggleClick);
      }

      // Item actions (delegated).
      this._onQueueClick = (e) => {
        const actionBtn = e.target.closest('[data-action]');
        if (actionBtn) {
          const itemEl = actionBtn.closest('.ffmpeg-queue-item');
          const itemId = itemEl?.dataset.itemId;
          const action = actionBtn.dataset.action;

          // Priority changes are handled on <select> change.
          if (action === 'priority') {
            return;
          }

          if (itemId && action) {
            this._handleItemAction(itemId, action);
          }
        }
      };
      this.queueList.addEventListener('click', this._onQueueClick);

      // Priority updates (delegated).
      this._onQueueChange = (e) => {
        const select = e.target.closest('.ffmpeg-queue-item__priority-select[data-action="priority"]');
        if (!select) {
          return;
        }

        const itemEl = select.closest('.ffmpeg-queue-item');
        const itemId = itemEl?.dataset.itemId;
        const priority = Number.parseInt(select.value, 10);

        if (!itemId || !Number.isInteger(priority)) {
          return;
        }

        this._handleItemAction(itemId, 'priority', { priority });
      };
      this.queueList.addEventListener('change', this._onQueueChange);
    }

    /**
     * Sets up SSE for real-time updates.
     *
     * @private
     */
    _setupSSE() {
      if (!Drupal.ffmpegMedia.createQueueClient) {
        return;
      }

      this.sseClient = Drupal.ffmpegMedia.createQueueClient()
        .on('queue-update', (data) => {
          if (data.queue) {
            this._updateFromSSE(data);
          }
        })
        .on('connected', () => {
          this.container.classList.add('sse-connected');
        })
        .on('error', () => {
          this.container.classList.remove('sse-connected');
        })
        .connect();
    }

    /**
     * Loads queue from server.
     *
     * @private
     */
    async _loadQueue() {
      try {
        this.queueList.innerHTML = `<div class="ffmpeg-queue-loading">${Drupal.t('Loading queue...')}</div>`;

        const response = await fetch(this._buildListRequestUrl(), {
          credentials: 'same-origin',
          headers: {
            'X-Requested-With': 'XMLHttpRequest'
          }
        });

        const data = await this._parseJsonResponse(response);
        if (!response.ok || data?.success === false) {
          throw new Error(data?.error || Drupal.t('Failed to load queue.'));
        }
        this.items = data.items || [];
        this._updateListNotice(data.meta || null);

        this._renderQueue();
      } catch (error) {
        console.error('[FFmpeg Queue]', error);
        this.queueList.innerHTML = `
          <div class="ffmpeg-queue-error">
            ${Drupal.t('Failed to load queue')}
          </div>
        `;
        this._updateListNotice(null);
      }
    }

    /**
     * Updates from SSE data.
     *
     * @param {Object} data - SSE data.
     * @private
     */
    _updateFromSSE(data) {
      // Update active items progress.
      if (data.active) {
        data.active.forEach((activeItem) => {
          const compositeKey = `${activeItem.media_id}:${activeItem.field_name || ''}`;
          const escapedKey = CSS.escape(compositeKey);
          const itemEl = this.queueList.querySelector(`[data-item-id="${escapedKey}"]`);
          if (itemEl) {
            const progressBar = itemEl.querySelector('.ffmpeg-queue-item__progress-bar');
            const progressText = itemEl.querySelector('.ffmpeg-queue-item__progress-text');

            if (progressBar) {
              progressBar.style.width = `${activeItem.percent}%`;
            }
            if (progressText) {
              progressText.textContent = `${activeItem.percent}%`;
            }
          }
        });
      }
    }

    /**
     * Renders the queue.
     *
     * @private
     */
    _renderQueue() {
      let items = [...this.items];

      // Filter.
      if (this.filterValue) {
        items = items.filter((item) =>
          item.name?.toLowerCase().includes(this.filterValue) ||
          item.media_id?.toString().includes(this.filterValue) ||
          item.field_name?.toLowerCase().includes(this.filterValue)
        );
      }

      // Sort.
      items.sort((a, b) => {
        switch (this.sortBy) {
          case 'created':
            return (b.created || 0) - (a.created || 0);
          case 'priority':
            return (b.priority || 0) - (a.priority || 0);
          case 'size':
            return (b.filesize || 0) - (a.filesize || 0);
          default:
            return (a.position || 0) - (b.position || 0);
        }
      });

      // Update count.
      this.queueCount.textContent = `(${items.length})`;

      // Announce filtered results to screen readers.
      const searchStatusEl = this.container.querySelector('#ffmpeg-queue-search-status');
      if (searchStatusEl) {
        searchStatusEl.textContent = Drupal.t('@count items shown', {'@count': items.length});
      }

      // Show empty state or render items.
      if (items.length === 0) {
        this.queueList.innerHTML = '';
        this.queueEmpty.style.display = '';
        return;
      }

      this.queueEmpty.style.display = 'none';

      this.queueList.innerHTML = items.map((item, index) => {
        const itemId = this._escapeHtml(item.id);
        const safeItemId = String(item.id ?? index).replace(/[^a-zA-Z0-9_-]/g, '') || String(index);
        const statusClass = this._normalizeStatus(item.status);
        const statusLabel = this._escapeHtml(item.status || 'pending');
        const progress = this._normalizePercent(item.progress);
        const priorityFieldId = `ffmpeg-queue-priority-${safeItemId}`;
        const pauseLabel = this._escapeHtml(Drupal.t('Pause queue item'));
        const resumeLabel = this._escapeHtml(Drupal.t('Resume queue item'));
        const detailsLabel = this._escapeHtml(Drupal.t('Show queue item details'));
        const removeLabel = this._escapeHtml(Drupal.t('Remove queue item'));

        return `
        <div class="ffmpeg-queue-item ${statusClass === 'processing' ? 'ffmpeg-queue-item--processing' : ''}"
             data-item-id="${itemId}"
             data-media-id="${this._escapeHtml(String(item.media_id))}"
             data-position="${index}"
             draggable="true"
             role="option"
             aria-selected="false"
             aria-describedby="ffmpeg-queue-keyboard-help"
             tabindex="0">
          <div class="ffmpeg-queue-item__drag-handle" aria-hidden="true">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <line x1="8" y1="6" x2="16" y2="6"/>
              <line x1="8" y1="12" x2="16" y2="12"/>
              <line x1="8" y1="18" x2="16" y2="18"/>
            </svg>
          </div>
          <div class="ffmpeg-queue-item__info">
            <div class="ffmpeg-queue-item__name">${this._escapeHtml(item.name || `Media #${item.media_id}`)}</div>
            <div class="ffmpeg-queue-item__meta">
              <span class="ffmpeg-queue-item__size">${this._formatSize(item.filesize || 0)}</span>
              <span class="ffmpeg-queue-item__status ffmpeg-queue-item__status--${statusClass}">${statusLabel}</span>
            </div>
            ${item.status === 'processing' ? `
              <div class="ffmpeg-queue-item__progress">
                <div class="ffmpeg-queue-item__progress-bar" style="width: ${progress}%"></div>
              </div>
              <span class="ffmpeg-queue-item__progress-text">${progress}%</span>
            ` : ''}
          </div>
          <div class="ffmpeg-queue-item__priority">
            <label class="ffmpeg-queue-item__priority-label" for="${priorityFieldId}">${Drupal.t('Priority')}</label>
            <select id="${priorityFieldId}" class="ffmpeg-queue-item__priority-select" data-action="priority">
              <option value="0" ${item.priority === 0 ? 'selected' : ''}>${Drupal.t('Normal')}</option>
              <option value="1" ${item.priority === 1 ? 'selected' : ''}>${Drupal.t('High')}</option>
              <option value="2" ${item.priority === 2 ? 'selected' : ''}>${Drupal.t('Urgent')}</option>
            </select>
          </div>
          <div class="ffmpeg-queue-item__actions">
            ${statusClass === 'paused' ? `
              <button type="button" class="ffmpeg-queue-item__action" data-action="resume" title="${Drupal.t('Resume')}" aria-label="${resumeLabel}">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                  <polygon points="5 3 19 12 5 21 5 3"/>
                </svg>
              </button>
            ` : (statusClass !== 'completed' && statusClass !== 'failed' && statusClass !== 'unknown') ? `
              <button type="button" class="ffmpeg-queue-item__action" data-action="pause" title="${Drupal.t('Pause')}" aria-label="${pauseLabel}">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                  <rect x="6" y="4" width="4" height="16"/>
                  <rect x="14" y="4" width="4" height="16"/>
                </svg>
              </button>
            ` : ''}
            <button type="button" class="ffmpeg-queue-item__action" data-action="details" title="${Drupal.t('Details')}" aria-label="${detailsLabel}">
              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <circle cx="12" cy="12" r="10"/>
                <line x1="12" y1="16" x2="12" y2="12"/>
                <line x1="12" y1="8" x2="12.01" y2="8"/>
              </svg>
            </button>
            <button type="button" class="ffmpeg-queue-item__action ffmpeg-queue-item__action--danger" data-action="remove" title="${Drupal.t('Remove')}" aria-label="${removeLabel}">
              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <line x1="18" y1="6" x2="6" y2="18"/>
                <line x1="6" y1="6" x2="18" y2="18"/>
              </svg>
            </button>
          </div>
        </div>
      `;
      }).join('');

      // Setup drag and drop.
      this._setupDragDrop();
    }

    /**
     * Sets up drag and drop for reordering.
     *
     * @private
     */
    _setupDragDrop() {
      const items = this.queueList.querySelectorAll('.ffmpeg-queue-item');
      let draggedItem = null;

      items.forEach((item) => {
        item.addEventListener('dragstart', (e) => {
          draggedItem = item;
          item.classList.add('ffmpeg-queue-item--dragging');
          e.dataTransfer.effectAllowed = 'move';
        });

        item.addEventListener('dragend', () => {
          item.classList.remove('ffmpeg-queue-item--dragging');
          draggedItem = null;

          // Get new order.
          const newOrder = Array.from(this.queueList.querySelectorAll('.ffmpeg-queue-item'))
            .map((el) => el.dataset.itemId);

          this._saveOrder(newOrder);
          this._announcePosition(item);
        });

        item.addEventListener('dragover', (e) => {
          e.preventDefault();
          e.dataTransfer.dropEffect = 'move';

          if (draggedItem && draggedItem !== item) {
            const rect = item.getBoundingClientRect();
            const midpoint = rect.top + rect.height / 2;

            if (e.clientY < midpoint) {
              item.parentNode.insertBefore(draggedItem, item);
            } else {
              item.parentNode.insertBefore(draggedItem, item.nextSibling);
            }
          }
        });
      });

      // Keyboard reordering (Alt+ArrowUp/Alt+ArrowDown).
      items.forEach((item) => {
        item.addEventListener('keydown', (e) => {
          if (!e.altKey) {
            return;
          }

          if (e.key === 'ArrowUp' && item.previousElementSibling) {
            e.preventDefault();
            item.parentNode.insertBefore(item, item.previousElementSibling);
            item.focus();
            const newOrder = Array.from(this.queueList.querySelectorAll('.ffmpeg-queue-item'))
              .map((el) => el.dataset.itemId);
            this._saveOrder(newOrder);
            this._announcePosition(item);
          }
          else if (e.key === 'ArrowDown' && item.nextElementSibling) {
            e.preventDefault();
            item.parentNode.insertBefore(item, item.nextElementSibling.nextSibling);
            item.focus();
            const newOrder = Array.from(this.queueList.querySelectorAll('.ffmpeg-queue-item'))
              .map((el) => el.dataset.itemId);
            this._saveOrder(newOrder);
            this._announcePosition(item);
          }
        });
      });
    }

    /**
     * Saves new queue order.
     *
     * @param {Array<string>} order - Item IDs in new order.
     * @private
     */
    async _saveOrder(order) {
      try {
        const csrfToken = await this._getCsrfToken();
        const response = await fetch(this._getReorderUrl(), {
          method: 'POST',
          credentials: 'same-origin',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken
          },
          body: JSON.stringify({ order })
        });

        const result = await this._parseJsonResponse(response);
        if (!response.ok || result?.success === false) {
          throw new Error(result?.error || Drupal.t('Failed to save queue order.'));
        }

        this.options.onReorder(order);
      } catch (error) {
        console.error('[FFmpeg Queue] Failed to save order:', error);
      }
    }

    /**
     * Announces item position for screen readers.
     *
     * @param {HTMLElement} item - The moved item.
     * @private
     */
    _announcePosition(item) {
      const statusEl = this.container.querySelector('#ffmpeg-queue-search-status');
      if (statusEl) {
        const pos = Array.from(this.queueList.querySelectorAll('.ffmpeg-queue-item')).indexOf(item) + 1;
        statusEl.textContent = Drupal.t('Item moved to position @pos', {'@pos': pos});
      }
    }

    /**
     * Handles item action.
     *
     * @param {string} itemId - Item ID.
     * @param {string} action - Action name.
     * @private
     */
    async _handleItemAction(itemId, action, payload = {}) {
      if (action === 'details') {
        this._showItemDetails(itemId);
        return;
      }

      if (action === 'remove' && !confirm(Drupal.t('Remove this item from the queue?'))) {
        return;
      }

      try {
        const csrfToken = await this._getCsrfToken();
        const response = await fetch(this._getActionUrl(), {
          method: 'POST',
          credentials: 'same-origin',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken
          },
          body: JSON.stringify({ itemId, action, ...payload })
        });

        const result = await this._parseJsonResponse(response);
        if (!response.ok || result?.success === false) {
          throw new Error(result?.error || Drupal.t('Queue action failed.'));
        }

        this.options.onItemAction(itemId, action, payload);
        await this._loadQueue();
      } catch (error) {
        console.error('[FFmpeg Queue] Action failed:', error);
      }
    }

    /**
     * Shows item details modal.
     *
     * @param {string} itemId - Item ID.
     * @private
     */
    _showItemDetails(itemId) {
      const item = this.items.find((i) => i.id === itemId);
      if (!item) return;

      const dialogIdSafe = `ffmpeg-queue-modal-${String(itemId).replace(/[^a-zA-Z0-9_-]/g, '')}`;
      const titleId = `${dialogIdSafe}-title`;
      const bodyId = `${dialogIdSafe}-body`;
      const statusClass = this._normalizeStatus(item.status);
      const statusLabel = this._escapeHtml(item.status || Drupal.t('pending'));
      const priorityLabels = [Drupal.t('Normal'), Drupal.t('High'), Drupal.t('Urgent')];
      const priorityLabel = priorityLabels[item.priority] || priorityLabels[0];

      const modal = document.createElement('div');
      modal.className = 'ffmpeg-queue-modal';
      modal.innerHTML = `
        <div class="ffmpeg-queue-modal__backdrop"></div>
        <div class="ffmpeg-queue-modal__content" role="dialog" aria-modal="true" aria-labelledby="${titleId}" aria-describedby="${bodyId}" tabindex="-1">
          <div class="ffmpeg-queue-modal__header">
            <h4 id="${titleId}">${Drupal.t('Queue Item Details')}</h4>
            <button type="button" class="ffmpeg-queue-modal__close" aria-label="${Drupal.t('Close')}">
              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <line x1="18" y1="6" x2="6" y2="18"/>
                <line x1="6" y1="6" x2="18" y2="18"/>
              </svg>
            </button>
          </div>
          <div class="ffmpeg-queue-modal__body" id="${bodyId}">
            <dl class="ffmpeg-queue-details">
              <dt>${Drupal.t('Media ID')}</dt>
              <dd>${this._escapeHtml(String(item.media_id))}</dd>
              ${item.field_name ? `
              <dt>${Drupal.t('Field')}</dt>
              <dd>${this._escapeHtml(item.field_name)}</dd>
              ` : ''}
              <dt>${Drupal.t('Name')}</dt>
              <dd>${this._escapeHtml(item.name || 'N/A')}</dd>
              <dt>${Drupal.t('File Size')}</dt>
              <dd>${this._formatSize(item.filesize || 0)}</dd>
              <dt>${Drupal.t('Status')}</dt>
              <dd><span class="ffmpeg-queue-item__status ffmpeg-queue-item__status--${statusClass}">${statusLabel}</span></dd>
              <dt>${Drupal.t('Priority')}</dt>
              <dd>${priorityLabel}</dd>
              <dt>${Drupal.t('Added')}</dt>
              <dd>${item.created ? new Date(item.created * 1000).toLocaleString() : 'N/A'}</dd>
              <dt>${Drupal.t('Profile')}</dt>
              <dd>${this._escapeHtml(item.profile || 'default')}</dd>
            </dl>
          </div>
        </div>
      `;

      document.body.appendChild(modal);
      document.body.classList.add('ffmpeg-queue-modal-open');

      const closeButton = modal.querySelector('.ffmpeg-queue-modal__close');
      const backdrop = modal.querySelector('.ffmpeg-queue-modal__backdrop');
      const dialog = modal.querySelector('.ffmpeg-queue-modal__content');
      const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;

      // Mark all other body children as inert so focus stays within the modal.
      Array.from(document.body.children).forEach((child) => {
        if (child !== modal) {
          child.inert = true;
        }
      });

      const cleanup = () => {
        document.removeEventListener('keydown', onDocumentKeydown);
        if (closeButton) {
          closeButton.removeEventListener('click', close);
        }
        if (backdrop) {
          backdrop.removeEventListener('click', onBackdropClick);
        }
      };

      const close = () => {
        cleanup();

        // Remove inert from body children before removing the modal.
        Array.from(document.body.children).forEach((child) => {
          child.inert = false;
        });

        modal.remove();
        document.body.classList.remove('ffmpeg-queue-modal-open');

        if (previousFocus && document.contains(previousFocus)) {
          previousFocus.focus();
        }
      };

      const onBackdropClick = (event) => {
        if (event.target === backdrop) {
          close();
        }
      };

      const onDocumentKeydown = (event) => {
        if (event.key === 'Escape') {
          event.preventDefault();
          close();
        }
      };

      if (closeButton) {
        closeButton.addEventListener('click', close);
      }
      if (backdrop) {
        backdrop.addEventListener('click', onBackdropClick);
      }
      document.addEventListener('keydown', onDocumentKeydown);

      if (closeButton instanceof HTMLElement) {
        closeButton.focus();
      }
      else if (dialog instanceof HTMLElement) {
        dialog.focus();
      }
    }

    /**
     * Normalizes queue status values for CSS usage.
     *
     * @param {string} status - Status value.
     * @returns {string} Normalized status.
     * @private
     */
    _normalizeStatus(status) {
      const normalized = String(status || '').toLowerCase();
      const allowed = ['pending', 'processing', 'paused', 'failed', 'completed', 'queued'];
      return allowed.includes(normalized) ? normalized : 'unknown';
    }

    /**
     * Normalizes percentage values.
     *
     * @param {number|string} value - Value to normalize.
     * @returns {number} Normalized percentage.
     * @private
     */
    _normalizePercent(value) {
      const numeric = Number(value);
      if (!Number.isFinite(numeric)) {
        return 0;
      }
      return Math.max(0, Math.min(100, Math.round(numeric)));
    }

    /**
     * Formats file size.
     *
     * @param {number} bytes - Size in bytes.
     * @returns {string} Formatted size.
     * @private
     */
    _formatSize(bytes) {
      const units = ['B', 'KB', 'MB', 'GB'];
      let i = 0;
      while (bytes >= 1024 && i < units.length - 1) {
        bytes /= 1024;
        i++;
      }
      return `${bytes.toFixed(1)} ${units[i]}`;
    }

    /**
     * Returns queue list endpoint URL.
     *
     * @returns {string} List URL.
     * @private
     */
    _getListUrl() {
      if (this.options.listUrl) {
        return this.options.listUrl;
      }
      if (drupalSettings.ffmpegMediaQueue?.listUrl) {
        return drupalSettings.ffmpegMediaQueue.listUrl;
      }
      return this._buildFallbackUrl('ffmpeg-media/ajax/queue/list');
    }

    /**
     * Builds queue list URL with defensive server-side limit parameter.
     *
     * @returns {string} Queue list request URL.
     * @private
     */
    _buildListRequestUrl() {
      const listUrl = this._getListUrl();
      let parsedUrl;

      try {
        parsedUrl = new URL(listUrl, window.location.origin);
      }
      catch (error) {
        return listUrl;
      }

      const configuredLimit = Number(this.options.listLimit);
      const normalizedLimit = Number.isFinite(configuredLimit)
        ? Math.max(25, Math.min(1000, Math.trunc(configuredLimit)))
        : 250;

      parsedUrl.searchParams.set('limit', String(normalizedLimit));
      return parsedUrl.toString();
    }

    /**
     * Updates queue notice when backend truncates large queue responses.
     *
     * @param {Object|null} meta - Queue response metadata.
     * @private
     */
    _updateListNotice(meta) {
      if (!this.queueNotice) {
        return;
      }

      const hasMore = Boolean(meta && meta.has_more);
      if (!hasMore) {
        this.queueNotice.hidden = true;
        this.queueNotice.textContent = '';
        return;
      }

      const returned = Number(meta.returned || 0);
      const total = Number(meta.total || 0);
      this.queueNotice.hidden = false;
      this.queueNotice.textContent = Drupal.t(
        'Showing first @shown of @total queued items. Refine filters or raise the queue editor limit in UI settings for full visibility.',
        {
          '@shown': returned,
          '@total': total
        }
      );
    }

    /**
     * Returns queue reorder endpoint URL.
     *
     * @returns {string} Reorder URL.
     * @private
     */
    _getReorderUrl() {
      if (this.options.reorderUrl) {
        return this.options.reorderUrl;
      }
      if (drupalSettings.ffmpegMediaQueue?.reorderUrl) {
        return drupalSettings.ffmpegMediaQueue.reorderUrl;
      }
      return this._buildFallbackUrl('ffmpeg-media/ajax/queue/reorder');
    }

    /**
     * Returns queue action endpoint URL.
     *
     * @returns {string} Action URL.
     * @private
     */
    _getActionUrl() {
      if (this.options.actionUrl) {
        return this.options.actionUrl;
      }
      if (drupalSettings.ffmpegMediaQueue?.actionUrl) {
        return drupalSettings.ffmpegMediaQueue.actionUrl;
      }
      return this._buildFallbackUrl('ffmpeg-media/ajax/queue/action');
    }

    /**
     * Builds a fallback URL relative to Drupal base path.
     *
     * @param {string} relativePath - Relative path without leading slash.
     * @returns {string} Absolute path.
     * @private
     */
    _buildFallbackUrl(relativePath) {
      const basePath = drupalSettings.path?.baseUrl || '/';
      return `${basePath}${relativePath}`.replace(/([^:]\/)\/+/g, '$1');
    }

    /**
     * Resolves a CSRF token for header-protected routes.
     *
     * @returns {Promise<string>} CSRF token.
     * @private
     */
    _getCsrfToken() {
      if (drupalSettings.csrfToken) {
        return Promise.resolve(drupalSettings.csrfToken);
      }

      if (!this._csrfTokenPromise) {
        this._csrfTokenPromise = fetch(this._buildFallbackUrl('session/token'), {
          credentials: 'same-origin',
          headers: { 'X-Requested-With': 'XMLHttpRequest' }
        })
          .then((response) => {
            if (!response.ok) {
              throw new Error(Drupal.t('Failed to fetch CSRF token.'));
            }
            return response.text();
          })
          .then((token) => token.trim());
      }

      return this._csrfTokenPromise;
    }

    /**
     * Parses a JSON response safely.
     *
     * @param {Response} response - Fetch response.
     * @returns {Promise<Object>} Parsed JSON object.
     * @private
     */
    async _parseJsonResponse(response) {
      try {
        return await response.json();
      }
      catch (error) {
        return {
          success: false,
          error: Drupal.t('Unexpected server response.')
        };
      }
    }

    /**
     * Escapes HTML.
     *
     * @param {string} str - String to escape.
     * @returns {string} Escaped string.
     * @private
     */
    _escapeHtml(str) {
      const div = document.createElement('div');
      div.textContent = str;
      return div.innerHTML;
    }

    /**
     * Destroys the queue editor.
     */
    destroy() {
      clearTimeout(this.searchDebounceTimeout);

      if (this.searchInput && this._onSearchInput) {
        this.searchInput.removeEventListener('input', this._onSearchInput);
      }
      if (this.sortSelect && this._onSortChange) {
        this.sortSelect.removeEventListener('change', this._onSortChange);
      }
      if (this.refreshButton && this._onRefreshClick) {
        this.refreshButton.removeEventListener('click', this._onRefreshClick);
      }
      if (this.queueList && this._onQueueClick) {
        this.queueList.removeEventListener('click', this._onQueueClick);
      }
      if (this.queueList && this._onQueueChange) {
        this.queueList.removeEventListener('change', this._onQueueChange);
      }
      if (this._keyboardToggle && this._onKeyboardToggleClick) {
        this._keyboardToggle.removeEventListener('click', this._onKeyboardToggleClick);
      }

      if (this.sseClient) {
        this.sseClient.disconnect();
      }
    }
  };

  /**
   * Drupal behavior.
   */
  Drupal.behaviors.ffmpegMediaQueueEditor = {
    attach: function (context, settings) {
      const queueSettings = settings.ffmpegMediaQueue || {};
      once('ffmpeg-queue-editor', '[data-ffmpeg-queue-editor]', context).forEach(function (element) {
        element.ffmpegQueueEditor = new Drupal.ffmpegMedia.QueueEditor(element, {
          listUrl: queueSettings.listUrl,
          listLimit: queueSettings.listLimit,
          reorderUrl: queueSettings.reorderUrl,
          actionUrl: queueSettings.actionUrl
        });
      });
    },

    detach: function (context, settings, trigger) {
      if (trigger === 'unload') {
        const elements = context.querySelectorAll ? context.querySelectorAll('[data-ffmpeg-queue-editor]') : [];
        elements.forEach(function (el) {
          if (el.ffmpegQueueEditor) {
            el.ffmpegQueueEditor.destroy();
          }
        });
      }
    }
  };

})(Drupal, drupalSettings, once);
