/**
 * Override Validation Indicator - floating, draggable button with tooltip
 * - Pulses when there are invalid overrides pending
 * - Hover: shows a compact tooltip summary of changes
 * - Left-click: opens full OverrideValidationDialog
 * - Right-click: accepts all (clears all invalid overrides)
 * - Drag to move; position persists in localStorage
 */


import { COVER_STATES, MODULE_ID, VISIBILITY_STATES } from '../constants.js';

class OverrideValidationIndicator {
  static #instance = null;

  static getInstance() {
    if (!this.#instance) this.#instance = new this();
    return this.#instance;
  }

  constructor() {
    this._el = null;
    this._tooltipEl = null;
    this._data = null; // { overrides: [], tokenName }
    // Keep the raw list passed in (unfiltered) so clearAll can remove overrides even when display filters hide them
    this._rawOverrides = [];
    this._overrideStack = new Map(); // tokenId -> { overrides, tokenName, timestamp }
    this._currentTokenId = null;
    this._overrideStack = new Map(); // tokenId -> { overrides, tokenName, timestamp }
    this._currentTokenId = null;
    this._drag = { active: false, start: { x: 0, y: 0 }, offset: { x: 0, y: 0 }, moved: false };
    // Guard against rapid show->hide flicker when recomputations settle to 0
    this._lastShowAt = 0; // ms timestamp of last show() with non-empty items
    this._lastCount = 0; // last shown count
    this._minVisibleMs = 800; // minimum time to keep visible after a non-empty show
  }

  // Determine whether an override item represents a meaningful change to display
  // We include differences in:
  // - visibility state (excluding 'avs' state)
  // - cover level
  // - concealment expectation vs current (concealed or hidden)
  #hasDisplayChange(o) {
    if (!o) return false;

    // Filter out overrides with no state or 'avs' state
    if (!o.state || o.state === 'avs') {
      return false;
    }

    const prevVis = o.state || (o.hasConcealment ? 'concealed' : 'observed');
    const prevCover = (o.expectedCover ?? (o.hasCover ? 'standard' : 'none'));
    const curVis = o.currentVisibility || 'observed';
    const curCover = o.currentCover || 'none';

    // Filter out 'avs' visibility states from display
    if (prevVis === 'avs' || curVis === 'avs') {
      return false;
    }

    // Only show if there are actual state differences
    return prevVis !== curVis || prevCover !== curCover;
  }

  // Determine whether an override should be shown based on sneak status
  // For tokens with active sneak flag, only show observer changes (where the sneaking token is observing others)
  // and filter out target changes (where others are observing the sneaking token)
  #shouldShowOverride(o) {
    if (!o || !o.observerId || !o.targetId) return true; // If missing data, show by default

