import React, { useEffect, useSyncExternalStore } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Play } from 'lucide-react';
import type { TFunction } from 'i18next';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { usePlayerStore } from '@/features/playback/store/playerStore';
import { useLuckyMixStore } from '@/features/randomMix';
import type { QueueItemRef } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
import type { QueueDisplayMode } from '@/store/authStoreTypes';
import { formatTrackTime } from '@/lib/format/formatDuration';
import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
import {
  getQueueResolverVersion,
  subscribeQueueResolver,
  resolveBatch,
} from '@/features/playback/store/queueTrackResolver';
import { collectQueueResolveRefs } from '@/features/queue/utils/collectQueueResolveRefs';
import { findQueueItemRefIndex } from '@/features/playback/utils/playback/queueIdentity';
import type { TimelineDisplayRow } from '@/features/playback/utils/buildTimelineDisplayRows';
import { findTimelineScrollLocalIndex } from '@/features/playback/utils/buildTimelineDisplayRows';
import { playTimelineHistoryTrack } from '@/features/playback/utils/playTimelineHistoryTrack';

type StartDrag = (
  payload: { data: string; label: string },
  x: number,
  y: number,
) => void;

interface Props {
  queue: QueueItemRef[];
  /** Timeline virtual rows; when set, `queue` is ignored for rendering. */
  timelineRows?: TimelineDisplayRow[];
  /** Canonical queue for history-row play / context-menu index lookup. */
  canonicalQueue?: QueueItemRef[];
  queueIndex: number;
  displayBaseIndex: number;
  queueDisplayMode: QueueDisplayMode;
  emptyLabel: string;
  contextMenu: PlayerState['contextMenu'];
  playTrack: PlayerState['playTrack'];
  activeTab: string;
  queueListRef: React.RefObject<HTMLDivElement | null>;
  suppressNextAutoScrollRef: React.MutableRefObject<boolean>;
  isQueueDrag: boolean;
  psyDragFromIdxRef: React.MutableRefObject<number | null>;
  externalDropTarget: { idx: number; before: boolean } | null;
  startDrag: StartDrag;
  orbitAttributionLabel: (trackId: string) => string | null;
  luckyRolling: boolean;
  t: TFunction;
}

const INITIAL_RECT = { width: 0, height: 600 };

