/**
 * ConfirmationBar — Strip-local feedback component for preset apply operations.
 *
 * Owns: Displaying preset apply confirmation, Undo button, auto-dismiss timer.
 * Rendered in StripOverlayLayer at position: absolute; bottom: 0.
 * Supports instant-replace rule (zero crossfade for consecutive bars).
 *
 * Import rule: may import from src/core/, src/contracts/, src/utils/ ONLY.
 * Constructors are side-effect free — call init() from module.js Hooks.once('ready').
 *
 * Story 3.2: Scene Auto-Apply & ConfirmationBar
 *
 * @module ui/gm/ConfirmationBar
 */

/**
 * Strip-local feedback bar for preset apply operations.
 * Provides immediate visual confirmation with one-click Undo.
 */
export class ConfirmationBar {
  /**
   * @param {import('../../foundry/FoundryAdapter.js').FoundryAdapter} adapter
   *   Injected FoundryAdapter surface.
   * @param {import('../../core/VisibilityManager.js').VisibilityManager} visibilityManager
   *   Injected VisibilityManager for reverting matrix on undo.
   * @param {import('../../core/SocketHandler.js').SocketHandler} socketHandler
   *   Injected SocketHandler (for potential future use).
   * @param {import('./ScryingPoolStrip.js').ScryingPoolStrip|import('../shared/StripOverlayLayer.js').StripOverlayLayer} stripOverlayLayerOrStrip
   *   Injected ScryingPoolStrip (which provides stripOverlayLayer) or StripOverlayLayer directly.
   */
  constructor(adapter, visibilityManager, socketHandler, stripOverlayLayerOrStrip) {
    this._adapter = adapter;
    this._visibilityManager = visibilityManager;
    this._socketHandler = socketHandler;
    
    // Extract StripOverlayLayer from the provided source
    // Can be: ScryingPoolStrip, RoleRenderer (with .strip), or StripOverlayLayer directly
    let source = stripOverlayLayerOrStrip;
    
    // If it's a RoleRenderer, get the strip from it
    if (source && source.strip && source.strip?.stripOverlayLayer) {
      source = source.strip;
    }
    
    // If it's a ScryingPoolStrip, get its stripOverlayLayer
    if (source && source.stripOverlayLayer) {
      this._stripOverlayLayer = source.stripOverlayLayer;
    } else {
      // Direct StripOverlayLayer
      this._stripOverlayLayer = source;
    }

    // State
    /** @type {object|null} */
    this._previousMatrix = null;
    /** @type {object|null} */
    this._lastPayload = null;
    /** @type {number|null} */
    this._dismissTimer = null;
    /** @type {boolean} */
    this._isVisible = false;
    /** @type {number} */
    this._lastAppliedTimestamp = 0;
    /** @type {number} */
    this._recentApplyCount = 0;
    /** @type {boolean} */
    this._hookRegistered = false;
    /** @type {Function} */
    this._hookHandler = this._onPresetApplied.bind(this);
    /** @type {Function|null} */
    this._clickHandler = null;

    // Constants
    /** @type {number} Default bar duration in ms */
    this._DEFAULT_DURATION = 8000;
    /** @type {number} Short duration when >=2 applies within 60s */
    this._SHORT_DURATION = 4000;
    /** @type {number} Recent apply window in ms */
    this._RECENT_WINDOW_MS = 60000;
  }

  /**
   * Initializes the ConfirmationBar by registering hook listeners.
   * Side-effect: registers listener for scrying-pool:presetApplied hook.
   * Idempotent - safe to call multiple times.
   */
  init() {
    // Only register hook if not already registered
    if (!this._hookRegistered) {
      this._adapter.hooks.on('scrying-pool:presetApplied', this._hookHandler);
      this._hookRegistered = true;
    }
    this._setupEventListeners();
  }

  /**
   * Sets up DOM event listeners for the confirmation bar.
   * Uses event delegation via StripOverlayLayer's element.
   * @private
   */
  _setupEventListeners() {
    if (!this._stripOverlayLayer || !this._stripOverlayLayer.element) {
      return;
    }

    const element = this._stripOverlayLayer.element;
    
    // Store bound handler
    this._clickHandler = (event) => {
      const target = event.target.closest?.('[data-action="confirmation-bar-undo"]');
      if (target) {
        event.preventDefault();
        event.stopPropagation();
        this._onUndo();
      }
    };
    
    // Use event delegation for undo button clicks
    element.addEventListener('click', this._clickHandler);
  }

