// @ts-nocheck

/**
 * Canonical player-state → display label map.
 * `active` maps to null — no label is shown when the feed is live.
 * @type {Readonly<Record<string, string|null>>}
 */
const PLAYER_STATE_LABELS = Object.freeze({
  hidden: 'Hidden from table',
  'self-muted': 'Camera paused',
  offline: 'Not connected',
  'cam-lost': 'Camera unavailable',
  reconnecting: 'Rejoining view',
  'never-connected': 'Not yet connected',
  ghost: 'Leaving',
  active: null,
});

// ---------------------------------------------------------------------------
// VisibilityDetailsPanel
// ---------------------------------------------------------------------------

/**
 * Native <dialog>-based panel showing full detail about a player's camera state.
 * Not exported — internal to the player/ layer.
 */
class VisibilityDetailsPanel {
  /**
   * @param {object|null} controller - ScryingPoolController (may be null if unavailable)
   */
  constructor(controller) {
    this._controller = controller;
    /** @type {HTMLDialogElement|null} */
    this._dialog = null;
    /** @type {HTMLElement|null} */
    this._triggerEl = null;
  }

  /**
   * Creates and shows the details panel as a modal dialog.
   * @param {string} state - Current player visibility state
   * @param {object|null} actor - User object with .name property (or null)
   * @param {string|null} reason - Reason for state change (e.g., "Hidden by: GM Name")
   * @param {HTMLElement} triggerEl - Element to return focus to on close
   */
  show(state, actor, reason, triggerEl) {
    if (this._dialog) return; // already open

    this._triggerEl = triggerEl;
    const stateLabel = PLAYER_STATE_LABELS[state] ?? state;
    const isHidden = state === 'hidden';
    const isDataStale = !this._controller;

    const dialog = document.createElement('dialog');
    dialog.className = 'sp-visibility-details-panel';
    dialog.setAttribute('aria-modal', 'true');

    // --- State explanation ---
    const stateEl = document.createElement('p');
    stateEl.className = 'sp-visibility-details-panel__state';
    stateEl.textContent = stateLabel;
    dialog.appendChild(stateEl);

    // --- Reason/actor display ---
    if (reason) {
      const reasonEl = document.createElement('p');
      reasonEl.className = 'sp-visibility-details-panel__reason';
      reasonEl.textContent = reason;
      dialog.appendChild(reasonEl);
    } else if (actor?.name) {
      const actorEl = document.createElement('p');
      actorEl.className = 'sp-visibility-details-panel__actor';
      actorEl.textContent = `State changed by: ${actor.name}`;
      dialog.appendChild(actorEl);
    }

    // --- Audience section ---
    if (isHidden) {
      const reassuranceEl = document.createElement('p');
      reassuranceEl.className = 'sp-visibility-details-panel__reassurance';
      reassuranceEl.textContent = 'Other players cannot see your feed';
      dialog.appendChild(reassuranceEl);
    } else {
      const audioEl = document.createElement('p');
      audioEl.className = 'sp-visibility-details-panel__audio-note';
      audioEl.textContent = 'Your audio is active for all participants.';
      dialog.appendChild(audioEl);
    }

    // --- Stale data indicator ---
    if (isDataStale) {
      const staleEl = document.createElement('p');
      staleEl.className = 'sp-visibility-details-panel__stale';
      staleEl.textContent = 'Data may be outdated';
      dialog.appendChild(staleEl);
    }

    // --- Close button ---
    const closeBtn = document.createElement('button');
    closeBtn.className = 'sp-visibility-details-panel__close';
    closeBtn.setAttribute('data-action', 'close-details');
    closeBtn.textContent = 'Close';
    closeBtn.addEventListener('click', () => dialog.close());
    dialog.appendChild(closeBtn);

    // --- Dismiss handlers ---
    // Backdrop click: only when the click target IS the dialog backdrop
    dialog.addEventListener('click', e => {
      if (e.target === dialog) dialog.close();
    });

    // Clean up on close (native Esc + programmatic close)
    dialog.addEventListener('close', () => this._onClose());

    document.body.appendChild(dialog);
    this._dialog = dialog;

    if (typeof dialog.showModal === 'function') {
      dialog.showModal();
    }
  }

  /**
   * Removes the dialog from the DOM and returns focus to the trigger element.
   */
  _onClose() {
    if (this._dialog) {
      this._dialog.remove();
      this._dialog = null;
    }
    this._triggerEl?.focus();
  }
}