export function QueueList({
  queue, timelineRows, canonicalQueue, queueIndex, displayBaseIndex, queueDisplayMode, emptyLabel,
  contextMenu, playTrack, activeTab, queueListRef,
  suppressNextAutoScrollRef, isQueueDrag, psyDragFromIdxRef, externalDropTarget,
  startDrag, orbitAttributionLabel, luckyRolling, t,
}: Props) {
  useSyncExternalStore(subscribeQueueResolver, getQueueResolverVersion);

  const usingTimeline = queueDisplayMode === 'timeline' && timelineRows != null;
  const rowCount = usingTimeline ? timelineRows.length : queue.length;

  // eslint-disable-next-line react-hooks/incompatible-library
  const rowVirtualizer = useVirtualizer({
    count: rowCount,
    getScrollElement: () => queueListRef.current,
    estimateSize: () => 52,
    overscan: 10,
    getItemKey: i => {
      if (usingTimeline) return timelineRows[i]!.key;
      return `${queue[i].trackId}:${i}`;
    },
    initialRect: INITIAL_RECT,
  });
  const virtualItems = rowVirtualizer.getVirtualItems();
  const totalSize = rowVirtualizer.getTotalSize();

  // Resolve the rows the user is actually looking at (queue thin-state). The
  // queueResolverBridge only warms a window around the *playing* index; scrolling
  // the queue independently would otherwise leave rows stuck on the '…'
  // placeholder (cache miss / evicted from the bounded LRU). Drive resolution off
  // the virtualizer's visible range so any scrolled-to row fetches on demand.
  const firstVisible = virtualItems.length > 0 ? virtualItems[0]!.index : 0;
  const lastVisible = virtualItems.length > 0 ? virtualItems[virtualItems.length - 1]!.index : 0;
  useEffect(() => {
    if (rowCount === 0) return;
    const refs = collectQueueResolveRefs({ usingTimeline, timelineRows, queue, firstVisible, lastVisible });
    if (refs.length > 0) void resolveBatch(refs);
  // Keyed on the visible range + mode; the resolver dedups against cache/in-flight,
  // so re-running on scroll is cheap. Queue-content changes are covered by the bridge.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [firstVisible, lastVisible, rowCount, usingTimeline]);

  useEffect(() => {
    if (suppressNextAutoScrollRef.current) {
      suppressNextAutoScrollRef.current = false;
      return;
    }
    if (activeTab !== 'queue' || rowCount === 0 || !usingTimeline || !timelineRows) return;

    const localIdx = findTimelineScrollLocalIndex(timelineRows);
    if (localIdx == null) return;

    const pinToTop = (index: number, scrollSelector: string) => {
      rowVirtualizer.scrollToIndex(index, { align: 'start' });
      const id = requestAnimationFrame(() => {
        const el = queueListRef.current?.querySelector<HTMLElement>(scrollSelector);
        el?.scrollIntoView({ block: 'start', behavior: 'instant' });
      });
      return () => cancelAnimationFrame(id);
    };

    return pinToTop(localIdx, `[data-timeline-local-idx="${localIdx}"]`);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [queueIndex, activeTab, queueDisplayMode, usingTimeline]);

  useEffect(() => {
    if (suppressNextAutoScrollRef.current) {
      suppressNextAutoScrollRef.current = false;
      return;
    }
    if (activeTab !== 'queue' || rowCount === 0 || usingTimeline) return;

    const pinToTop = (localIndex: number, scrollSelector: string) => {
      rowVirtualizer.scrollToIndex(localIndex, { align: 'start' });
      const id = requestAnimationFrame(() => {
        const el = queueListRef.current?.querySelector<HTMLElement>(scrollSelector);
        el?.scrollIntoView({ block: 'start', behavior: 'instant' });
      });
      return () => cancelAnimationFrame(id);
    };

    if (queueDisplayMode === 'queue') {
      if (queueIndex < 0) return;
      return pinToTop(0, `[data-queue-idx="${displayBaseIndex}"]`);
    }

    if (queueIndex < 0) return;
    const viewport = queueListRef.current;
    if (viewport) {
      const rowEl = viewport.querySelector<HTMLElement>(`[data-queue-idx="${queueIndex}"]`);
      if (rowEl) {
        const rowRect = rowEl.getBoundingClientRect();
        const viewRect = viewport.getBoundingClientRect();
        const fullyVisible = rowRect.top >= viewRect.top && rowRect.bottom <= viewRect.bottom;
        if (fullyVisible) return;
      }
    }
    return pinToTop(queueIndex, `[data-queue-idx="${queueIndex}"]`);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [queueIndex, activeTab, queueDisplayMode, rowCount, usingTimeline]);

  const playHistoryRow = (serverId: string, trackId: string) => {
    suppressNextAutoScrollRef.current = true;
    void playTimelineHistoryTrack(serverId, trackId, canonicalQueue);
  };

  const renderTrackRow = (args: {
    track: ReturnType<typeof resolveQueueTrack>;
    absIdx: number | null;
    localIndex: number;
    isPlaying: boolean;
    isPast: boolean;
    isHistory: boolean;
    base?: QueueItemRef;
    dragStyle: React.CSSProperties;
  }) => {
    const { track, absIdx, localIndex, isPlaying, isPast, isHistory, base, dragStyle } = args;
    return (
      <div
        data-timeline-local-idx={localIndex}
        {...(isHistory ? { 'data-timeline-kind': 'history' } : {})}
        {...(absIdx != null ? { 'data-queue-idx': absIdx } : {})}
        className={`queue-item ${isPlaying ? 'active' : ''} ${contextMenu.isOpen && contextMenu.type === (absIdx != null ? 'queue-item' : 'song') && (absIdx != null ? contextMenu.queueIndex === absIdx : contextMenu.item === track) ? 'context-active' : ''}`}
        onClick={() => {
          if (isHistory) {
            playHistoryRow(base?.serverId ?? track.serverId ?? '', track.id);
            return;
          }
          if (absIdx == null) return;
          suppressNextAutoScrollRef.current = true;
          playTrack(track, undefined, undefined, undefined, absIdx);
        }}
        onContextMenu={(e) => {
          e.preventDefault();
          if (isHistory && absIdx == null) {
            usePlayerStore.getState().openContextMenu(e.clientX, e.clientY, track, 'song');
            return;
          }
          if (absIdx == null) return;
          usePlayerStore.getState().openContextMenu(e.clientX, e.clientY, track, 'queue-item', absIdx);
        }}
        onMouseDown={(e) => {
          if (isHistory || absIdx == null) return;
          if (e.button !== 0) return;
          e.preventDefault();
          const startX = e.clientX;
          const startY = e.clientY;
          const onMove = (me: MouseEvent) => {
            if (Math.abs(me.clientX - startX) > 5 || Math.abs(me.clientY - startY) > 5) {
              document.removeEventListener('mousemove', onMove);
              document.removeEventListener('mouseup', onUp);
              psyDragFromIdxRef.current = absIdx;
              startDrag({ data: JSON.stringify({ type: 'queue_reorder', index: absIdx }), label: track.title }, me.clientX, me.clientY);
            }
          };
          const onUp = () => {
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('mouseup', onUp);
          };
          document.addEventListener('mousemove', onMove);
          document.addEventListener('mouseup', onUp);
        }}
        style={{ ...(isPast && !isPlaying ? { opacity: 0.5 } : null), ...dragStyle }}
      >
        <div className="queue-item-info">
          <div className="queue-item-title truncate" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
            {isPlaying && <Play size={10} fill="currentColor" style={{ flexShrink: 0 }} />}
            <span className="truncate">{track.title}</span>
          </div>
          <div className="queue-item-artist truncate">{track.artist}</div>
          {(() => {
            const label = orbitAttributionLabel(track.id);
            return label ? <div className="queue-item-attribution truncate">{label}</div> : null;
          })()}
        </div>
        <div className="queue-item-duration">
          {formatTrackTime(track.duration)}
        </div>
      </div>
    );
  };

  return (
    <OverlayScrollArea
      viewportRef={queueListRef}
      className="queue-list-wrap"
      viewportClassName="queue-list"
      measureDeps={[activeTab, rowCount, totalSize]}
      railInset="panel"
      viewportScrollBehaviorAuto={isQueueDrag}
    >
      {rowCount === 0 ? (
        emptyLabel ? (
          <div className="queue-empty">{emptyLabel}</div>
        ) : null
      ) : (
        <div style={{ height: totalSize, width: '100%', position: 'relative' }}>
        {virtualItems.map(vi => {
          const idx = vi.index;

          if (usingTimeline && timelineRows) {
            const row = timelineRows[idx]!;
            if (row.kind === 'divider') {
              return (
                <div
                  key={row.key}
                  data-index={idx}
                  data-timeline-local-idx={row.localIndex}
                  ref={rowVirtualizer.measureElement}
                  style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${vi.start}px)` }}
                >
                  <div className="queue-divider" style={{ margin: '2px 0' }}>
                    <span style={{ fontSize: '11px', fontWeight: 600, color: 'var(--text-muted)' }}>
                      {t(row.labelKey)}
                    </span>
                  </div>
                </div>
              );
            }

            const base = row.kind === 'history'
              ? { serverId: row.ref.serverId, trackId: row.ref.trackId }
              : row.ref;
            const track = resolveQueueTrack(base);
            const absIdx = row.kind === 'history'
              ? findQueueItemRefIndex(
                canonicalQueue ?? usePlayerStore.getState().queueItems,
                row.ref,
              )
              : row.queueIndex;
            const isPlaying = row.kind === 'current';
            const isPast = row.kind === 'history';
            const prevRow = idx > 0 ? timelineRows[idx - 1] : null;
            const isFirstAutoAdded = row.kind === 'upcoming' && row.ref.autoAdded
              && (prevRow?.kind !== 'upcoming' || !prevRow.ref.autoAdded);
            const isFirstRadioAdded = row.kind === 'upcoming' && row.ref.radioAdded
              && (prevRow?.kind !== 'upcoming' || !prevRow.ref.radioAdded);

            let dragStyle: React.CSSProperties = {};
            if (row.kind !== 'history' && isQueueDrag && psyDragFromIdxRef.current === absIdx) {
              dragStyle = { opacity: 0.4, background: 'var(--bg-hover)' };
            } else if (row.kind !== 'history' && isQueueDrag && externalDropTarget?.idx === absIdx) {
              dragStyle = externalDropTarget.before
                ? { borderTop: '2px solid var(--accent)', paddingTop: '6px', marginTop: '-2px' }
                : { borderBottom: '2px solid var(--accent)', paddingBottom: '6px', marginBottom: '-2px' };
            }

            return (
              <div
                key={row.key}
                data-index={idx}
                ref={rowVirtualizer.measureElement}
                style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${vi.start}px)` }}
              >
                {isFirstRadioAdded && (
                  <div className="queue-divider" style={{ margin: '2px 0' }}>
                    <span style={{ fontSize: '11px', fontWeight: 500, color: 'var(--text-muted)' }}>{t('queue.radioAdded')}</span>
                  </div>
                )}
                {isFirstAutoAdded && (
                  <div className="queue-divider" style={{ margin: '2px 0' }}>
                    <span style={{ fontSize: '11px', fontWeight: 500, color: 'var(--text-muted)' }}>{t('queue.autoAdded')}</span>
                  </div>
                )}
                {renderTrackRow({
                  track,
                  absIdx: row.kind === 'history' ? (absIdx >= 0 ? absIdx : null) : absIdx,
                  localIndex: row.localIndex,
                  isPlaying,
                  isPast,
                  isHistory: row.kind === 'history',
                  base,
                  dragStyle,
                })}
                {luckyRolling && isPlaying && (
                  <button
                    type="button"
                    className="queue-lucky-loading"
                    onClick={() => useLuckyMixStore.getState().cancel()}
                    data-tooltip={t('luckyMix.cancelTooltip')}
                    aria-label={t('luckyMix.cancelTooltip')}
                  >
                    <div className="queue-lucky-loading__dice">
                      <div className="queue-lucky-cube queue-lucky-cube--a">
                        <span className="lucky-mix-pip lucky-mix-pip--tl" />
                        <span className="lucky-mix-pip lucky-mix-pip--tr" />
                        <span className="lucky-mix-pip lucky-mix-pip--bl" />
                        <span className="lucky-mix-pip lucky-mix-pip--br" />
                      </div>
                      <div className="queue-lucky-cube queue-lucky-cube--b">
                        <span className="lucky-mix-pip lucky-mix-pip--center" />
                      </div>
                      <div className="queue-lucky-cube queue-lucky-cube--c">
                        <span className="lucky-mix-pip lucky-mix-pip--tl" />
                        <span className="lucky-mix-pip lucky-mix-pip--center" />
                        <span className="lucky-mix-pip lucky-mix-pip--br" />
                      </div>
                    </div>
                  </button>
                )}
              </div>
            );
          }

          const absIdx = displayBaseIndex + idx;
          const base = queue[idx];
          const track = resolveQueueTrack(base);
          const isPlaying = absIdx === queueIndex;
          const isPast = false;
          const isFirstAutoAdded = base.autoAdded && (idx === 0 || !queue[idx - 1].autoAdded);
          const isFirstRadioAdded = base.radioAdded && (idx === 0 || !queue[idx - 1].radioAdded);

          let dragStyle: React.CSSProperties = {};
          if (isQueueDrag && psyDragFromIdxRef.current === absIdx) {
            dragStyle = { opacity: 0.4, background: 'var(--bg-hover)' };
          } else if (isQueueDrag && externalDropTarget?.idx === absIdx) {
            if (externalDropTarget.before) {
              dragStyle = { borderTop: '2px solid var(--accent)', paddingTop: '6px', marginTop: '-2px' };
            } else {
              dragStyle = { borderBottom: '2px solid var(--accent)', paddingBottom: '6px', marginBottom: '-2px' };
            }
          }

          return (
            <div
              key={vi.key}
              data-index={idx}
              ref={rowVirtualizer.measureElement}
              style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${vi.start}px)` }}
            >
            {isFirstRadioAdded && (
              <div className="queue-divider" style={{ margin: '2px 0' }}>
                <span style={{ fontSize: '11px', fontWeight: 500, color: 'var(--text-muted)' }}>{t('queue.radioAdded')}</span>
              </div>
            )}
            {isFirstAutoAdded && (
              <div className="queue-divider" style={{ margin: '2px 0' }}>
                <span style={{ fontSize: '11px', fontWeight: 500, color: 'var(--text-muted)' }}>{t('queue.autoAdded')}</span>
              </div>
            )}
            {renderTrackRow({
              track,
              absIdx,
              localIndex: idx,
              isPlaying,
              isPast,
              isHistory: false,
              base,
              dragStyle,
            })}
            {luckyRolling && isPlaying && (
              <button
                type="button"
                className="queue-lucky-loading"
                onClick={() => useLuckyMixStore.getState().cancel()}
                data-tooltip={t('luckyMix.cancelTooltip')}
                aria-label={t('luckyMix.cancelTooltip')}
              >
                <div className="queue-lucky-loading__dice">
                  <div className="queue-lucky-cube queue-lucky-cube--a">
                    <span className="lucky-mix-pip lucky-mix-pip--tl" />
                    <span className="lucky-mix-pip lucky-mix-pip--tr" />
                    <span className="lucky-mix-pip lucky-mix-pip--bl" />
                    <span className="lucky-mix-pip lucky-mix-pip--br" />
                  </div>
                  <div className="queue-lucky-cube queue-lucky-cube--b">
                    <span className="lucky-mix-pip lucky-mix-pip--center" />
                  </div>
                  <div className="queue-lucky-cube queue-lucky-cube--c">
                    <span className="lucky-mix-pip lucky-mix-pip--tl" />
                    <span className="lucky-mix-pip lucky-mix-pip--center" />
                    <span className="lucky-mix-pip lucky-mix-pip--br" />
                  </div>
                </div>
              </button>
            )}
            </div>
          );
        })}
        </div>
      )}
    </OverlayScrollArea>
  );
}