    try {
      // Get the observer and target tokens
      const observerToken = canvas?.tokens?.get?.(o.observerId);
      const targetToken = canvas?.tokens?.get?.(o.targetId);

      // Check if observer is sneaking
      const observerIsSneaking = !!observerToken?.document?.getFlag?.('pf2e-visioner', 'sneak-active');

      // Check if target is sneaking  
      const targetIsSneaking = !!targetToken?.document?.getFlag?.('pf2e-visioner', 'sneak-active');

      // If observer is sneaking, only show overrides where they are the observer (what they can see)
      if (observerIsSneaking) {
        return true; // Show observer changes (sneaking token observing others)
      }

      // If target is sneaking, filter out overrides where they are the target (others observing them)
      if (targetIsSneaking) {
        return false; // Hide target changes (others observing sneaking token)
      }

      return true; // Show all other overrides
    } catch {
      return true; // On error, show by default
    }
  }

  show(overrideData, tokenName, movedTokenId = null, options = {}) {
    if (!game.user?.isGM) return;

    this._showOptions = options || {};

    // If dialog is already open, don't show another one (prevents conflicts)
    const existingDialog = Object.values(ui.windows).find(w => w.constructor.name === 'OverrideValidationDialog');
    if (existingDialog) {
      this.#updateTurnChangeBadge();
      return;
    }
    try {
      const avsOnlyInCombat = game.settings.get(MODULE_ID, 'avsOnlyInCombat');
      if (avsOnlyInCombat) {
        const inCombat = !!(game.combat?.started && game.combat?.combatants?.size > 0);
        if (!inCombat) {
          this.hide(true);
          return;
        }
      }
    } catch {
      /* ignore */
    }

    // Keep a raw copy for clearAll regardless of display filters
    const all = Array.isArray(overrideData) ? overrideData : [];
    this._rawOverrides = all;
    // Filter for display: show only entries that actually changed (visibility or cover) and handle sneak filtering
    const overrides = all.filter((o) => this.#hasDisplayChange(o) && this.#shouldShowOverride(o));

    if (movedTokenId && overrides.length > 0) {

      this._overrideStack.set(movedTokenId, {
        overrides: all,
        tokenName,
        timestamp: Date.now()
      });
      this.#showStackedIndicator();
    } else if (this._overrideStack.size > 0) {
      this.#showStackedIndicator();
    } else {
      this.#showDirect(overrides, all, tokenName);
    }
  }

  #showDirect(overrides, rawOverrides, tokenName) {
    if (overrides.length === 0) {
      if (Date.now() - this._lastShowAt < this._minVisibleMs) return;
      this.hide(true);
      return;
    }

    this._rawOverrides = rawOverrides;
    this.#ensureStyles();
    this._data = { overrides, tokenName, movedTokenId: null, pulse: false };
    if (!this._el) this.#createElement();
    this.#updateBadge();
    this.#updateTurnChangeBadge();
    this._el.classList.add('pf2e-visioner-override-indicator--visible');
    this._el.classList.remove('pulse');
    this._lastShowAt = Date.now();
    this._lastCount = overrides.length;
  }

  #showStackedIndicator() {
    // First, clean up entries with no valid overrides
    for (const [tokenId, data] of Array.from(this._overrideStack.entries())) {
      const filtered = data.overrides.filter((o) => this.#hasDisplayChange(o) && this.#shouldShowOverride(o));
      if (filtered.length === 0) {
        this._overrideStack.delete(tokenId);
      }
    }

    const entries = Array.from(this._overrideStack.entries());
    if (entries.length === 0) {
      this.hide(true);
      return;
    }

    const [currentTokenId, currentData] = entries[entries.length - 1];
    const filtered = currentData.overrides.filter((o) => this.#hasDisplayChange(o) && this.#shouldShowOverride(o));

    if (filtered.length === 0) {
      this._overrideStack.delete(currentTokenId);
      this.#showStackedIndicator();
      return;
    }

    this._rawOverrides = currentData.overrides;
    this._currentTokenId = currentTokenId;

    this.#ensureStyles();
    const currentIndex = entries.length;
    const queuePosition = this._overrideStack.size > 1
      ? ` (${game.i18n.format('PF2E_VISIONER.OVERRIDE_INDICATOR.QUEUE_POSITION', { current: currentIndex, total: this._overrideStack.size })})`
      : '';

    // Get next token in queue (second to last) for tooltip display
    const nextEntry = entries.length > 1 ? entries[entries.length - 2] : null;
    const nextTokenName = nextEntry ? nextEntry[1].tokenName : null;

    this._data = { overrides: filtered, tokenName: currentData.tokenName + queuePosition, movedTokenId: currentTokenId, pulse: false, nextTokenName };
    if (!this._el) this.#createElement();
    this.#updateBadge();
    this.#updateQueueBadge();
    this.#updateTurnChangeBadge();
    this._el.classList.add('pf2e-visioner-override-indicator--visible');
    this._el.classList.remove('pulse');
    this._lastShowAt = Date.now();
    this._lastCount = filtered.length;
  }

  #updateBadge() {
    const badge = this._el?.querySelector('.indicator-badge');
    if (!badge) return;
    const raw = this._rawOverrides || [];
    const count = raw.filter((o) => this.#shouldShowOverride(o) && o.state && o.state !== 'avs').length
      || this._data?.overrides?.length || 0;
    badge.textContent = count > 0 ? String(count) : '';
  }

  #updateTurnChangeBadge() {
    const turnChangeBadge = this._el?.querySelector('.indicator-round-change');
    if (!turnChangeBadge) return;
    const isTurnChange = this._showOptions?.isTurnChange || false;
    turnChangeBadge.style.display = isTurnChange ? 'flex' : 'none';
  }

  #updateQueueBadge() {
    const queueContainer = this._el?.querySelector('.indicator-queue');
    if (!queueContainer) return;

    const queueSize = this._overrideStack.size;
    if (queueSize <= 1) {
      queueContainer.innerHTML = '';
      return;
    }

    queueContainer.innerHTML = `
      <div class="queue-badge" data-tooltip="${queueSize} tokens in queue">
        <i class="fas fa-layer-group"></i>
        <span class="queue-count">${queueSize}</span>
      </div>
    `;
  }

  addToStack(tokenId) {
    if (!game.user?.isGM) return;
    const token = canvas.tokens?.get(tokenId);
    if (!token) return;

    if (!this._overrideStack.has(tokenId)) {
      const overrides = this.#getOverridesForToken(tokenId);
      if (overrides.length > 0) {
        this._overrideStack.set(tokenId, {
          overrides,
          tokenName: token.name,
          timestamp: Date.now()
        });
        this.#showStackedIndicator();
      }
    }
  }

  #getOverridesForToken(tokenId) {
    const overrides = [];
    try {
      const token = canvas.tokens?.get(tokenId);
      if (!token) return overrides;

      const flags = token.document?.flags?.['pf2e-visioner'] || {};
      for (const [flagKey, flagData] of Object.entries(flags)) {
        if (!flagKey.startsWith('avs-override-from-')) continue;
        const observerId = flagKey.replace('avs-override-from-', '');
        const observerToken = canvas.tokens?.get(observerId);
        overrides.push({
          observerId,
          targetId: tokenId,
          observerName: flagData.observerName || observerToken?.name || 'Unknown',
          targetName: token.name,
          state: flagData.state,
          hasCover: flagData.hasCover,
          hasConcealment: flagData.hasConcealment,
          expectedCover: flagData.expectedCover,
          source: flagData.source,
        });
      }

      const allTokens = canvas.tokens?.placeables || [];
      for (const t of allTokens) {
        const fk = `avs-override-from-${tokenId}`;
        const fd = t.document?.flags?.['pf2e-visioner']?.[fk];
        if (fd) {
          overrides.push({
            observerId: tokenId,
            targetId: t.id,
            observerName: token.name,
            targetName: t.name,
            state: fd.state,
            hasCover: fd.hasCover,
            hasConcealment: fd.hasConcealment,
            expectedCover: fd.expectedCover,
            source: fd.source,
          });
        }
      }
    } catch { }
    return overrides;
  }

  hide(force = false) {
    if (!this._el) return;
    if (!force && this._lastCount > 0 && Date.now() - this._lastShowAt < this._minVisibleMs) return;

    if (this._currentTokenId) {
      this._overrideStack.delete(this._currentTokenId);
      this._currentTokenId = null;
    }

    if (this._overrideStack.size > 0 && !force) {
      this.#showStackedIndicator();
    } else {
      this._el.classList.remove('pf2e-visioner-override-indicator--visible');
      this._el.classList.remove('pulse');
      this.#hideTooltip();
      this._lastCount = 0;
      this._overrideStack.clear();
    }
  }

  // Public: re-apply computed styles (e.g., after settings change)
  refreshStyles() {
    try {
      this.#ensureStyles();
      // Recompute tooltip contents/colors if visible
      if (this._tooltipEl?.isConnected) this.#renderTooltipContents();
    } catch { /* noop */ }
  }

  update(overrideData, tokenName) {
    if (!game.user?.isGM) return;
    // Ensure latest styles are applied (hot-reload safe)
    this.#ensureStyles();
    const all = Array.isArray(overrideData) ? overrideData : [];
    this._rawOverrides = all;
    // Maintain the same filtering rule for display - including sneak filtering
    const overrides = all.filter((o) => this.#hasDisplayChange(o) && this.#shouldShowOverride(o));
    this._data = { overrides, tokenName };
    this.#updateBadge();
    if (this._tooltipEl?.isConnected) this.#renderTooltipContents();
  }

  async openDialog() {
    if (!game.user?.isGM) return;
    if (!this._rawOverrides?.length) return;
    try {
      const { OverrideValidationDialog } = await import('./OverrideValidationDialog.js');
      // Expose moved token id for grouping via a global scratch, then show dialog
      try { game.pf2eVisioner = game.pf2eVisioner || {}; game.pf2eVisioner.lastMovedTokenId = this._data.movedTokenId || null; } catch { }

      const dialog = new OverrideValidationDialog({
        invalidOverrides: this._rawOverrides,
        tokenName: this._data.tokenName,
        movedTokenId: this._data.movedTokenId || null,
        ...(this._showOptions || {})
      });
      await dialog.render(true);
      // Keep indicator visible; user can minimize dialog back
    } catch (e) {
      console.error('PF2E Visioner | Failed to open OverrideValidationDialog from indicator:', e);
    }
  }

  hasQueuedTokens() {
    return this._overrideStack.size > 0;
  }

  async clearAll() {
    // Accept only the current indicator's overrides, not the entire queue
    const raw = Array.isArray(this._rawOverrides) ? this._rawOverrides : [];
    if (!raw.length) return;
    try {
      const { default: AvsOverrideManager } = await import('../chat/services/infra/AvsOverrideManager.js');
      // Track pairs we clear to immediately recompute their natural AVS states
      const affectedPairs = new Set(); // key: `${observerId}-${targetId}`
      for (const { observerId, targetId } of raw) {
        await AvsOverrideManager.removeOverride(observerId, targetId);
        if (observerId && targetId) affectedPairs.add(`${observerId}-${targetId}`);
      }
      ui.notifications?.info?.(`Accepted ${raw.length} AVS change(s)`);

      // Remove current token from queue and show next
      if (this._currentTokenId) {
        this._overrideStack.delete(this._currentTokenId);
        this._currentTokenId = null;
      }

      if (this._overrideStack.size > 0) {
        this.#showStackedIndicator();
      } else {
        this.hide(true);
      }

      // Immediately recalculate AVS and refresh visuals/perception
      try {
        // 1) For the concrete pairs we just cleared, compute visibility now even if tokens are normally excluded
        try {
          const { optimizedVisibilityCalculator } = await import('../visibility/auto-visibility/index.js');
          const { setVisibilityBetween } = await import('../stores/visibility-map.js');
          for (const key of affectedPairs) {
            const [observerId, targetId] = key.split('-');
            const observer = canvas.tokens?.get?.(observerId);
            const target = canvas.tokens?.get?.(targetId);
            if (!observer || !target) continue;
            try {
              const visOT = await optimizedVisibilityCalculator.calculateVisibility(observer, target);
              await setVisibilityBetween(observer, target, visOT, { isAutomatic: true });
            } catch { /* per-pair best effort */ }
            // Also update reverse direction to keep maps consistent
            try {
              const visTO = await optimizedVisibilityCalculator.calculateVisibility(target, observer);
              await setVisibilityBetween(target, observer, visTO, { isAutomatic: true });
            } catch { /* per-pair best effort */ }
          }
        } catch (pairErr) {
          console.warn('PF2E Visioner | Immediate pair recomputation after override clear failed:', pairErr);
        }

        // 2) Force a full recalculation to settle remaining states without the cleared overrides
        const apiModule = await import('../api.js');
        // Force a full recalculation to settle states without the cleared overrides
        try { await apiModule.autoVisibility.recalculateAll(true); } catch { /* noop */ }
        // Refresh token visuals and client perception
        try { await apiModule.api.updateTokenVisuals(); } catch { /* noop */ }
        try { apiModule.api.refreshEveryonesPerception(); } catch { /* noop */ }
        try { canvas?.perception?.update?.({ refreshVision: true }); } catch { /* noop */ }
      } catch (e) {
        console.warn('PF2E Visioner | Post-clear AVS refresh failed:', e);
      }
    } catch (e) {
      console.error('PF2E Visioner | Failed to clear overrides from indicator:', e);
    }
  }

  async keepAll() {
    if (!this._data?.overrides?.length) return;
    try {
      // Retain overrides and dismiss indicator; dialog handles the semantics of "keep".
      this.hide();
      ui.notifications?.info?.('Rejected all AVS changes');
    } catch (e) {
      console.error('PF2E Visioner | Failed to keep overrides from indicator:', e);
    }
  }

  #createElement() {
    this.#ensureStyles();

    const el = document.createElement('div');
    el.className = 'pf2e-visioner-override-indicator';
    el.innerHTML = `
      <div class="indicator-icon"><i class="fas fa-bolt-auto"></i></div>
      <div class="indicator-badge"></div>
      <div class="indicator-queue"></div>
      <div class="indicator-round-change" data-tooltip="Turn Change"><i class="fas fa-clock"></i></div>
    `;

    // Restore position
    try {
      const saved = localStorage.getItem('pf2e-visioner-override-indicator-pos');
      if (saved) {
        const pos = JSON.parse(saved);
        if (pos?.left) el.style.left = pos.left;
        if (pos?.top) el.style.top = pos.top;
      }
    } catch { }

    // Mouse handlers
    el.addEventListener('mousedown', (ev) => this.#onMouseDown(ev));
    document.addEventListener('mousemove', (ev) => this.#onMouseMove(ev));
    document.addEventListener('mouseup', (ev) => this.#onMouseUp(ev));

    // Hover tooltip with delayed hide so cursor can reach the tooltip
    el.addEventListener('mouseenter', () => {
      clearTimeout(this._tooltipHideTimer);
      this.#showTooltip();
    });
    el.addEventListener('mouseleave', () => {
      this._tooltipHideTimer = setTimeout(() => this.#hideTooltip(), 150);
    });

    // Scroll tooltip content when hovering over indicator
    el.addEventListener('wheel', (e) => {
      if (this._tooltipEl?.isConnected) {
        e.preventDefault();
        const scrollable = this._tooltipEl.querySelector('.tip-content-scrollable');
        if (scrollable) {
          scrollable.scrollTop += e.deltaY;
        }
      }
    }, { passive: false });

    // Clicks
    el.addEventListener('click', async (ev) => {
      if (this._drag.moved) return; // ignore click after drag
      ev.preventDefault();
      ev.stopPropagation();
      await this.openDialog();
    });
    el.addEventListener('contextmenu', async (ev) => {
      ev.preventDefault();
      ev.stopPropagation();
      if (ev.shiftKey) {
        await this.keepAll();
      } else {
        await this.clearAll();
      }
    });

    document.body.appendChild(el);
    this._el = el;
  }

  #onMouseDown(event) {
    if (event.button !== 0) return; // left only for drag
    this._drag.active = true;
    this._drag.moved = false;
    this._drag.start.x = event.clientX;
    this._drag.start.y = event.clientY;
    const rect = this._el.getBoundingClientRect();
    this._drag.offset.x = event.clientX - rect.left;
    this._drag.offset.y = event.clientY - rect.top;
    this._el.classList.add('dragging');
  }

  #onMouseMove(event) {
    if (!this._drag.active) return;
    const dx = event.clientX - this._drag.start.x;
    const dy = event.clientY - this._drag.start.y;
    if (!this._drag.moved && Math.hypot(dx, dy) > 4) this._drag.moved = true;
    if (!this._drag.moved) return;
    const x = event.clientX - this._drag.offset.x;
    const y = event.clientY - this._drag.offset.y;
    const maxX = window.innerWidth - this._el.offsetWidth;
    const maxY = window.innerHeight - this._el.offsetHeight;
    this._el.style.left = Math.max(0, Math.min(x, maxX)) + 'px';
    this._el.style.top = Math.max(0, Math.min(y, maxY)) + 'px';
  }

  #onMouseUp() {
    if (!this._drag.active) return;
    this._drag.active = false;
    this._el.classList.remove('dragging');
    if (this._drag.moved) {
      try {
        localStorage.setItem(
          'pf2e-visioner-override-indicator-pos',
          JSON.stringify({ left: this._el.style.left, top: this._el.style.top })
        );
      } catch { }
      setTimeout(() => (this._drag.moved = false), 50);
    } else {
      this._drag.moved = false;
    }
  }

  #showTooltip() {
    if (!this._data?.overrides?.length) return;
    if (this._tooltipEl?.isConnected) return;
    const tip = document.createElement('div');
    // Tooltip similarly avoids pf2e-visioner to prevent unintended global styling overrides
    tip.className = 'pf2e-visioner-override-tooltip';
    this._tooltipEl = tip;
    this.#renderTooltipContents();

    tip.addEventListener('mouseenter', () => clearTimeout(this._tooltipHideTimer));
    tip.addEventListener('mouseleave', () => {
      this._tooltipHideTimer = setTimeout(() => this.#hideTooltip(), 150);
    });

    document.body.appendChild(tip);
    this.#bindRowContextMenus();
    const rect = this._el.getBoundingClientRect();
    tip.style.left = rect.right + 8 + 'px';
    tip.style.top = Math.max(8, rect.top - 8) + 'px';
  }

  #hideTooltip() {
    if (this._tooltipEl?.parentElement) this._tooltipEl.parentElement.removeChild(this._tooltipEl);
    this._tooltipEl = null;
  }

  #bindRowContextMenus() {
    if (!this._tooltipEl) return;
    const rows = this._tooltipEl.querySelectorAll('.tip-row[data-observer-id][data-target-id]');
    rows.forEach((row) => {
      row.addEventListener('contextmenu', async (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        const observerId = row.dataset.observerId;
        const targetId = row.dataset.targetId;
        if (!observerId || !targetId) return;
        try {
          const { default: AvsOverrideManager } = await import(
            '../chat/services/infra/AvsOverrideManager.js'
          );
          await AvsOverrideManager.removeOverride(observerId, targetId);
          row.style.opacity = '0.3';
          row.style.textDecoration = 'line-through';
          this._rawOverrides = (this._rawOverrides || []).filter(
            (o) => !(o.observerId === observerId && o.targetId === targetId)
          );
          if (this._data?.overrides) {
            this._data.overrides = this._data.overrides.filter(
              (o) => !(o.observerId === observerId && o.targetId === targetId)
            );
          }
          this.#updateBadge();
          if ((this._rawOverrides?.length || 0) === 0) {
            this.hide(true);
          }
        } catch (e) {
          console.warn('PF2E Visioner | Failed to accept row override:', e);
        }
      });
    });
  }

  #renderTooltipContents() {
    if (!this._tooltipEl) return;
    // Render the already-filtered display items to keep counts and grouping consistent
    // The _data.overrides should already be filtered by both #hasDisplayChange and #shouldShowOverride
    const all = this._data?.overrides || [];
    const movedId = this._data?.movedTokenId ?? (globalThis?.game?.pf2eVisioner?.lastMovedTokenId ?? null);

    const mkVis = (key) => {
      if (key === 'avs') return '';
      const cfg = VISIBILITY_STATES?.[key];
      // Filter out 'avs' state from visibility display
      const label = game?.i18n?.localize?.(cfg.label) || cfg.label || '';
      const cls = cfg.cssClass || `visibility-${key}`;
      return `<i class="${cfg.icon} state-indicator ${cls}" data-kind="visibility" data-state="${key}" data-tooltip="${label}"></i>`;
    };
    const mkCover = (key) => {
      const cfg = COVER_STATES?.[key] || { icon: 'fas fa-shield', label: game.i18n.localize('PF2E_VISIONER.TOKEN_MANAGER.COVER_STATE'), cssClass: 'cover-none' };
      const label = game?.i18n?.localize?.(cfg.label) || cfg.label || '';
      const cls = cfg.cssClass || `cover-${key}`;
      return `<i class="${cfg.icon} state-indicator ${cls}" data-kind="cover" data-state="${key}" data-tooltip="${label}"></i>`;
    };

    const buildRow = (o) => {
      // Items that reach here have already been filtered by #hasDisplayChange() and #shouldShowOverride()
      // So we should always display them, even if they don't show state changes
      if (!o) return '';

      const prevVis = o.state || (o.hasConcealment ? 'concealed' : 'observed');
      const prevCover = (o.expectedCover ?? (o.hasCover ? 'standard' : 'none'));
      const curVis = o.currentVisibility || 'observed';
      const curCover = o.currentCover || 'none';

      // Filter out 'avs' states from display but still show the row
      const showVisChange = prevVis !== curVis && prevVis !== 'avs' && curVis !== 'avs';
      const showCoverChange = prevCover !== curCover;

      const reasons = (o.reasonIcons || []).map((r) => `<i class="${r.icon}" data-tooltip="${r.text}"></i>`).join('');
      const isDeferred = /deferred/i.test(o.source || '');
      const deferredBadge = isDeferred ? ` <span style="font-size:0.75em; opacity:0.7;" data-tooltip="Applied from deferred Seek"><i class="fas fa-clock"></i></span>` : '';
      return `
        <div class="tip-row" data-observer-id="${o.observerId || ''}" data-target-id="${o.targetId || ''}" style="cursor:pointer; padding:4px 6px; border-radius:4px; transition:background 0.15s;" onmouseenter="this.style.background='rgba(255,255,255,0.08)'" onmouseleave="this.style.background='transparent'" data-tooltip="${game.i18n.localize('PF2E_VISIONER.OVERRIDE_INDICATOR.ROW_RIGHT_CLICK')}">
          <div class="who">${o.observerName} <i class="fas fa-arrow-right"></i> ${o.targetName}</div>
          ${showVisChange ? `<div class="state-pair vis">${mkVis(prevVis)} <i class="fas fa-arrow-right"></i> ${mkVis(curVis)}${deferredBadge}</div>` : ''}
          ${showCoverChange ? `<div class="state-pair cover">${mkCover(prevCover)} <i class="fas fa-arrow-right"></i> ${mkCover(curCover)}</div>` : ''}
          ${reasons ? `<div class="reasons">${reasons}</div>` : ''}
        </div>
      `;
    };

    // If we know the moved token, split into two groups; otherwise render flat
    let contentHTML = '';
    if (movedId) {
      const asObserver = all.filter((o) => o.observerId === movedId);
      const asTarget = all.filter((o) => o.targetId === movedId);

      const movedTokenName = this._overrideStack.get(movedId)?.tokenName || null;
      const section = (title, arr, groupKey) => arr.length
        ? `
          <div class="tip-group" data-group="${groupKey}">
            <div class="tip-group-header">
              <div class="tip-subheader">${title}${movedTokenName ? ` <span class="moving-token-indicator"><i class="fas fa-person-walking"></i> ${movedTokenName}</span>` : ''}</div>
            </div>
            <div class="tip-group-body">${arr.map(buildRow).join('')}</div>
          </div>
        `
        : '';

      contentHTML = section('Changes as observer', asObserver, 'observer') + section('Changes as target', asTarget, 'target');
      if (!contentHTML) contentHTML = '<div class="tip-empty">No details available</div>';
    } else {
      contentHTML = all.map(buildRow).join('') || '<div class="tip-empty">No details available</div>';
    }

    this._tooltipEl.innerHTML = `
      <div class="tip-header"><i class="fas fa-bolt-auto"></i> ${this._data?.overrides?.length || 0} ${game.i18n.localize('PF2E_VISIONER.OVERRIDE_INDICATOR.TOOLTIP_HEADER')}</div>
      <div class="tip-content-scrollable">
        ${contentHTML}
      </div>
      <div class="tip-footer">
        ${this._data?.nextTokenName ? `<div class="footer-row"><div class="footer-left" style="color: #4a9eff; font-style: italic;"><i class="fas fa-arrow-right"></i> ${game.i18n.localize('PF2E_VISIONER.OVERRIDE_INDICATOR.TOOLTIP_NEXT')} ${this._data.nextTokenName}</div></div>` : ''}
        <div class="footer-row">
          <div class="footer-left">${game.i18n.localize('PF2E_VISIONER.OVERRIDE_INDICATOR.TOOLTIP_LEFT_CLICK')}</div>
          <div class="footer-right">${game.i18n.localize('PF2E_VISIONER.OVERRIDE_INDICATOR.TOOLTIP_RIGHT_CLICK')}</div>
        </div>
        <div class="footer-row">
          <div class="footer-left"></div>
          <div class="footer-right">${game.i18n.localize('PF2E_VISIONER.OVERRIDE_INDICATOR.TOOLTIP_SHIFT_RIGHT_CLICK')}</div>
        </div>
      </div>
    `;

    // After HTML injection, enforce per-state colors inline to defeat any external cascading !important rules.
    this.#applyInlineStateColors();
    this.#bindRowContextMenus();
  }

  #applyInlineStateColors() {
    if (!this._tooltipEl) return;
    let mode = 'none';
    try { mode = game.settings.get('pf2e-visioner', 'colorblindMode') || 'none'; } catch { /* noop */ }

    // Internal explicit per-mode palette (kept consistent with hover-tooltips & cover visualization)
    const palettes = {
      protanopia: {
        visibility: {
          observed: '#0072b2', // blue replacing green
          concealed: '#f0e442', // yellow
          hidden: '#cc79a7', // pink/magenta
          undetected: '#d55e00', // dark orange
        },
        cover: {
          none: '#0072b2',
          lesser: '#f0e442',
          standard: '#cc79a7',
          greater: '#9467bd',
        },
      },
      deuteranopia: {
        visibility: {
          observed: '#0072b2',
          concealed: '#f0e442',
          hidden: '#ff8c00',
          undetected: '#d946ef',
        },
        cover: {
          none: '#0072b2',
          lesser: '#f0e442',
          standard: '#ff8c00',
          greater: '#d946ef',
        },
      },
      tritanopia: {
        visibility: {
          observed: '#00b050',
          concealed: '#ffd700',
          hidden: '#ff6600',
          undetected: '#dc143c',
        },
        cover: {
          none: '#00b050',
          lesser: '#ffd700',
          standard: '#ff6600',
          greater: '#dc143c',
        },
      },
      achromatopsia: {
        visibility: {
          observed: '#ffffff',
          concealed: '#cccccc',
          hidden: '#888888',
          undetected: '#333333',
        },
        cover: {
          none: '#ffffff',
          lesser: '#cccccc',
          standard: '#888888',
          greater: '#333333',
        },
      },
    };

    // Fallback variable-based approach (original) in case mode is none or a future mode not in palettes
    const bodyStyle = getComputedStyle(document.body);
    const variableFallbacks = {
      visibility: {
        observed: ['--visibility-observed', '--visibility-observed-color', '#4caf50'],
        concealed: ['--visibility-concealed', '--visibility-concealed-color', '#ffc107'],
        hidden: ['--visibility-hidden', '--visibility-hidden-color', '#ff9800'],
        undetected: ['--visibility-undetected', '--visibility-undetected-color', '#f44336'],
      },
      cover: {
        none: ['--cover-none', '--cover-none-color', '#4caf50'],
        lesser: ['--cover-lesser', '--cover-lesser-color', '#ffc107'],
        standard: ['--cover-standard', '--cover-standard-color', '#ff6600'],
        greater: ['--cover-greater', '--cover-greater-color', '#f44336'],
      }
    };

    const resolveColor = (kind, state) => {
      // If a supported colorblind mode is active, prefer explicit palette
      if (mode !== 'none' && palettes[mode]?.[kind]?.[state]) {
        return palettes[mode][kind][state];
      }
      // Variable chain fallback
      const chain = variableFallbacks[kind]?.[state] || [];
      for (const v of chain) {
        const val = bodyStyle.getPropertyValue(v).trim();
        if (val) return val;
      }
      return '#4caf50';
    };

    this._tooltipEl.querySelectorAll('.state-indicator').forEach(el => {
      const kind = el.getAttribute('data-kind');
      const state = el.getAttribute('data-state');
      if (!kind || !state) return;
      const color = resolveColor(kind, state);
      // Inline with !important via setProperty priority (not widely supported until CSSOM Level 2, so also append style attribute fallback)
      try { el.style.setProperty('color', color, 'important'); } catch { el.style.color = color; }
    });
  }

  #ensureStyles() {
    const existing = document.getElementById('pf2e-visioner-override-indicator-styles');
    // Read size preference (client setting); default medium
    let size = 'medium';
    try {
      size = game.settings.get('pf2e-visioner', 'avsChangesIndicatorSize') || 'medium';
    } catch { /* setting might not exist yet during early loads */ }

    const presets = {
      small: { box: 34, radius: 8, font: 15, badgeFont: 10, badgePadX: 5, badgePadY: 2, badgeOffset: 5, pulseInset: -5, pulseRadius: 10, pulseBorder: 2, border: 2, tipFont: 11, tipPad: 6 },
      medium: { box: 42, radius: 9, font: 18, badgeFont: 11, badgePadX: 6, badgePadY: 2, badgeOffset: 6, pulseInset: -6, pulseRadius: 12, pulseBorder: 2, border: 2, tipFont: 12, tipPad: 6 },
      large: { box: 52, radius: 10, font: 22, badgeFont: 12, badgePadX: 7, badgePadY: 3, badgeOffset: 7, pulseInset: -7, pulseRadius: 14, pulseBorder: 3, border: 2, tipFont: 13, tipPad: 7 },
      xlarge: { box: 64, radius: 12, font: 26, badgeFont: 13, badgePadX: 8, badgePadY: 4, badgeOffset: 8, pulseInset: -8, pulseRadius: 16, pulseBorder: 3, border: 3, tipFont: 14, tipPad: 8 },
    };
    const p = presets[size] || presets.medium;

    const css = `
      .pf2e-visioner-override-indicator {
        position: fixed; top: 60%; left: 10px; width: ${p.box}px; height: ${p.box}px; background: rgba(20,20,20,0.92); border: ${p.border}px solid var(--pf2e-visioner-warning); border-radius: ${p.radius}px; color: var(--color-text-light-primary, #fff); display: none; align-items: center; justify-content: center; cursor: move; z-index: 1001; box-shadow: 0 2px 12px rgba(0,0,0,0.4); transition: transform .15s ease, box-shadow .15s ease; user-select: none; overflow: visible;
      }
      .pf2e-visioner-override-indicator--visible { display: flex; }
      .pf2e-visioner-override-indicator.dragging { cursor: grabbing; transform: scale(1.06); box-shadow: 0 4px 18px rgba(0,0,0,0.5); }
      .pf2e-visioner-override-indicator .indicator-icon { font-size: ${p.font}px; color: #ffffff; }
      .pf2e-visioner-override-indicator .indicator-badge { position: absolute; top: -8px; right: -6px; background: rgba(244,67,54,0.95); color: #fff; border: 1px solid rgba(244,67,54,1); border-radius: ${p.radius - 2}px; padding: ${p.badgePadY}px ${p.badgePadX}px; font-size: ${p.badgeFont}px; font-weight: 700; line-height: 1; min-width: 18px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
      .pf2e-visioner-override-indicator .indicator-queue { position: absolute; bottom: -6px; right: -6px; }
      .pf2e-visioner-override-indicator .queue-badge { display: flex; align-items: center; gap: 2px; background: rgba(33,150,243,0.95); border: 1px solid rgba(33,150,243,1); border-radius: 8px; padding: 1px 4px; font-size: 9px; font-weight: 700; color: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.4); }
      .pf2e-visioner-override-indicator .queue-badge i { font-size: 8px; }
      .pf2e-visioner-override-indicator .queue-count { line-height: 1; }
      .pf2e-visioner-override-indicator .indicator-round-change { position: absolute; bottom: -6px; left: -6px; display: none; align-items: center; justify-content: center; background: rgba(156,39,176,0.95); border: 1px solid rgba(156,39,176,1); border-radius: 50%; width: 20px; height: 20px; color: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.4); }
      .pf2e-visioner-override-indicator .indicator-round-change i { font-size: 10px; }

  .pf2e-visioner-override-tooltip { position: fixed; min-width: 300px; max-width: 450px; background: rgba(30,30,30,0.98); color: var(--color-text-light-primary, #fff); border: 1px solid var(--color-border-light-primary, #555); border-radius: 8px; z-index: 1002; font-size: ${p.tipFont}px; box-shadow: 0 2px 16px rgba(0,0,0,0.45); backdrop-filter: none !important; -webkit-backdrop-filter: none !important; display: flex; flex-direction: column; max-height: min(80vh, 600px); }
      .pf2e-visioner-override-tooltip .tip-header { padding: ${p.tipPad}px ${p.tipPad}px 6px ${p.tipPad}px; font-weight: 600; color: var(--pf2e-visioner-warning); flex-shrink: 0; }
      .pf2e-visioner-override-tooltip .tip-content-scrollable { overflow-y: auto; overflow-x: hidden; max-height: 150px; min-height: 50px; padding: 0 ${p.tipPad}px; flex: 1 1 auto; }
      .pf2e-visioner-override-tooltip .tip-content-scrollable::-webkit-scrollbar { width: 8px; }
      .pf2e-visioner-override-tooltip .tip-content-scrollable::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 4px; }
      .pf2e-visioner-override-tooltip .tip-content-scrollable::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; }
      .pf2e-visioner-override-tooltip .tip-content-scrollable::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
      .pf2e-visioner-override-tooltip .tip-footer { padding: 6px ${p.tipPad}px ${p.tipPad}px ${p.tipPad}px; flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; }
      .pf2e-visioner-override-tooltip .footer-row { display: flex; justify-content: space-between; align-items: center; white-space: nowrap; }
      .pf2e-visioner-override-tooltip .footer-left { flex: 0 0 auto; }
      .pf2e-visioner-override-tooltip .footer-right { flex: 0 0 auto; text-align: right; }
      .pf2e-visioner-override-tooltip .moving-token-indicator { color: var(--pf2e-visioner-success, #4caf50); font-weight: 600; font-size: 0.9em; margin-left: 8px; }
      .pf2e-visioner-override-tooltip .moving-token-indicator i { margin-right: 4px; }
      /* Force per-state colors inside tooltip (override any external .state-indicator !important rules) */
      .pf2e-visioner-override-tooltip .state-indicator.visibility-observed { color: var(--visibility-observed, var(--visibility-observed-color, #4caf50)) !important; }
      .pf2e-visioner-override-tooltip .state-indicator.visibility-concealed { color: var(--visibility-concealed, var(--visibility-concealed-color, #ffc107)) !important; }
      .pf2e-visioner-override-tooltip .state-indicator.visibility-hidden { color: var(--visibility-hidden, var(--visibility-hidden-color, #ff9800)) !important; }
      .pf2e-visioner-override-tooltip .state-indicator.visibility-undetected { color: var(--visibility-undetected, var(--visibility-undetected-color, #f44336)) !important; }
      .pf2e-visioner-override-tooltip .state-indicator.cover-none { color: var(--cover-none, var(--cover-none-color, #4caf50)) !important; }
      .pf2e-visioner-override-tooltip .state-indicator.cover-lesser { color: var(--cover-lesser, var(--cover-lesser-color, #ffc107)) !important; }
      .pf2e-visioner-override-tooltip .state-indicator.cover-standard { color: var(--cover-standard, var(--cover-standard-color, #ff6600)) !important; }
      .pf2e-visioner-override-tooltip .state-indicator.cover-greater { color: var(--cover-greater, var(--cover-greater-color, #f44336)) !important; }
      /* Normalize cover icon visual size vs visibility */
      .pf2e-visioner-override-tooltip .state-indicator[class*='cover-'] { font-size: 1.08em; }
      .pf2e-visioner-override-tooltip .tip-group { margin-top: 4px; }
      .pf2e-visioner-override-tooltip .tip-group-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-top: 4px; }
      .pf2e-visioner-override-tooltip .tip-subheader { font-weight: 600; color: var(--color-text-dark-secondary, #bbb); }
      .pf2e-visioner-override-tooltip .tip-group-body { margin-top: 2px; }
      .pf2e-visioner-override-tooltip .tip-row { display: grid; grid-template-columns: 1fr auto auto auto; column-gap: 8px; row-gap: 4px; align-items: center; padding: 6px 0; border-top: 1px solid rgba(255,255,255,0.06); }
      .pf2e-visioner-override-tooltip .tip-row:first-of-type { border-top: none; }
      .pf2e-visioner-override-tooltip .who { color: #ddd; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .pf2e-visioner-override-tooltip .state-pair { display: inline-flex; align-items: center; gap: 4px; color: #aaa; }
      /* Add separation between the visibility and cover state groups */
      .pf2e-visioner-override-tooltip .state-pair + .state-pair { margin-left: 10px; }
      .pf2e-visioner-override-tooltip .state-pair i.fas.fa-arrow-right { color: #999; }
      .pf2e-visioner-override-tooltip .state-pair i.state-indicator { margin: 0; }
      /* Tooltip-specific reset: ensure icons remain simple (no background boxes) regardless of global .state-indicator styling */
      .pf2e-visioner-override-tooltip .state-indicator {
        background: transparent !important;
        border: none !important;
        padding: 0 !important;
        box-shadow: none !important;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: auto;
        height: auto;
      }
      .pf2e-visioner-override-tooltip .reasons { display: inline-flex; align-items: center; gap: 4px; color: var(--pf2e-visioner-info, #90caf9); }
  .pf2e-visioner-override-tooltip .reasons i { font-size: ${Math.max(10, p.tipFont - 1)}px; }
      .pf2e-visioner-override-tooltip .tip-empty { color: var(--color-text-dark-secondary, #bbb); padding: 8px 0; }
    `;
    if (existing) {
      existing.textContent = css;
    } else {
      const style = document.createElement('style');
      style.id = 'pf2e-visioner-override-indicator-styles';
      style.textContent = css;
      document.head.appendChild(style);
    }
  }
}

const overrideValidationIndicator = OverrideValidationIndicator.getInstance();
export default overrideValidationIndicator;
export { OverrideValidationIndicator };