  /**
   * Cleans up internal state and unregisters listeners.
   * Safe to call multiple times.
   */
  teardown() {
    this._adapter.hooks.off('scrying-pool:presetApplied', this._hookHandler);
    this._hookRegistered = false;
    this._removeEventListeners();
    this._clearDismissTimer();
    this._previousMatrix = null;
    this._lastPayload = null;
    this._isVisible = false;
    this._recentApplyCount = 0;
  }

  /**
   * Removes DOM event listeners.
   * @private
   */
  _removeEventListeners() {
    if (!this._stripOverlayLayer || !this._stripOverlayLayer.element || !this._clickHandler) {
      return;
    }

    const element = this._stripOverlayLayer.element;
    element.removeEventListener('click', this._clickHandler);
    this._clickHandler = null;
  }

  /**
   * Shows the confirmation bar with the given payload.
   * Captures previous matrix for undo, renders bar, starts dismiss timer.
   *
   * @param {object} payload - The preset applied payload.
   * @param {string} payload.presetName - Name of the applied preset.
   * @param {object} payload.matrix - The visibility matrix that was applied.
   * @param {boolean} payload.autoApplied - Whether this was an auto-apply.
   * @param {boolean} [payload.partialFail] - Whether some participants failed to update.
   * @param {number} [payload.timestamp] - When the preset was applied.
   */
  show(payload) {
    if (!payload) {
      return;
    }

    // Store previous matrix for undo
    if (payload.matrix) {
      this._previousMatrix = payload.matrix;
    }
    this._lastPayload = payload;
    this._lastAppliedTimestamp = payload.timestamp ?? Date.now();

    // Track recent applies for short duration logic
    const now = Date.now();
    if (now - this._lastAppliedTimestamp < this._RECENT_WINDOW_MS) {
      this._recentApplyCount++;
    } else {
      this._recentApplyCount = 1;
    }

    // Render the bar
    this._render();

    // Start dismiss timer
    this._startDismissTimer();
  }

  /**
   * Hides the confirmation bar.
   * Clears timer, previous matrix, and updates visibility state.
   */
  hide() {
    this._clearDismissTimer();
    this._previousMatrix = null;
    this._lastPayload = null;
    this._isVisible = false;
    this._recentApplyCount = 0;
  }

  /**
   * Handles the Undo button click.
   * Reverts to previous matrix and hides the bar.
   * Emits hook for undo notification.
   * @private
   */
  _onUndo() {
    if (!this._previousMatrix || !this._visibilityManager) {
      return;
    }

    try {
      this._visibilityManager.applyMatrix(this._previousMatrix);
      
      // Emit undo notification
      this._adapter.hooks.callAll('scrying-pool:presetUndo', {
        presetName: this._lastPayload?.presetName ?? 'unknown',
        timestamp: Date.now(),
      });
    } catch (err) {
      console.error(
        '[ScryingPool] ConfirmationBar: failed to undo preset apply',
        err instanceof Error ? err.message : String(err)
      );
    } finally {
      this.hide();
    }
  }

  /**
   * Renders the confirmation bar in the strip overlay.
   * Calculates hidden/visible counts from matrix.
   * Determines variant (default or amber for partial fail).
   * @private
   */
  _render() {
    if (!this._lastPayload || !this._stripOverlayLayer) {
      return;
    }

    const { presetName, partialFail, matrix } = this._lastPayload;
    const counts = this._calculateCounts(matrix);
    const variant = partialFail ? 'amber' : 'default';
    const message = this._buildMessage(presetName, counts, variant);

    // Build HTML content
    const html = this._buildHtml(message, variant);

    // Render via strip overlay
    this._stripOverlayLayer.render(html);
    this._isVisible = true;
  }

