/**
 * @file
 * SSE (Server-Sent Events) client for real-time progress updates.
 */

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

  /**
   * SSE Progress Client for FFmpeg Media.
   *
   * Provides real-time progress updates, eliminating client-side polling.
   */
  Drupal.ffmpegMedia = Drupal.ffmpegMedia || {};

  /**
   * SSE Client class.
   */
  Drupal.ffmpegMedia.SseClient = class SseClient {
    /**
     * Creates an SSE client instance.
     *
     * @param {Object} options - Configuration options.
     * @param {string} options.endpoint - SSE endpoint URL.
     * @param {number} [options.reconnectInterval=3000] - Reconnect delay in ms.
     * @param {number} [options.maxRetries=5] - Maximum reconnection attempts.
     */
    constructor(options) {
      this.endpoint = options.endpoint;
      this.reconnectInterval = options.reconnectInterval || 3000;
      this.maxRetries = options.maxRetries || 5;
      this.retryCount = 0;
      this.eventSource = null;
      this.listeners = new Map();
      this._boundEventTypes = new Set();
      this.lastEventId = null;
      this.isConnected = false;
      this.isManuallyClosed = false;
    }

    /**
     * Connects to the SSE endpoint.
     *
     * @returns {SseClient} This instance for chaining.
     */
    connect() {
      if (this.eventSource) {
        this.disconnect();
      }

      this.isManuallyClosed = false;
      let url = this.endpoint;

      // Add last event ID for resumption.
      if (this.lastEventId) {
        const separator = url.includes('?') ? '&' : '?';
        url += `${separator}lastEventId=${this.lastEventId}`;
      }

      try {
        this.eventSource = new EventSource(url);
        this._setupEventHandlers();
      } catch (error) {
        console.error('[FFmpeg SSE] Failed to create EventSource:', error);
        this._handleError(error);
      }

      return this;
    }

    /**
     * Disconnects from the SSE endpoint.
     */
    disconnect() {
      this.isManuallyClosed = true;
      if (this.eventSource) {
        this.eventSource.close();
        this.eventSource = null;
      }
      this._boundEventTypes.clear();
      this.isConnected = false;
      this._emit('disconnected', { manual: true });
    }

    /**
     * Registers an event listener.
     *
     * @param {string} eventType - Event type to listen for.
     * @param {Function} callback - Callback function.
     * @returns {SseClient} This instance for chaining.
     */
    on(eventType, callback) {
      if (!this.listeners.has(eventType)) {
        this.listeners.set(eventType, new Set());
      }
      this.listeners.get(eventType).add(callback);

      // Register on EventSource if connected and not already bound for
      // this event type. Only one addEventListener per event type is needed
      // since _handleEvent dispatches to all registered callbacks via _emit.
      if (this.eventSource && !this._boundEventTypes.has(eventType) && eventType !== 'connected' && eventType !== 'disconnected' && eventType !== 'error') {
        this._boundEventTypes.add(eventType);
        this.eventSource.addEventListener(eventType, (event) => {
          this._handleEvent(eventType, event);
        });
      }

      return this;
    }

    /**
     * Removes an event listener.
     *
     * @param {string} eventType - Event type.
     * @param {Function} callback - Callback to remove.
     * @returns {SseClient} This instance for chaining.
     */
    off(eventType, callback) {
      if (this.listeners.has(eventType)) {
        this.listeners.get(eventType).delete(callback);
      }
      return this;
    }

    /**
     * Sets up event handlers on the EventSource.
     *
     * Binds onopen/onerror and registers one addEventListener per custom
     * event type. Tracks bound types in _boundEventTypes so that later
     * on() calls do not add duplicate listeners on the same EventSource.
     *
     * @private
     */
    _setupEventHandlers() {
      if (!this.eventSource) return;

      this.eventSource.onopen = () => {
        this.isConnected = true;
        this.retryCount = 0;
        this._emit('connected', { endpoint: this.endpoint });
      };

      this.eventSource.onerror = (error) => {
        this.isConnected = false;
        this._handleError(error);
      };

      // Register existing listeners. One addEventListener per event type
      // is sufficient since _handleEvent dispatches to all callbacks.
      for (const [eventType] of this.listeners) {
        if (eventType !== 'connected' && eventType !== 'disconnected' && eventType !== 'error' && !this._boundEventTypes.has(eventType)) {
          this._boundEventTypes.add(eventType);
          this.eventSource.addEventListener(eventType, (event) => {
            this._handleEvent(eventType, event);
          });
        }
      }
    }

    /**
     * Handles incoming SSE events.
     *
     * @param {string} eventType - Event type.
     * @param {MessageEvent} event - The SSE event.
     * @private
     */
    _handleEvent(eventType, event) {
      if (event.lastEventId) {
        this.lastEventId = event.lastEventId;
      }

      let data;
      try {
        data = JSON.parse(event.data);
      } catch (e) {
        data = event.data;
      }

      this._emit(eventType, data);
    }

    /**
     * Handles connection errors.
     *
     * @param {Event} error - Error event.
     * @private
     */
    _handleError(error) {
      this._emit('error', { error, retryCount: this.retryCount });

      if (!this.isManuallyClosed && this.retryCount < this.maxRetries) {
        this.retryCount++;
        var delay = Math.min(this.reconnectInterval * Math.pow(2, this.retryCount - 1), 30000);
        console.debug(`[FFmpeg SSE] Reconnecting in ${delay}ms (attempt ${this.retryCount}/${this.maxRetries})`);

        setTimeout(() => {
          if (!this.isManuallyClosed) {
            this.connect();
          }
        }, delay);
      } else if (this.retryCount >= this.maxRetries) {
        this._emit('maxRetriesReached', { retries: this.maxRetries });
      }
    }

    /**
     * Emits an event to registered listeners.
     *
     * @param {string} eventType - Event type.
     * @param {*} data - Event data.
     * @private
     */
    _emit(eventType, data) {
      if (this.listeners.has(eventType)) {
        for (const callback of this.listeners.get(eventType)) {
          try {
            callback(data);
          } catch (e) {
            console.error(`[FFmpeg SSE] Error in ${eventType} listener:`, e);
          }
        }
      }
    }
  };

  /**
   * Creates a progress client for a specific media item.
   *
   * @param {number} mediaId - Media entity ID.
   * @param {Object} [options] - Additional options.
   * @returns {SseClient} SSE client instance.
   */
  Drupal.ffmpegMedia.createProgressClient = function (mediaId, options = {}) {
    const basePath = drupalSettings.path?.baseUrl || '/';
    let endpoint = `${basePath}ffmpeg-media/sse/progress/${mediaId}`;
    if (options.fieldName) {
      endpoint += `?field_name=${encodeURIComponent(options.fieldName)}`;
    }

    return new Drupal.ffmpegMedia.SseClient({
      endpoint,
      ...options
    });
  };

  /**
   * Creates a queue progress client.
   *
   * @param {Object} [options] - Additional options.
   * @returns {SseClient} SSE client instance.
   */
  Drupal.ffmpegMedia.createQueueClient = function (options = {}) {
    const basePath = drupalSettings.path?.baseUrl || '/';
    const endpoint = `${basePath}ffmpeg-media/sse/queue`;

    return new Drupal.ffmpegMedia.SseClient({
      endpoint,
      ...options
    });
  };

  /**
   * Creates a dashboard stats client.
   *
   * @param {Object} [options] - Additional options.
   * @returns {SseClient} SSE client instance.
   */
  Drupal.ffmpegMedia.createDashboardClient = function (options = {}) {
    const basePath = drupalSettings.path?.baseUrl || '/';
    const endpoint = `${basePath}ffmpeg-media/sse/dashboard`;

    return new Drupal.ffmpegMedia.SseClient({
      endpoint,
      ...options
    });
  };

  /**
   * Behavior for automatic progress tracking.
   */
  Drupal.behaviors.ffmpegMediaSseProgress = {
    attach: function (context, settings) {
      // Auto-attach to elements with data-ffmpeg-progress attribute.
      once('ffmpeg-sse-progress', '[data-ffmpeg-progress]', context).forEach(function (element) {
        const mediaId = element.dataset.ffmpegProgress;
        if (!mediaId) return;

        const progressBar = element.querySelector('.progress-bar, [role="progressbar"]');
        const statusText = element.querySelector('.ffmpeg-progress-status, .ffmpeg-status-text');
        const percentText = element.querySelector('.ffmpeg-progress-percent');

        const client = Drupal.ffmpegMedia.createProgressClient(mediaId)
          .on('connected', () => {
            element.classList.add('ffmpeg-sse-connected');
            element.classList.remove('ffmpeg-sse-error');
          })
          .on('progress', (data) => {
            if (progressBar) {
              progressBar.style.width = `${data.percent}%`;
              progressBar.setAttribute('aria-valuenow', data.percent);
              progressBar.setAttribute('aria-valuemin', '0');
              progressBar.setAttribute('aria-valuemax', '100');
              progressBar.setAttribute('aria-label',
                Drupal.t('Conversion progress: @percent%', {'@percent': data.percent}));
            }
            if (percentText) {
              percentText.textContent = `${data.percent}%`;
            }
            if (statusText && data.current_step) {
              statusText.textContent = data.current_step;
            }
          })
          .on('complete', (data) => {
            element.classList.add('ffmpeg-conversion-complete');
            if (data.success) {
              element.classList.add('ffmpeg-conversion-success');
            } else {
              element.classList.add('ffmpeg-conversion-failed');
            }
            client.disconnect();
          })
          .on('error', () => {
            element.classList.add('ffmpeg-sse-error');
          })
          .connect();

        // Store client reference for cleanup.
        element.ffmpegSseClient = client;
      });
    },

    detach: function (context, settings, trigger) {
      if (trigger === 'unload') {
        const elements = context.querySelectorAll ? context.querySelectorAll('[data-ffmpeg-progress]') : [];
        elements.forEach(function (element) {
          if (element.ffmpegSseClient) {
            element.ffmpegSseClient.disconnect();
          }
        });
      }
    }
  };

  /**
   * Behavior for dashboard auto-refresh.
   */
  Drupal.behaviors.ffmpegMediaSseDashboard = {
    attach: function (context, settings) {
      if (settings.ffmpegMediaUi?.enableSseDashboard !== true) {
        return;
      }

      once('ffmpeg-sse-dashboard', '[data-ffmpeg-dashboard]', context).forEach(function (element) {
        const queueCount = element.querySelector('.queue-count');
        const activeCount = element.querySelector('.active-count');
        const activeList = element.querySelector('.ffmpeg-active-conversions');
        const liveRegion = element.querySelector('[data-ffmpeg-dashboard-live]');

        const client = Drupal.ffmpegMedia.createQueueClient()
          .on('queue-update', (data) => {
            if (queueCount && data.queue) {
              queueCount.textContent = data.queue.pending || 0;
            }
            if (activeCount && data.active) {
              activeCount.textContent = data.active.length;
            }
            if (activeList && data.active) {
              Drupal.ffmpegMedia.updateActiveList(activeList, data.active);
            }
            if (liveRegion) {
              const pending = data.queue ? (data.queue.pending || 0) : 0;
              const active = data.active ? data.active.length : 0;
              const message = Drupal.t('Queue pending: @pending. Active conversions: @active.', {
                '@pending': pending,
                '@active': active
              });
              if (liveRegion.textContent !== message) {
                liveRegion.textContent = message;
              }
            }
          })
          .connect();

        element.ffmpegSseClient = client;
      });
    },

    detach: function (context, settings, trigger) {
      if (trigger === 'unload') {
        const elements = context.querySelectorAll ? context.querySelectorAll('[data-ffmpeg-dashboard]') : [];
        elements.forEach(function (element) {
          if (element.ffmpegSseClient) {
            element.ffmpegSseClient.disconnect();
          }
        });
      }
    }
  };

  /**
   * Updates the active conversions list.
   *
   * @param {HTMLElement} container - List container.
   * @param {Array} activeItems - Active conversion items.
   */
  Drupal.ffmpegMedia.updateActiveList = function (container, activeItems) {
    if (!container) return;

    // Update existing items or add new ones.
    activeItems.forEach(function (item) {
      let itemElement = container.querySelector(`[data-media-id="${item.media_id}"]`);

      if (!itemElement) {
        itemElement = document.createElement('div');
        itemElement.className = 'ffmpeg-active-conversion-item';
        itemElement.dataset.mediaId = item.media_id;
        var idSpan = document.createElement('span');
        idSpan.className = 'ffmpeg-media-id';
        idSpan.textContent = Drupal.t('Media #@id', {'@id': item.media_id});
        itemElement.appendChild(idSpan);

        var barContainer = document.createElement('div');
        barContainer.className = 'progress-bar-container';
        var bar = document.createElement('div');
        bar.className = 'progress-bar';
        bar.setAttribute('role', 'progressbar');
        bar.style.width = '0%';
        bar.setAttribute('aria-valuenow', '0');
        bar.setAttribute('aria-valuemin', '0');
        bar.setAttribute('aria-valuemax', '100');
        bar.setAttribute('aria-label', Drupal.t('Conversion progress'));
        barContainer.appendChild(bar);
        itemElement.appendChild(barContainer);

        var percentSpan = document.createElement('span');
        percentSpan.className = 'ffmpeg-progress-percent';
        percentSpan.textContent = '0%';
        itemElement.appendChild(percentSpan);

        var stepSpan = document.createElement('span');
        stepSpan.className = 'ffmpeg-current-step';
        itemElement.appendChild(stepSpan);

        container.appendChild(itemElement);
      }

      const bar = itemElement.querySelector('.progress-bar');
      const percent = itemElement.querySelector('.ffmpeg-progress-percent');
      const step = itemElement.querySelector('.ffmpeg-current-step');

      if (bar) {
        bar.style.width = `${item.percent}%`;
        bar.setAttribute('aria-valuenow', item.percent);
        bar.setAttribute('aria-label',
          Drupal.t('Conversion progress: @percent%', {'@percent': item.percent}));
      }
      if (percent) percent.textContent = `${item.percent}%`;
      if (step) step.textContent = item.current_step || '';
    });

    // Remove items that are no longer active.
    const activeIds = new Set(activeItems.map(i => String(i.media_id)));
    container.querySelectorAll('[data-media-id]').forEach(function (el) {
      if (!activeIds.has(el.dataset.mediaId)) {
        el.classList.add('removing');
        setTimeout(() => el.remove(), 300);
      }
    });
  };

})(Drupal, drupalSettings, once);
