import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { ELECTIVE_SETS } from '../data/electiveSets';
import { SPECIALIZATIONS } from '../data/specializations';
import { coursesBySet } from '../data/lookups';
import { COURSE_DESCRIPTIONS } from '../data/courseDescriptions';
import { courseById } from '../data/lookups';
import { useMediaQuery } from '../hooks/useMediaQuery';
import { makePriorityScorer, makePriorityRankWeight } from '../solver/priority';
import { specColor } from '../data/specColors';
import type { OptimizationMode, Term } from '../data/types';
import type { SetAnalysis } from '../solver/decisionTree';

function SpecTag({ specId }: { specId: string }) {
  const c = specColor(specId);
  return (
    <span
      title={specNameById[specId] ?? specId}
      style={{
        display: 'inline-block',
        fontSize: '9px', fontWeight: 700, letterSpacing: '0.2px',
        padding: '1px 5px', borderRadius: '3px',
        background: c.bg, color: c.fg, border: `1px solid ${c.border}`,
        whiteSpace: 'nowrap', lineHeight: '1.3',
      }}
    >
      {specId}
    </span>
  );
}

// Reverse map: courseId → specialization names that require it
const requiredForSpec: Record<string, string[]> = {};
for (const spec of SPECIALIZATIONS) {
  if (spec.requiredCourseId) {
    (requiredForSpec[spec.requiredCourseId] ??= []).push(spec.name);
  }
}

// specId → full spec name
const specNameById: Record<string, string> = {};
for (const spec of SPECIALIZATIONS) {
  specNameById[spec.id] = spec.name;
}

function CourseInfoPopover({
  courseId,
  courseName,
  anchorRect,
  onClose,
  onHoverEnter,
  onHoverLeave,
}: {
  courseId: string;
  courseName: string;
  anchorRect: DOMRect | null;
  onClose: () => void;
  onHoverEnter: () => void;
  onHoverLeave: () => void;
}) {
  const popoverRef = useRef<HTMLDivElement>(null);
  const breakpoint = useMediaQuery();
  const isMobile = breakpoint === 'mobile';
  const info = COURSE_DESCRIPTIONS[courseId];

  // Close on Escape
  useEffect(() => {
    function onKeyDown(e: KeyboardEvent) {
      if (e.key === 'Escape') onClose();
    }
    document.addEventListener('keydown', onKeyDown);
    return () => document.removeEventListener('keydown', onKeyDown);
  }, [onClose]);

  // Close on click outside
  useEffect(() => {
    function onClickOutside(e: MouseEvent) {
      if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
        onClose();
      }
    }
    // Defer so the opening click doesn't immediately close
    const id = setTimeout(() => document.addEventListener('mousedown', onClickOutside), 0);
    return () => {
      clearTimeout(id);
      document.removeEventListener('mousedown', onClickOutside);
    };
  }, [onClose]);

  if (!info) return null;

  const instructorLabel = info.instructors.length > 1 ? 'Instructors' : 'Instructor';
  const instructorText = info.instructors.length > 0
    ? info.instructors.join(', ')
    : null;

  // Mobile: centered fixed overlay. Desktop: anchored near the icon, flipping above if needed.
  const popoverMaxHeight = 300;
  const spaceBelow = anchorRect ? window.innerHeight - anchorRect.bottom - 6 : popoverMaxHeight;
  const spaceAbove = anchorRect ? anchorRect.top - 6 : popoverMaxHeight;
  const placeAbove = spaceBelow < Math.min(popoverMaxHeight, 150) && spaceAbove > spaceBelow;

  const positionStyle: React.CSSProperties = isMobile
    ? {
        position: 'fixed',
        left: '16px',
        right: '16px',
        top: '50%',
        transform: 'translateY(-50%)',
        maxWidth: 'calc(100vw - 32px)',
      }
    : {
        position: 'fixed',
        left: anchorRect ? Math.min(anchorRect.left, window.innerWidth - 340) : 0,
        ...(placeAbove
          ? { bottom: anchorRect ? window.innerHeight - anchorRect.top + 6 : 0, maxHeight: Math.min(popoverMaxHeight, spaceAbove) }
          : { top: anchorRect ? anchorRect.bottom + 6 : 0, maxHeight: Math.min(popoverMaxHeight, spaceBelow) }),
        maxWidth: '320px',
      };

  return (
    <>
      {isMobile && (
        <div style={{
          position: 'fixed', inset: 0,
          background: 'rgba(0,0,0,0.3)',
          zIndex: 999,
        }} />
      )}
      <div
        ref={popoverRef}
        onMouseEnter={onHoverEnter}
        onMouseLeave={onHoverLeave}
        style={{
          ...positionStyle,
          zIndex: 1000,
          background: '#fff',
          border: '1px solid #d1d5db',
          borderRadius: '8px',
          boxShadow: '0 4px 16px rgba(0,0,0,0.15)',
          padding: '12px',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '6px' }}>
          <div style={{ fontWeight: 600, fontSize: '14px', color: '#1e293b', flex: 1, paddingRight: '8px' }}>
            {courseName}
          </div>
          <button
            onClick={onClose}
            style={{
              background: 'none', border: 'none', cursor: 'pointer',
              fontSize: '16px', color: '#94a3b8', padding: '0 2px',
              lineHeight: 1, flexShrink: 0,
            }}
            aria-label="Close"
          >
            &times;
          </button>
        </div>
        {instructorText && (
          <div style={{ fontSize: '12px', color: '#6366f1', marginBottom: '6px', fontWeight: 500 }}>
            {instructorLabel}: {instructorText}
          </div>
        )}
        {courseById[courseId]?.qualifications.length > 0 && (
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginBottom: '8px' }}>
            {courseById[courseId].qualifications.map((q) => (
              <span
                key={q.specId}
                title={specNameById[q.specId]}
                style={{
                  fontSize: '10px', fontWeight: 600,
                  padding: '2px 6px', borderRadius: '3px',
                  background: '#f0f0ff', color: '#4338ca',
                  border: '1px solid #e0e0ff',
                  whiteSpace: 'nowrap',
                }}
              >
                {q.specId}{q.marker !== 'standard' ? ` (${q.marker})` : ''}
              </span>
            ))}
          </div>
        )}
        <div style={{
          fontSize: '12px', color: '#475569', lineHeight: 1.5,
          overflowY: 'auto', flex: 1,
          whiteSpace: 'pre-line',
        }}>
          {info.description}
        </div>
      </div>
    </>
  );
}