  /**
   * Calculates hidden and visible counts from a visibility matrix.
   * Only counts states that affect visibility (active, hidden).
   * @param {object} matrix - The visibility matrix.
   * @returns {object} Hidden and visible counts.
   * @private
   */
  _calculateCounts(matrix) {
    if (!matrix || !matrix.matrix) {
      return { hidden: 0, visible: 0 };
    }

    let hidden = 0;
    let visible = 0;

    for (const [, state] of Object.entries(matrix.matrix)) {
      if (state === 'hidden') {
        hidden++;
      } else if (state === 'active') {
        visible++;
      }
      // Other states (self-muted, offline, etc.) are not counted
      // as they don't affect the "hidden from table" status
    }

    return { hidden, visible };
  }

  /**
   * Builds the display message for the confirmation bar.
   * @param {string} presetName - Name of the preset.
   * @param {object} counts - Hidden and visible counts.
   * @param {string} variant - 'default' or 'amber'.
   * @returns {string} The formatted message.
   * @private
   */
  _buildMessage(presetName, counts, variant) {
    const baseMsg = this._adapter.i18n.localize('scrying-pool.presets.confirmation.applied')
      .replace('{name}', presetName);
    const countMsg = this._adapter.i18n.localize('scrying-pool.presets.confirmation.counts')
      .replace('{hidden}', counts.hidden)
      .replace('{visible}', counts.visible);

    if (variant === 'amber') {
      const suffix = this._adapter.i18n.localize('scrying-pool.presets.confirmation.partial-fail');
      return `${baseMsg} ${countMsg} ${suffix}`;
    }

    return `${baseMsg} ${countMsg}`;
  }

  /**
   * Builds the HTML content for the confirmation bar.
   * Includes message, undo button, accessibility attributes.
   * Uses data-action pattern for event delegation.
   * @param {string} message - The display message.
   * @param {string} variant - 'default' or 'amber'.
   * @returns {string} HTML string.
   * @private
   */
  _buildHtml(message, variant) {
    const variantClass = variant === 'amber' ? 'sp-confirmation-bar--amber' : 'sp-confirmation-bar--default';
    const undoLabel = this._adapter.i18n.localize('scrying-pool.presets.confirmation.undo');

    // Use data-action for event delegation via StripOverlayLayer
    // The onclick handler is set up in _setupEventListeners
    return `
<div class="scrying-pool__confirmation-bar ${variantClass}" 
     role="status" 
     aria-live="polite"
     aria-label="${message}. ${undoLabel}">
  <span class="sp-confirmation-bar__message">${this._escapeHtml(message)}</span>
  <button class="sp-confirmation-bar__undo-btn"
          role="button"
          aria-label="${undoLabel}"
          data-action="confirmation-bar-undo">
    ${undoLabel}
  </button>
</div>
    `;
  }

  /**
   * Escapes HTML special characters to prevent XSS.
   * @param {string} str - String to escape.
   * @returns {string} Escaped string.
   * @private
   */
  _escapeHtml(str) {
    if (!str || typeof str !== 'string') {
      return '';
    }
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;');
  }

  /**
   * Starts the auto-dismiss timer.
   * Uses short duration (4000ms) if >=2 applies within 60s window.
   * Otherwise uses default duration (8000ms).
   * @private
   */
  _startDismissTimer() {
    // Clear any existing timer
    this._clearDismissTimer();

    // Determine duration based on recent activity
    const now = Date.now();
    const useShort = this._recentApplyCount >= 2 && 
      (now - this._lastAppliedTimestamp) < this._RECENT_WINDOW_MS;
    const duration = useShort ? this._SHORT_DURATION : this._DEFAULT_DURATION;

    this._dismissTimer = setTimeout(() => {
      this.hide();
    }, duration);
  }

  /**
   * Clears the active dismiss timer.
   * @private
   */
  _clearDismissTimer() {
    if (this._dismissTimer) {
      clearTimeout(this._dismissTimer);
      this._dismissTimer = null;
    }
  }

  /**
   * Hook handler for scrying-pool:presetApplied events.
   * Shows the bar, implementing instant-replace rule.
   * @param {object} payload - The preset applied payload.
   * @private
   */
  _onPresetApplied(payload) {
    // Instant-replace: just show again, no crossfade
    this.show(payload);
  }
}