// ---------------------------------------------------------------------------
// FirstEncounterPanel
// ---------------------------------------------------------------------------

/**
 * Non-modal explanatory panel shown the first time a player's badge updates.
 * Collapses after a 10s idle timer into a persistent chip.
 * Not exported — internal to the player/ layer.
 */
class FirstEncounterPanel {
  /**
   * @param {Function} setEncounteredFn - async fn that sets the firstBadgeEncounter flag
   * @param {Function} openDetailsFn - fn() that opens VisibilityDetailsPanel
   */
  #collapseTimer = null;

  constructor(setEncounteredFn, openDetailsFn) {
    this._setEncountered = setEncounteredFn;
    this._openDetails = openDetailsFn;
    /** @type {HTMLElement|null} */
    this._panel = null;
    /** @type {HTMLElement|null} */
    this._chip = null;
    /** @type {number} */
    this._remainingMs = 10_000;
    /** @type {number|null} */
    this._timerStartedAt = null;
  }

  /**
   * Creates and shows the explanatory panel.
   * anchorEl is accepted for API consistency but positioning is handled via CSS.
   * @param {HTMLElement} _anchorEl
   */
  // eslint-disable-next-line no-unused-vars
  show(_anchorEl) {
    if (this._panel) return; // already shown

    const panel = document.createElement('div');
    panel.className = 'sp-first-encounter-panel';
    panel.setAttribute('role', 'dialog');
    panel.setAttribute('aria-modal', 'false');
    panel.setAttribute('aria-label', 'Camera visibility explanation');

    const title = document.createElement('p');
    title.className = 'sp-first-encounter-panel__title';
    title.textContent = 'Your camera visibility changed.';
    panel.appendChild(title);

    const body = document.createElement('p');
    body.className = 'sp-first-encounter-panel__body';
    body.textContent = 'Audio continues normally.';
    panel.appendChild(body);

    const gotItBtn = document.createElement('button');
    gotItBtn.className = 'sp-first-encounter-panel__got-it';
    gotItBtn.setAttribute('data-action', 'got-it');
    gotItBtn.textContent = 'Got it';
    gotItBtn.addEventListener('click', async () => {
      await this._onGotIt();
    });
    panel.appendChild(gotItBtn);

    // Timer pause/resume on hover/focus
    panel.addEventListener('mouseenter', () => this._pauseTimer());
    panel.addEventListener('mouseleave', () => this._resumeTimer());
    panel.addEventListener('focusin', () => this._pauseTimer());
    panel.addEventListener('focusout', () => this._resumeTimer());

    document.body.appendChild(panel);
    this._panel = panel;

    this._startTimer();
  }

  /**
   * Starts the collapse timer, recording when it started.
   */
  _startTimer() {
    this._timerStartedAt = Date.now();
    this.#collapseTimer = setTimeout(() => this._collapse(), this._remainingMs);
  }