interface CourseSelectionProps {
  pinnedCourses: Record<string, string | null>;
  treeResults: SetAnalysis[];
  treeLoading: boolean;
  disabledCourseIds: Set<string>;
  ranking: string[];
  mode: OptimizationMode;
  onPin: (setId: string, courseId: string) => void;
  onUnpin: (setId: string) => void;
  onClearAll: () => void;
}

function ElectiveSet({
  setId,
  setName,
  pinnedCourseId,
  analysis,
  loading,
  disabledCourseIds,
  scorer,
  rankWeight,
  mode,
  isMobile,
  onPin,
  onUnpin,
  openPopoverId,
  onOpenPopover,
  onClosePopover,
  onHoverOpen,
  onHoverLeave,
}: {
  setId: string;
  setName: string;
  pinnedCourseId: string | null | undefined;
  analysis?: SetAnalysis;
  loading: boolean;
  disabledCourseIds: Set<string>;
  scorer: (specs: string[]) => number;
  rankWeight: (specs: string[]) => number;
  mode: OptimizationMode;
  isMobile: boolean;
  onPin: (courseId: string) => void;
  onUnpin: () => void;
  openPopoverId: string | null;
  onOpenPopover: (courseId: string, rect: DOMRect) => void;
  onClosePopover: () => void;
  onHoverOpen: (courseId: string, rect: DOMRect) => void;
  onHoverLeave: () => void;
}) {
  const courses = coursesBySet[setId];
  const isPinned = pinnedCourseId != null;
  const pinnedCourse = isPinned ? courses.find((c) => c.id === pinnedCourseId) : null;

  // Build a map from courseId to ceiling choice data
  const ceilingMap = new Map(
    (analysis?.choices ?? []).map((ch) => [ch.courseId, ch]),
  );
  const hasHighImpact = analysis && analysis.impact > 0;

  // Determine the recommended choice. Mode-dependent comparison matches the
  // top-K comparator: priority-order ranks by (rankWeight, count); max-count by (count, rankWeight).
  // Lex rank weight: a single high-priority spec dominates any combination of lower ones.
  let recommendedCourseId: string | null = null;
  if (analysis && analysis.choices.length > 0) {
    let best: { id: string; count: number; weight: number } | null = null;
    for (const ch of analysis.choices) {
      if (!ch.evaluated) continue;
      const weight = rankWeight(ch.ceilingSpecs);
      const isBetter =
        !best ||
        (mode === 'priority-order'
          ? weight > best.weight || (weight === best.weight && ch.ceilingCount > best.count)
          : ch.ceilingCount > best.count || (ch.ceilingCount === best.count && weight > best.weight));
      if (isBetter) {
        best = { id: ch.courseId, count: ch.ceilingCount, weight };
      }
    }
    if (best && (best.count > 0 || best.weight > 0)) recommendedCourseId = best.id;
  }

  // Per-set still-searching indicator: search in progress AND at least one cell unevaluated
  const setSearching =
    loading && !!analysis && analysis.choices.some((c) => !c.evaluated);

  return (
    <div
      style={{
        border: isPinned ? '1px solid #3b82f6' : '1px solid #ccc',
        borderStyle: isPinned ? 'solid' : 'dashed',
        borderRadius: '8px',
        padding: isPinned ? '8px 12px' : '12px',
        marginBottom: '8px',
        background: isPinned ? '#eff6ff' : '#fafafa',
        transition: 'border-color 200ms, background-color 200ms, padding 200ms',
      }}
    >
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        gap: '8px',
        marginBottom: isPinned ? 0 : '8px',
      }}>
        <h4 style={{
          fontSize: '13px', margin: 0, color: '#444',
          display: 'flex', alignItems: 'baseline', gap: '8px', flexWrap: 'wrap',
          minWidth: 0, flex: 1,
        }}>
          <span style={{ flexShrink: 0 }}>{setName}{isPinned ? ':' : ''}</span>
          {isPinned && pinnedCourse && (
            <span style={{
              fontSize: '13px', fontWeight: 600, color: '#1e40af',
              minWidth: 0,
              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
            }}>
              {pinnedCourse.name}
            </span>
          )}
          {!isPinned && setSearching && (
            <span style={{
              display: 'inline-block', width: '10px', height: '10px', borderRadius: '50%',
              border: '2px solid #cbd5e1', borderTopColor: '#6366f1',
              animation: 'spin 0.8s linear infinite',
            }} aria-label="searching" />
          )}
          {!isPinned && hasHighImpact && (
            <span style={{ fontSize: '11px', color: '#d97706', fontWeight: 400 }}>high impact</span>
          )}
        </h4>
        {!isPinned && isMobile && (
          <span style={{
            fontSize: '10px', color: '#94a3b8', fontWeight: 500, letterSpacing: '0.3px',
            textTransform: 'uppercase',
            flexShrink: 0,
          }}>
            top outcome if picked ↓
          </span>
        )}
        {isPinned && (
          <button
            onClick={onUnpin}
            style={{
              fontSize: '11px', border: '1px solid #bfdbfe', background: '#eff6ff',
              color: '#2563eb', cursor: 'pointer', padding: '3px 10px',
              borderRadius: '4px', fontWeight: 500,
              flexShrink: 0,
            }}
          >
            Clear
          </button>
        )}
      </div>
      {/* Course list view */}
      <div style={{
        maxHeight: isPinned ? '0' : '500px',
        opacity: isPinned ? 0 : 1,
        overflow: 'hidden',
        pointerEvents: isPinned ? 'none' : 'auto',
        transition: 'max-height 250ms ease-out, opacity 200ms',
      }}>
        <div style={{
          display: 'flex',
          flexDirection: isMobile ? 'column' : 'row',
          alignItems: isMobile ? 'stretch' : 'stretch',
          gap: '4px',
        }}>
          {courses.map((course) => {
            const isCancelled = !!course.cancelled;
            const isDisabled = disabledCourseIds.has(course.id);
            const isUnavailable = isCancelled || isDisabled;
            const ceiling = ceilingMap.get(course.id);
            const reqFor = requiredForSpec[course.id];
            const showSkeleton = loading && !analysis;
            const cellSearching = !!ceiling && !ceiling.evaluated;
            const isRecommended = recommendedCourseId === course.id;
            const hasInfo = !!COURSE_DESCRIPTIONS[course.id];

            const infoIcon = !isUnavailable && hasInfo ? (
              <span
                role="button"
                tabIndex={0}
                aria-label={`Info about ${course.name}`}
                onClick={(e) => {
                  e.stopPropagation();
                  e.preventDefault();
                  if (openPopoverId === course.id) {
                    onClosePopover();
                  } else {
                    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
                    onOpenPopover(course.id, rect);
                  }
                }}
                onMouseEnter={(e) => {
                  if (window.matchMedia('(hover: hover)').matches) {
                    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
                    onHoverOpen(course.id, rect);
                  }
                }}
                onMouseLeave={() => {
                  if (window.matchMedia('(hover: hover)').matches) {
                    onHoverLeave();
                  }
                }}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' || e.key === ' ') {
                    e.stopPropagation();
                    e.preventDefault();
                    if (openPopoverId === course.id) {
                      onClosePopover();
                    } else {
                      const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
                      onOpenPopover(course.id, rect);
                    }
                  }
                }}
                style={{
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  width: '16px', height: '16px', borderRadius: '50%',
                  border: '1px solid #cbd5e1', background: openPopoverId === course.id ? '#e0e7ff' : '#f1f5f9',
                  color: '#6366f1', fontSize: '10px', fontWeight: 700,
                  cursor: 'pointer', flexShrink: 0,
                  fontStyle: 'normal', textDecoration: 'none',
                  lineHeight: 1,
                }}
              >
                i
              </span>
            ) : null;

            const ceilingTags = !isUnavailable && (showSkeleton || cellSearching) ? (
              <span style={{
                fontSize: '11px', color: '#94a3b8', fontStyle: 'italic',
                display: 'inline-flex', alignItems: 'center', gap: '4px',
              }}>
                <span style={{
                  display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%',
                  background: '#cbd5e1', animation: 'cell-pulse 1.2s ease-in-out infinite',
                }} />
                searching
              </span>
            ) : !isUnavailable && ceiling && ceiling.evaluated ? (
              <span style={{
                display: 'inline-flex', alignItems: 'center', gap: '4px',
                flexWrap: 'wrap', justifyContent: isMobile ? 'flex-end' : 'flex-start',
              }}>
                {ceiling.ceilingSpecs.length === 0 ? (
                  <span style={{ fontSize: '11px', color: '#9ca3af', fontStyle: 'italic' }}>
                    no specs
                  </span>
                ) : (
                  ceiling.ceilingSpecs.map((s) => <SpecTag key={s} specId={s} />)
                )}
              </span>
            ) : null;

            if (isMobile) {
              return (
                <button
                  key={course.id}
                  onClick={isUnavailable ? undefined : () => onPin(course.id)}
                  disabled={isUnavailable}
                  style={{
                    display: 'flex', flexDirection: 'column', alignItems: 'stretch',
                    textAlign: 'left', padding: '6px 10px',
                    border: '1px solid #e5e7eb', borderRadius: '4px',
                    background: isUnavailable ? '#f5f5f5' : '#fff',
                    cursor: isUnavailable ? 'default' : 'pointer',
                    fontSize: '13px',
                    color: isUnavailable ? '#bbb' : '#333',
                    pointerEvents: isUnavailable ? 'none' : 'auto',
                  }}
                >
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' }}>
                    <span style={{
                      flex: 1,
                      textDecoration: isCancelled ? 'line-through' : 'none',
                      fontStyle: isCancelled ? 'italic' : 'normal',
                      display: 'flex',
                      alignItems: 'center',
                      gap: '4px',
                    }}>
                      {course.name}
                      {isCancelled && (
                        <span style={{ fontSize: '11px', color: '#999', marginLeft: '6px', fontStyle: 'normal', textDecoration: 'none' }}>
                          (Cancelled)
                        </span>
                      )}
                      {!isCancelled && isDisabled && (
                        <span style={{ fontSize: '11px', color: '#999', marginLeft: '6px' }}>
                          (Already selected)
                        </span>
                      )}
                      {isRecommended && !isUnavailable && (
                        <span
                          title="Best outcome among the choices in this set"
                          style={{
                            fontSize: '10px', color: '#15803d', fontWeight: 600,
                            display: 'inline-flex', alignItems: 'center', gap: '2px',
                            padding: '0 4px', borderRadius: '3px', background: '#dcfce7',
                            border: '1px solid #bbf7d0', lineHeight: 1.2,
                          }}
                        >
                          <span aria-hidden="true">★</span>
                          <span>Recommended</span>
                        </span>
                      )}
                      {infoIcon}
                    </span>
                    {ceilingTags}
                  </div>
                  {reqFor && !isUnavailable && (
                    <span style={{ fontSize: '11px', color: '#92400e', marginTop: '2px' }}>
                      Required for {reqFor.join(', ')}
                    </span>
                  )}
                </button>
              );
            }

            // Desktop horizontal button
            return (
              <button
                key={course.id}
                onClick={isUnavailable ? undefined : () => onPin(course.id)}
                disabled={isUnavailable}
                title={course.name}
                style={{
                  flex: '1 1 0',
                  minWidth: 0,
                  display: 'flex', flexDirection: 'column', alignItems: 'stretch',
                  textAlign: 'left', padding: '6px 8px', gap: '4px',
                  border: isRecommended && !isUnavailable ? '1px solid #bbf7d0' : '1px solid #e5e7eb',
                  borderRadius: '4px',
                  background: isUnavailable ? '#f5f5f5' : isRecommended ? '#f0fdf4' : '#fff',
                  cursor: isUnavailable ? 'default' : 'pointer',
                  fontSize: '12px',
                  color: isUnavailable ? '#bbb' : '#333',
                  pointerEvents: isUnavailable ? 'none' : 'auto',
                  font: 'inherit',
                }}
              >
                <div style={{
                  display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                  minHeight: '16px',
                }}>
                  <span style={{ display: 'inline-flex', alignItems: 'center' }}>{infoIcon}</span>
                  {isRecommended && !isUnavailable && (
                    <span
                      title="Best outcome among the choices in this set"
                      aria-label="Recommended"
                      style={{
                        fontSize: '12px', color: '#15803d', lineHeight: 1,
                      }}
                    >
                      ★
                    </span>
                  )}
                </div>
                <div style={{
                  fontSize: '12px',
                  color: isUnavailable ? '#bbb' : '#333',
                  fontWeight: isRecommended && !isUnavailable ? 600 : 500,
                  lineHeight: 1.3,
                  textDecoration: isCancelled ? 'line-through' : 'none',
                  fontStyle: isCancelled ? 'italic' : 'normal',
                  display: '-webkit-box',
                  WebkitLineClamp: 3,
                  WebkitBoxOrient: 'vertical',
                  overflow: 'hidden',
                  flex: 1,
                }}>
                  {course.name}
                </div>
                {ceilingTags && (
                  <div style={{
                    display: 'flex', flexWrap: 'wrap', gap: '3px',
                    marginTop: 'auto', minHeight: '18px',
                  }}>
                    {ceilingTags}
                  </div>
                )}
                {isCancelled && (
                  <span style={{ fontSize: '10px', color: '#999', fontStyle: 'normal', textDecoration: 'none' }}>
                    Cancelled
                  </span>
                )}
                {!isCancelled && isDisabled && (
                  <span style={{ fontSize: '10px', color: '#999' }}>
                    Already selected
                  </span>
                )}
                {reqFor && !isUnavailable && (
                  <span style={{ fontSize: '10px', color: '#92400e', lineHeight: 1.3 }}>
                    Req. for {reqFor.join(', ')}
                  </span>
                )}
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

const skeletonStyle = `
@keyframes skeleton-pulse { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
@keyframes cell-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
@keyframes spin { to { transform: rotate(360deg); } }
`;

export function CourseSelection({ pinnedCourses, treeResults, treeLoading, disabledCourseIds, ranking, mode, onPin, onUnpin, onClearAll }: CourseSelectionProps) {
  const scorer = useMemo(() => makePriorityScorer(ranking), [ranking]);
  const rankWeight = useMemo(() => makePriorityRankWeight(ranking), [ranking]);
  const terms: Term[] = ['Spring', 'Summer', 'Fall'];
  const hasPinned = Object.keys(pinnedCourses).length > 0;
  const breakpoint = useMediaQuery();
  const isMobile = breakpoint === 'mobile';

  // Index tree results by setId for O(1) lookup
  const treeBySet = new Map(treeResults.map((a) => [a.setId, a]));

  // Single popover state: only one open at a time
  const [popover, setPopover] = useState<{ courseId: string; courseName: string; anchorRect: DOMRect } | null>(null);
  const hoverCloseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

  const cancelHoverClose = useCallback(() => {
    if (hoverCloseTimer.current) {
      clearTimeout(hoverCloseTimer.current);
      hoverCloseTimer.current = null;
    }
  }, []);

  const handleOpenPopover = useCallback((courseId: string, rect: DOMRect) => {
    cancelHoverClose();
    const allCourses = Object.values(coursesBySet).flat();
    const course = allCourses.find(c => c.id === courseId);
    setPopover({ courseId, courseName: course?.name ?? '', anchorRect: rect });
  }, [cancelHoverClose]);

  const handleClosePopover = useCallback(() => {
    cancelHoverClose();
    setPopover(null);
  }, [cancelHoverClose]);

  // Hover open: same as click open but cancels any pending close
  const handleHoverOpen = useCallback((courseId: string, rect: DOMRect) => {
    cancelHoverClose();
    const allCourses = Object.values(coursesBySet).flat();
    const course = allCourses.find(c => c.id === courseId);
    setPopover({ courseId, courseName: course?.name ?? '', anchorRect: rect });
  }, [cancelHoverClose]);

  // Hover leave: delayed close so mouse can move from icon to popover
  const handleHoverLeave = useCallback(() => {
    hoverCloseTimer.current = setTimeout(() => {
      setPopover(null);
    }, 150);
  }, []);

  return (
    <div>
      <style>{skeletonStyle}</style>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
        <div>
          <h2 style={{ fontSize: '16px', margin: 0 }}>Course Selection</h2>
          <p style={{ fontSize: '12px', color: '#888', margin: '2px 0 0' }}>Select one course per elective slot. Analysis shows how each choice affects your specializations.</p>
        </div>
        {hasPinned && (
          <button
            onClick={onClearAll}
            style={{
              fontSize: '12px', border: '1px solid #fecaca', background: '#fef2f2',
              color: '#dc2626', cursor: 'pointer', padding: '3px 10px',
              borderRadius: '4px', fontWeight: 500,
            }}
          >
            Clear All
          </button>
        )}
      </div>
      {terms.map((term) => (
        <div key={term} style={{ marginBottom: '16px' }}>
          <h3 style={{ fontSize: '13px', color: '#888', marginBottom: '8px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
            {term}
          </h3>
          {ELECTIVE_SETS.filter((s) => s.term === term).map((set) => (
            <ElectiveSet
              key={set.id}
              setId={set.id}
              setName={set.name}
              pinnedCourseId={pinnedCourses[set.id]}
              analysis={treeBySet.get(set.id)}
              loading={treeLoading}
              disabledCourseIds={disabledCourseIds}
              scorer={scorer}
              rankWeight={rankWeight}
              mode={mode}
              isMobile={isMobile}
              onPin={(courseId) => onPin(set.id, courseId)}
              onUnpin={() => onUnpin(set.id)}
              openPopoverId={popover?.courseId ?? null}
              onOpenPopover={handleOpenPopover}
              onClosePopover={handleClosePopover}
              onHoverOpen={handleHoverOpen}
              onHoverLeave={handleHoverLeave}
            />
          ))}
        </div>
      ))}
      {popover && (
        <CourseInfoPopover
          courseId={popover.courseId}
          courseName={popover.courseName}
          anchorRect={popover.anchorRect}
          onClose={handleClosePopover}
          onHoverEnter={cancelHoverClose}
          onHoverLeave={handleHoverLeave}
        />
      )}
    </div>
  );
}