  /**
   * Pauses the collapse timer, storing remaining time.
   */
  _pauseTimer() {
    if (this.#collapseTimer === null) return;
    const elapsed = Date.now() - (this._timerStartedAt ?? Date.now());
    this._remainingMs = Math.max(0, this._remainingMs - elapsed);
    clearTimeout(this.#collapseTimer);
    this.#collapseTimer = null;
  }

  /**
   * Resumes the collapse timer with remaining time.
   */
  _resumeTimer() {
    if (this.#collapseTimer !== null) return; // already running
    this._timerStartedAt = Date.now();
    this.#collapseTimer = setTimeout(() => this._collapse(), this._remainingMs);
  }

  /**
   * "Got it" handler — clears timer, sets flag, dismisses panel.
   * Uses async to ensure flag is persisted before dismissing.
   */
  async _onGotIt() {
    clearTimeout(this.#collapseTimer); // ghost prevention
    this.#collapseTimer = null;
    try {
      await this._setEncountered();
    } catch (err) {
      console.error('[ScryingPool] Failed to set firstBadgeEncounter flag:', err);
    }
    this._dismiss();
  }

  /**
   * Collapses the panel (via CSS class + 300ms timer matching CSS ease-out) and replaces it with a chip.
   */
  _collapse() {
    // Clear any pending timer from _startTimer before creating new one
    clearTimeout(this.#collapseTimer);
    this.#collapseTimer = null;
    if (!this._panel) return;

    const panel = this._panel;

    // Apply collapse animation via CSS class
    panel.classList.add('sp-first-encounter-panel--collapsing');

    const activeEl = document.activeElement;
    const wasInsidePanel = activeEl ? panel.contains(activeEl) : false;

    // Replace after CSS transition duration (300ms ease-out per AC)
    this.#collapseTimer = setTimeout(() => {
      this.#collapseTimer = null;
      if (!this._panel) return;
      this._panel.remove();
      this._panel = null;

      const chip = this._createChip();
      document.body.appendChild(chip);
      this._chip = chip;

      if (wasInsidePanel) {
        chip.focus();
      }
    }, 300);
  }

  /**
   * Creates the collapsed chip element.
   * @returns {HTMLElement}
   */
  _createChip() {
    const chip = document.createElement('div');
    chip.className = 'sp-visibility-chip';
    chip.setAttribute('role', 'button');
    chip.setAttribute('tabindex', '0');
    chip.setAttribute('aria-label', 'Camera visibility — click for details');

    chip.addEventListener('click', () => this._openDetails());
    chip.addEventListener('keydown', e => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        this._openDetails();
      }
    });

    return chip;
  }

  /**
   * Removes the panel from the DOM without triggering collapse animation.
   */
  _dismiss() {
    if (this._panel) {
      this._panel.remove();
      this._panel = null;
    }
  }

  /**
   * Must be called on teardown — clears timer to prevent ghost timers.
   */
  _onClose() {
    clearTimeout(this.#collapseTimer); // ghost prevention
    this.#collapseTimer = null;
  }

  /**
   * Removes both panel and chip from DOM.
   */
  cleanup() {
    this._onClose();
    if (this._panel) {
      this._panel.remove();
      this._panel = null;
    }
    if (this._chip) {
      this._chip.remove();
      this._chip = null;
    }
  }
}

// ---------------------------------------------------------------------------
// VisibilityBadge
// ---------------------------------------------------------------------------

/**
 * Player-facing camera visibility badge.
 * Mounted on the player's own AV tile via AVTileAdapter.
 * Shows the current visibility state and triggers first-encounter education UI.
 *
 * @class
 */
export class VisibilityBadge {
  /**
   * @param {object} stateStore - StateStore instance
   * @param {object|null} controller - ScryingPoolController (may be null)
   * @param {object} avTileAdapter - AVTileAdapter instance (shared with RoleRenderer)
   * @param {object} adapter - FoundryAdapter instance
   */
  constructor(stateStore, controller, avTileAdapter, adapter) {
    this._stateStore = stateStore;
    this._controller = controller;
    this._avTileAdapter = avTileAdapter;
    this._adapter = adapter;
    /** @type {string|null} */
    this._currentUserId = null;
    /** @type {string} */
    this._currentState = 'active';
    /** @type {object|null} */
    this._currentStateActor = null;
    /** @type {string|null} */
    this._currentStateReason = null;
    /** @type {HTMLElement|null} */
    this._badgeEl = null;
    /** @type {FirstEncounterPanel|null} */
    this._firstEncounterPanel = null;
    /** @type {Function|null} */
    this._stateChangedHandler = null;
  }

  /**
   * Initialises the badge — resolves the current user, subscribes to state changes,
   * and mounts the initial badge element.
   * No-ops if no current user can be resolved.
   */
  init() {
    const currentUser = this._adapter.users.current?.();
    if (!currentUser?.id) {
      this._currentUserId = null;
      return;
    }
    this._currentUserId = currentUser.id;

    // Subscribe to state changes
    this._stateChangedHandler = data => this._onStateChanged(data);
    Hooks.on('scrying-pool:stateChanged', this._stateChangedHandler);

    // Mount initial badge
    const initialState = this._stateStore.getState?.(this._currentUserId) ?? 'active';
    this._currentState = initialState;
    this._badgeEl = this._createBadgeElement(initialState);
    this._avTileAdapter.mount(this._currentUserId, this._badgeEl);

    // Re-mount badge if Foundry re-renders the AV tile
    this._avTileAdapter.onTileRerender(this._currentUserId, () => {
      this._mountBadge(this._currentState);
    });
  }

  /**
   * Creates the badge DOM element for the given state.
   * @param {string} state
   * @returns {HTMLElement}
   */
  _createBadgeElement(state) {
    const stateLabel = PLAYER_STATE_LABELS[state] ?? null;
    const ariaLabel = `Camera visibility: ${stateLabel ?? 'Active'}`;

    const el = document.createElement('div');
    el.className = 'sp-visibility-badge';
    el.dataset.spRole = 'visibility-badge';
    el.setAttribute('role', 'status');
    el.setAttribute('aria-live', 'polite');
    el.setAttribute('aria-label', ariaLabel);

    const labelSpan = document.createElement('span');
    labelSpan.className = 'sp-visibility-badge__label';
    labelSpan.textContent = stateLabel ?? '';
    el.appendChild(labelSpan);

    el.addEventListener('click', () => this._openDetailsPanel(el));

    return el;
  }

  /**
   * Re-mounts the badge at the given state (idempotent via AVTileAdapter).
   * @param {string} state
   */
  _mountBadge(state) {
    if (!this._currentUserId) return;
    this._currentState = state;
    const stateLabel = PLAYER_STATE_LABELS[state] ?? null;
    const ariaLabel = `Camera visibility: ${stateLabel ?? 'Active'}`;

    if (!this._badgeEl) {
      this._badgeEl = this._createBadgeElement(state);
    } else {
      this._badgeEl.setAttribute('aria-label', ariaLabel);
      const labelSpan = this._badgeEl.querySelector('.sp-visibility-badge__label');
      if (labelSpan) {
        labelSpan.textContent = stateLabel ?? '';
      }
    }

    this._avTileAdapter.mount(this._currentUserId, this._badgeEl);
  }

  /**
   * Handles a `scrying-pool:stateChanged` hook event.
   * Guards to only process events for the current user.
   * @param {{ userId: string, state: string, actor?: object, reason?: string }} data
   */
  _onStateChanged(data) {
    // Validate data shape
    if (!data || typeof data !== 'object' || !data.userId || !data.state) {
      return;
    }
    if (data.userId !== this._currentUserId) return;

    this._currentState = data.state;
    this._currentStateActor = data.actor;
    this._currentStateReason = data.reason;
    this._mountBadge(data.state);

    // Trigger first-encounter panel if not yet shown
    if (!this._getFirstBadgeEncountered() && !this._firstEncounterPanel) {
      this._firstEncounterPanel = new FirstEncounterPanel(
        () => this._setFirstBadgeEncountered(),
        () => this._openDetailsPanel(this._badgeEl)
      );
      this._firstEncounterPanel.show(this._badgeEl);
    }
  }

  /**
   * Opens the VisibilityDetailsPanel for the current state.
   * @param {HTMLElement|null} triggerEl - Element to return focus to on close
   */
  _openDetailsPanel(triggerEl) {
    if (!triggerEl) return;
    // Use stored actor/reason from state change, or fall back to current user
    const actor = this._currentStateActor ?? this._adapter.users.current?.() ?? null;
    const panel = new VisibilityDetailsPanel(this._controller);
    panel.show(this._currentState, actor, this._currentStateReason, triggerEl);
  }

  /**
   * Returns whether the player has already seen the first-encounter panel.
   * @returns {boolean}
   */
  _getFirstBadgeEncountered() {
    return this._adapter.users.current()?.getFlag('scrying-pool', 'firstBadgeEncounter') ?? false;
  }

  /**
   * Persists the firstBadgeEncounter flag via Foundry user flags.
   * @returns {Promise<void>}
   */
  async _setFirstBadgeEncountered() {
    await this._adapter.users.current()?.setFlag('scrying-pool', 'firstBadgeEncounter', true);
  }

  /**
   * Tears down badge subscriptions and releases AVTileAdapter observers.
   * Cleans up DOM elements and event listeners.
   */
  teardown() {
    if (this._stateChangedHandler) {
      Hooks.off('scrying-pool:stateChanged', this._stateChangedHandler);
      this._stateChangedHandler = null;
    }
    if (this._firstEncounterPanel) {
      this._firstEncounterPanel.cleanup();
      this._firstEncounterPanel = null;
    }
    if (this._badgeEl) {
      this._badgeEl.remove();
      this._badgeEl = null;
    }
    this._avTileAdapter.disconnect();
    this._currentUserId = null;
    this._currentState = 'active';
  }
}
