
const { useState, useMemo, useCallback, useEffect, useRef } = React;

// ── GPA helpers ──
const GRADE_POINTS = {
  'A+': 4.0, 'A': 4.0, 'A-': 3.7,
  'B+': 3.3, 'B': 3.0, 'B-': 2.7,
  'C+': 2.3, 'C': 2.0, 'C-': 1.7,
  'D+': 1.3, 'D': 1.0, 'D-': 0.7,
  'F':  0.0,
};
const GRADE_OPTIONS = ['A+','A','A-','B+','B','B-','C+','C','C-','D+','D','D-','F'];

function calcGPA(semesters, courseGrades) {
  let totalQP = 0, totalUnits = 0;
  for (const keys of Object.values(semesters)) {
    for (const k of keys) {
      const grade = courseGrades[k];
      if (!grade || !(grade in GRADE_POINTS)) continue;
      const units = (window.MOCK_COURSES[k] || {}).units || 3;
      totalQP    += GRADE_POINTS[grade] * units;
      totalUnits += units;
    }
  }
  if (totalUnits === 0) return null;
  return (totalQP / totalUnits).toFixed(2);
}

function gpaColor(gpa) {
  if (!gpa) return 'var(--text-3)';
  const g = parseFloat(gpa);
  if (g >= 3.5) return 'var(--green)';
  if (g >= 3.0) return 'var(--accent)';
  if (g >= 2.0) return 'var(--orange)';
  return 'var(--red)';
}

// Honors GPA — only courses with honors: true
function calcHonorsGPA(semesters, courseGrades) {
  let totalQP = 0, totalUnits = 0;
  for (const keys of Object.values(semesters)) {
    for (const k of keys) {
      const course = window.MOCK_COURSES[k];
      if (!course || !course.honors) continue;
      const grade = courseGrades[k];
      if (!grade || !(grade in GRADE_POINTS)) continue;
      totalQP    += GRADE_POINTS[grade] * (course.units || 3);
      totalUnits += (course.units || 3);
    }
  }
  if (totalUnits === 0) return null;
  return (totalQP / totalUnits).toFixed(2);
}

// IVC Honors thresholds:
//   ≥ 3.50 → safe (UCLA TAP zone)
//   3.25–3.49 → at minimum, borderline
//   < 3.25 → below threshold, benefits at risk
function honorsGpaColor(gpa) {
  if (!gpa) return 'var(--text-3)';
  const g = parseFloat(gpa);
  if (g >= 3.5)  return 'var(--green)';
  if (g >= 3.25) return 'var(--orange)';
  return 'var(--red)';
}

function honorsGpaLabel(gpa) {
  if (!gpa) return '';
  const g = parseFloat(gpa);
  if (g >= 3.5)  return 'On track';
  if (g >= 3.25) return 'At minimum';
  return 'Below threshold';
}

const SEM_TYPE_LABEL = { fall: 'Fall', spring: 'Spring', summer: 'Summer', winter: 'Winter' };
function semLabel(semId) {
  const [type, yr] = semId.split('-');
  return `${SEM_TYPE_LABEL[type] || type[0].toUpperCase() + type.slice(1)} ${yr}`;
}
function nextSemId(semId) {
  const [type, yrStr] = semId.split('-');
  const yr = parseInt(yrStr);
  if (type === 'fall')   return `spring-${yr + 1}`;
  if (type === 'spring') return `summer-${yr}`;
  if (type === 'summer') return `fall-${yr}`;
  return `fall-${yr + 1}`;
}
function prevSemId(semId) {
  const [type, yrStr] = semId.split('-');
  const yr = parseInt(yrStr);
  if (type === 'fall')   return `summer-${yr}`;
  if (type === 'summer') return `spring-${yr}`;
  if (type === 'spring') return `fall-${yr - 1}`;
  return `spring-${yr}`;
}

function DiffDot({ courseKey }) {
  const c = window.MOCK_COURSES[courseKey];
  const d = c ? c.difficulty : 'easy';
  return (
    <span className="diff-dot" style={{ background: window.DIFF_COLOR[d] }} title={window.DIFF_LABEL[d]}></span>
  );
}

function StatusBadge({ status }) {
  const labels = window.STATUS_LABELS;
  const colors = window.STATUS_COLORS;
  const s = status || 'planned';
  return (
    <span className="status-badge" style={{ background: colors[s].bg, color: colors[s].text }}>
      {labels[s]}
    </span>
  );
}

function CourseCard({ courseKey, status, grade, onStatusChange, onGradeChange, onRemove, isDragging, onDragStart, onDragOver, onDragEnd }) {
  const course = window.MOCK_COURSES[courseKey] || { title: '', units: 3, tags: [] };
  const [menuOpen, setMenuOpen] = useState(false);
  const [gradeOpen, setGradeOpen] = useState(false);
  const statuses = ['planned', 'in-progress', 'completed'];

  const gpPts = grade ? GRADE_POINTS[grade] : null;
  const qp    = gpPts !== null ? (gpPts * (course.units || 3)).toFixed(1) : null;

  return (
    <div
      className={`pb-card status-${status || 'planned'} ${isDragging ? 'dragging' : ''}`}
      data-course-key={courseKey}
      draggable={true}
      onDragStart={(e) => onDragStart && onDragStart(e, courseKey)}
      onDragOver={(e) => onDragOver && onDragOver(e, courseKey)}
      onDragEnd={onDragEnd}
    >
      <span
        className="pb-card-grip"
        title="Drag to reorder"
        aria-hidden="true"
      >
        <svg width="10" height="14" viewBox="0 0 10 14" fill="none">
          <circle cx="3" cy="3" r="1.5" fill="currentColor"/><circle cx="7" cy="3" r="1.5" fill="currentColor"/>
          <circle cx="3" cy="7" r="1.5" fill="currentColor"/><circle cx="7" cy="7" r="1.5" fill="currentColor"/>
          <circle cx="3" cy="11" r="1.5" fill="currentColor"/><circle cx="7" cy="11" r="1.5" fill="currentColor"/>
        </svg>
      </span>
      <div className="pb-card-body">
        <div className="pb-card-top">
          <DiffDot courseKey={courseKey} />
          <span className="pb-card-key">{courseKey}</span>
          {course.honors && (
            <span style={{
              fontSize: '0.6rem', fontWeight: 700, padding: '1px 5px',
              borderRadius: '4px', background: 'var(--accent-bg)',
              color: 'var(--accent)', letterSpacing: '0.04em', textTransform: 'uppercase',
            }}>HON</span>
          )}
          <span className="pb-card-units">{course.units} units</span>
        </div>
        {course.title && <div className="pb-card-title">{course.title}</div>}
        <div className="pb-card-bottom">
          <div className="pb-status-wrap">
            <button className="pb-status-btn" onClick={() => setMenuOpen(v => !v)}>
              <StatusBadge status={status} />
              <span className="pb-status-chevron">▾</span>
            </button>
            {menuOpen && (
              <div className="pb-status-menu">
                {statuses.map(s => (
                  <button key={s} className={`pb-status-option ${(status || 'planned') === s ? 'active' : ''}`}
                    onClick={() => { onStatusChange(courseKey, s); setMenuOpen(false); }}>
                    {window.STATUS_LABELS[s]}
                  </button>
                ))}
              </div>
            )}
          </div>

          {/* Grade picker */}
          <div className="pb-grade-wrap">
            <button className="pb-grade-btn" onClick={() => setGradeOpen(v => !v)}
              style={{ color: grade ? gpaColor(gpPts !== null ? gpPts.toFixed(2) : null) : 'var(--text-3)' }}>
              {grade || 'Grade'}
              <span className="pb-status-chevron">▾</span>
            </button>
            {gradeOpen && (
              <div className="pb-grade-menu">
                <button className="pb-grade-option pb-grade-clear" onClick={() => { onGradeChange(courseKey, null); setGradeOpen(false); }}>
                  — Clear
                </button>
                {GRADE_OPTIONS.map(g => (
                  <button key={g}
                    className={`pb-grade-option ${grade === g ? 'active' : ''}`}
                    style={{ color: grade === g ? gpaColor(GRADE_POINTS[g].toFixed(2)) : undefined }}
                    onClick={() => { onGradeChange(courseKey, g); setGradeOpen(false); }}>
                    <span className="pb-grade-letter">{g}</span>
                    <span className="pb-grade-pts">{GRADE_POINTS[g].toFixed(1)}</span>
                  </button>
                ))}
              </div>
            )}
          </div>

          {qp !== null && (
            <span className="pb-qp-badge" title={`${qp} quality points (${grade} × ${course.units} units)`}>
              {qp} QP
            </span>
          )}

          <button className="pb-card-remove" onClick={() => onRemove(courseKey)}>×</button>
        </div>
      </div>
    </div>
  );
}

function SemesterRow({ semId, courses, courseStatuses, courseGrades, isTransferred, onStatusChange, onGradeChange, onRemoveCourse, onAddCourse, onCardDragStart, onCardDragOver, onCardDragEnd, onSemDragOver, draggingKey, onDeleteSemester, unitCap, summerCap }) {
  const [expanded, setExpanded] = useState(true);

  const totalUnits = courses.reduce((s, k) => s + ((window.MOCK_COURSES[k] || {}).units || 3), 0);
  const semType = semId.split('-')[0];
  const cap = semType === 'summer' ? (summerCap || 9) : (unitCap || 18);
  const over = totalUnits > cap;

  const label = semLabel(semId);

  return (
    <div
      className={`pb-sem-row ${isTransferred ? 'transferred' : ''}`}
      data-sem-id={semId}
      data-transferred={isTransferred ? 'true' : 'false'}
    >
      <div className="pb-sem-header" onClick={() => setExpanded(v => !v)}>
        <div className="pb-sem-left">
          <span className={`pb-sem-type-dot sem-${semType}`}></span>
          <span className="pb-sem-label">{label}</span>
          {isTransferred && <span className="pb-sem-after-badge">After transfer</span>}
        </div>
        <div className="pb-sem-right">
          <span className={`pb-sem-units ${over ? 'over' : totalUnits > cap * 0.85 ? 'heavy' : ''}`}>
            {totalUnits} / {cap} units
          </span>
          <span className="pb-sem-chevron">{expanded ? '▴' : '▾'}</span>
          {onDeleteSemester && (
            <button
              className="pb-sem-delete"
              onClick={(e) => { e.stopPropagation(); onDeleteSemester(semId); }}
              title={`Delete ${label}`}
              aria-label={`Delete ${label}`}
            >
              <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
                <path d="M3 3l7 7M10 3l-7 7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
              </svg>
            </button>
          )}
        </div>
      </div>

      {expanded && (
        <div
          className="pb-sem-cards"
          onDragOver={(e) => onSemDragOver && onSemDragOver(e, semId)}
          onDrop={(e) => e.preventDefault()}
        >
          {courses.map((key) => (
            <CourseCard
              key={key}
              courseKey={key}
              status={courseStatuses[key]}
              grade={courseGrades[key]}
              onStatusChange={onStatusChange}
              onGradeChange={onGradeChange}
              onRemove={k => onRemoveCourse(semId, k)}
              isDragging={draggingKey === key}
              onDragStart={(e, k) => onCardDragStart(e, k, semId)}
              onDragOver={(e, k)  => onCardDragOver(e, k, semId)}
              onDragEnd={onCardDragEnd}
            />
          ))}
          {!isTransferred && (
            <button className="pb-sem-add-btn" onClick={() => onAddCourse(semId)}>
              + Add course
            </button>
          )}
          {courses.length === 0 && !isTransferred && (
            <div className="pb-sem-empty">Click + Add or drag a course here</div>
          )}
          {isTransferred && courses.length === 0 && (
            <div className="pb-sem-locked">This semester is after your transfer date</div>
          )}
        </div>
      )}
    </div>
  );
}

function PlanView({ profile, onUpdateProfile, onReset }) {
  const { semesters, courseStatuses, courseGrades = {}, selections, transferTerm } = profile;
  const [sidebarOpen, setSidebarOpen] = useState(true);
  const [sidebarQuery, setSidebarQuery] = useState('');
  // ── Native HTML5 drag-and-drop state ─────────────────────────────────
  // draggingRef holds { key, fromSemId }. fromSemId === null means the drag
  // started in the sidebar course bank. draggingKey is the same key in state
  // so cards can render the `.dragging` (opacity 0.4) class.
  const draggingRef = useRef(null);
  const [draggingKey, setDraggingKey] = useState(null);
  const [addModal, setAddModal] = useState(null); // semId
  const [addQuery, setAddQuery] = useState('');
  const [sidebarLoading, setSidebarLoading] = useState(false);
  const [addLoading, setAddLoading] = useState(false);
  const [sidebarApiVer, setSidebarApiVer] = useState(0);
  const [addApiVer, setAddApiVer] = useState(0);
  const sidebarTimerRef = useRef(null);
  const addTimerRef = useRef(null);

  const API_BASE = 'https://transfer-lens.onrender.com';

  function injectApiCourses(courses) {
    courses.forEach(c => {
      const key = `${c.prefix} ${c.course_number}`;
      if (!window.MOCK_COURSES[key]) {
        window.MOCK_COURSES[key] = {
          title: c.course_title,
          units: c.min_units || 3,
          difficulty: 'easy',
          available: true,
        };
      }
    });
  }

  useEffect(() => {
    if (sidebarQuery.length < 2) { setSidebarLoading(false); return; }
    if (sidebarTimerRef.current) clearTimeout(sidebarTimerRef.current);
    setSidebarLoading(true);
    sidebarTimerRef.current = setTimeout(() => {
      fetch(`${API_BASE}/api/sending-courses?q=${encodeURIComponent(sidebarQuery)}&limit=20`)
        .then(r => r.ok ? r.json() : [])
        .then(courses => { injectApiCourses(courses); setSidebarLoading(false); setSidebarApiVer(v => v + 1); })
        .catch(() => setSidebarLoading(false));
    }, 300);
    return () => { if (sidebarTimerRef.current) clearTimeout(sidebarTimerRef.current); };
  }, [sidebarQuery]);

  useEffect(() => {
    if (!addModal || addQuery.length < 2) { setAddLoading(false); return; }
    if (addTimerRef.current) clearTimeout(addTimerRef.current);
    setAddLoading(true);
    addTimerRef.current = setTimeout(() => {
      fetch(`${API_BASE}/api/sending-courses?q=${encodeURIComponent(addQuery)}&limit=20`)
        .then(r => r.ok ? r.json() : [])
        .then(courses => { injectApiCourses(courses); setAddLoading(false); setAddApiVer(v => v + 1); })
        .catch(() => setAddLoading(false));
    }, 300);
    return () => { if (addTimerRef.current) clearTimeout(addTimerRef.current); };
  }, [addQuery, addModal]);

  const SEM_ORDER = { spring: 0, summer: 1, fall: 2 };
  const semSortKey = (id) => {
    const [type, yr] = id.split('-');
    return parseInt(yr) * 10 + (SEM_ORDER[type] ?? 9);
  };

  const semIds = useMemo(() => {
    const base = [
      'fall-2025', 'spring-2026', 'summer-2026',
      'fall-2026', 'spring-2027', 'summer-2027',
      'fall-2027', 'spring-2028',
    ];
    const hidden = new Set(profile.hiddenSems || []);
    const all = new Set(base.filter(id => !id.includes('summer') || profile.useSummer));
    for (const id of (profile.extraSems || [])) all.add(id);
    for (const h of hidden) all.delete(h);
    return Array.from(all).sort((a, b) => semSortKey(a) - semSortKey(b));
  }, [profile.useSummer, profile.extraSems, profile.hiddenSems]);

  const handleAddSemester = () => {
    if (semIds.length === 0) return;
    const last = semIds[semIds.length - 1];
    let next = nextSemId(last);
    while (semIds.includes(next) || (next.startsWith('summer-') && !profile.useSummer)) {
      next = nextSemId(next);
    }
    const hidden = (profile.hiddenSems || []).filter(id => id !== next);
    onUpdateProfile({
      extraSems: [...(profile.extraSems || []), next],
      hiddenSems: hidden,
    });
  };

  const handleAddPrevious = () => {
    if (semIds.length === 0) return;
    const first = semIds[0];
    let prev = prevSemId(first);
    while (semIds.includes(prev) || (prev.startsWith('summer-') && !profile.useSummer)) {
      prev = prevSemId(prev);
    }
    const hidden = (profile.hiddenSems || []).filter(id => id !== prev);
    onUpdateProfile({
      extraSems: [...(profile.extraSems || []), prev],
      hiddenSems: hidden,
    });
  };

  const handleDeleteSemester = useCallback((semId) => {
    const semCourses = semesters[semId] || [];
    if (semCourses.length > 0) {
      const ok = window.confirm(`Delete ${semLabel(semId)}? ${semCourses.length} course${semCourses.length !== 1 ? 's' : ''} will be moved back to the course bank.`);
      if (!ok) return;
    }
    const updatedSemesters = { ...semesters };
    delete updatedSemesters[semId];
    onUpdateProfile({
      semesters: updatedSemesters,
      hiddenSems: [...new Set([...(profile.hiddenSems || []), semId])],
      extraSems: (profile.extraSems || []).filter(id => id !== semId),
    });
  }, [semesters, profile.hiddenSems, profile.extraSems, onUpdateProfile]);

  const allPlacedKeys = useMemo(() => new Set(Object.values(semesters).flat()), [semesters]);
  const transferTermYear = transferTerm ? parseInt(transferTerm.split('-')[1]) : 2027;
  const transferTermType = transferTerm ? transferTerm.split('-')[0] : 'fall';

  const isTransferred = (semId) => {
    const [type, yr] = semId.split('-');
    const year = parseInt(yr);
    if (year > transferTermYear) return true;
    if (year === transferTermYear && type === 'fall' && transferTermType === 'spring') return true;
    return false;
  };

  const totalUnits = Object.values(semesters).flat().reduce((s, k) => s + ((window.MOCK_COURSES[k] || {}).units || 3), 0);
  const gpa = calcGPA(semesters, courseGrades);
  const gradedCount = Object.values(semesters).flat().filter(k => courseGrades[k]).length;
  const honorsGpa = profile.honorsMode ? calcHonorsGPA(semesters, courseGrades) : null;
  const honorsGradedCount = profile.honorsMode
    ? Object.values(semesters).flat().filter(k => courseGrades[k] && (window.MOCK_COURSES[k] || {}).honors).length
    : 0;

  const handleStatusChange = useCallback((key, status) => {
    onUpdateProfile({ courseStatuses: { ...courseStatuses, [key]: status } });
  }, [courseStatuses, onUpdateProfile]);

  const handleGradeChange = useCallback((key, grade) => {
    const next = { ...courseGrades };
    if (grade === null) delete next[key]; else next[key] = grade;
    onUpdateProfile({ courseGrades: next });
  }, [courseGrades, onUpdateProfile]);

  const handleRemoveCourse = useCallback((semId, key) => {
    const updated = { ...semesters, [semId]: (semesters[semId] || []).filter(k => k !== key) };
    onUpdateProfile({ semesters: updated });
  }, [semesters, onUpdateProfile]);

  // ── Native HTML5 drag-and-drop ───────────────────────────────────────
  // dragstart on a card or sidebar item records the source. dragover on a
  // target card splices the dragged key into that card's slot and we
  // re-render. dragover on the empty area of a semester appends to that
  // semester. dragend / drop clears the dragging state.
  const handleDragStart = useCallback((e, courseKey, fromSemId) => {
    draggingRef.current = { key: courseKey, fromSemId: fromSemId ?? null };
    e.dataTransfer.effectAllowed = 'move';
    try { e.dataTransfer.setData('text/plain', courseKey); } catch (_) {}
    // Defer so the browser captures the un-faded card for its drag image.
    requestAnimationFrame(() => setDraggingKey(courseKey));
  }, []);

  const handleDragEnd = useCallback(() => {
    draggingRef.current = null;
    setDraggingKey(null);
  }, []);

  // dragover on a target card: splice dragged key into target's index
  // within its semester. Works for in-sem reorder, cross-sem moves, and
  // sidebar → semester drags.
  const handleCardDragOver = useCallback((e, targetKey, targetSemId) => {
    e.preventDefault();
    const drag = draggingRef.current;
    if (!drag || drag.key === targetKey) return;
    e.dataTransfer.dropEffect = 'move';

    // Where is the target right now?
    const targetList = semesters[targetSemId] || [];
    const targetIdx  = targetList.indexOf(targetKey);
    if (targetIdx === -1) return;

    // Where is the dragged card right now (if anywhere)?
    let sourceSemId = null, sourceIdx = -1;
    for (const [sid, keys] of Object.entries(semesters)) {
      const i = keys.indexOf(drag.key);
      if (i !== -1) { sourceSemId = sid; sourceIdx = i; break; }
    }
    // Already at the target slot → nothing to do (this dragover fires every
    // few ms, we don't want to thrash state).
    if (sourceSemId === targetSemId && sourceIdx === targetIdx) return;

    // Remove from wherever it lives, then splice at the target's ORIGINAL
    // index (captured before filtering). This gives the right "swap-style"
    // behavior in both directions:
    //   - dragging A onto B in [A,B,C] (source < target) → [B,A,C]
    //   - dragging C onto A in [A,B,C] (source > target) → [C,A,B]
    // (After removing source, what was at targetIdx is now at targetIdx-1
    //  in the source-before case, so splicing at targetIdx inserts AFTER it.)
    const next = {};
    for (const [sid, keys] of Object.entries(semesters)) {
      next[sid] = keys.filter(k => k !== drag.key);
    }
    const insertIdx = Math.min(targetIdx, next[targetSemId].length);
    next[targetSemId].splice(insertIdx, 0, drag.key);
    onUpdateProfile({ semesters: next });
  }, [semesters, onUpdateProfile]);

  // dragover on the semester container (not a card) → append to that
  // semester. Only fires when the pointer is on padding / empty space.
  const handleSemDragOver = useCallback((e, semId) => {
    const drag = draggingRef.current;
    if (!drag) return;
    if (e.target.closest('.pb-card')) return; // card handler will deal with it
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';

    const targetList = semesters[semId] || [];
    // Already last in this semester → no-op.
    if (targetList[targetList.length - 1] === drag.key) return;

    const next = {};
    for (const [sid, keys] of Object.entries(semesters)) {
      next[sid] = keys.filter(k => k !== drag.key);
    }
    next[semId] = [...(next[semId] || []), drag.key];
    onUpdateProfile({ semesters: next });
  }, [semesters, onUpdateProfile]);

  // dragover on the sidebar list → drag back to bank (removes from
  // whichever semester it's in).
  const handleSidebarDragOver = useCallback((e) => {
    const drag = draggingRef.current;
    if (!drag) return;
    // Already in the bank.
    let inSem = false;
    for (const keys of Object.values(semesters)) {
      if (keys.includes(drag.key)) { inSem = true; break; }
    }
    if (!inSem) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';

    const next = {};
    for (const [sid, keys] of Object.entries(semesters)) {
      next[sid] = keys.filter(k => k !== drag.key);
    }
    onUpdateProfile({ semesters: next });
  }, [semesters, onUpdateProfile]);

  const handleAddCourse = useCallback((semId) => {
    setAddModal(semId);
    setAddQuery('');
  }, []);

  const confirmAdd = (key) => {
    if (!addModal) return;
    const updated = { ...semesters, [addModal]: [...(semesters[addModal] || []), key] };
    onUpdateProfile({ semesters: updated });
    setAddModal(null);
  };

  const handleRebalance = useCallback(async () => {
    const majorIds = profile.majorIds || [];
    if (!majorIds.length) return;
    try {
      const res = await fetch(`${API_BASE}/api/generate-plan`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          major_ids: majorIds,
          transfer_term: profile.transferTerm,
          use_summer: profile.useSummer,
          summer_load: profile.summerLoad || 'light',
          ge_selections: profile.geSelections || {},
        }),
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const plan = await res.json();
      window.MOCK_COURSES = {};
      for (const [key, info] of Object.entries(plan.courses || {})) {
        window.MOCK_COURSES[key] = { title: info.title, units: info.units, difficulty: 'medium', available: 'FS' };
      }
      onUpdateProfile({ semesters: plan.semesters });
      setSidebarApiVer(v => v + 1);
    } catch (e) {
      console.error('Rebalance failed:', e);
    }
  }, [profile.majorIds, profile.transferTerm, profile.useSummer, profile.summerLoad, profile.geSelections, onUpdateProfile]);

  const handleExportPDF = useCallback(() => {
    const SEM_LABEL = { fall: 'Fall', spring: 'Spring', summer: 'Summer' };
    const semSortKey = id => {
      const [t, y] = id.split('-');
      return parseInt(y) * 10 + ({ spring: 0, summer: 1, fall: 2 }[t] ?? 9);
    };
    const sortedSems = Object.keys(semesters).sort((a, b) => semSortKey(a) - semSortKey(b));
    const planTotal = Object.values(semesters).flat()
      .reduce((s, k) => s + ((window.MOCK_COURSES[k] || {}).units || 3), 0);

    let html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>TransferLens Plan</title>
<style>
body{font-family:system-ui,sans-serif;margin:48px;color:#1C1714;max-width:680px}
h1{font-size:1.3rem;font-weight:700;margin:0 0 4px}
.meta{font-size:0.82rem;color:#6B5E55;margin-bottom:36px}
.sem{margin-bottom:28px}
.sem-title{font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#9E8E84;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #E0D8CE}
.row{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid #F4EFE6;font-size:0.85rem}
.key{font-weight:700}
.title{color:#6B5E55;margin-left:10px}
.units{color:#9E8E84;white-space:nowrap}
.sem-total{font-size:0.75rem;font-weight:600;color:#6B5E55;text-align:right;margin-top:4px}
</style></head><body>`;
    html += `<h1>Transfer Plan</h1>`;
    html += `<div class="meta">`;
    html += selections?.map(s => `${s.school?.abbr ?? ''} · ${s.major}`).join(' &nbsp;|&nbsp; ') || '';
    html += ` &nbsp;·&nbsp; Transfer: ${transferTerm || ''} &nbsp;·&nbsp; ${planTotal} total units</div>`;

    for (const semId of sortedSems) {
      const cs = semesters[semId] || [];
      const [type, yr] = semId.split('-');
      const label = `${SEM_LABEL[type] || type} ${yr}`;
      const semUnits = cs.reduce((s, k) => s + ((window.MOCK_COURSES[k] || {}).units || 3), 0);
      html += `<div class="sem"><div class="sem-title">${label}</div>`;
      for (const key of cs) {
        const c = window.MOCK_COURSES[key] || {};
        html += `<div class="row"><span><span class="key">${key}</span><span class="title">${c.title || ''}</span></span><span class="units">${c.units || 3} units</span></div>`;
      }
      html += `<div class="sem-total">${semUnits} units</div></div>`;
    }
    html += `</body></html>`;

    const w = window.open('', '_blank');
    if (!w) { alert('Allow popups for this page to export PDF.'); return; }
    w.document.write(html);
    w.document.close();
    setTimeout(() => { w.focus(); w.print(); }, 300);
  }, [semesters, selections, transferTerm]);

  const unplacedCourses = useMemo(
    () => Object.keys(window.MOCK_COURSES).filter(k => !allPlacedKeys.has(k)),
    [allPlacedKeys, sidebarApiVer]
  );
  const filteredSidebar = useMemo(() => {
    if (sidebarQuery) {
      const q = sidebarQuery.toLowerCase();
      return Object.keys(window.MOCK_COURSES).filter(k =>
        k.toLowerCase().includes(q) || (window.MOCK_COURSES[k].title || '').toLowerCase().includes(q)
      );
    }
    return unplacedCourses.slice(0, 24);
  }, [sidebarQuery, unplacedCourses, sidebarApiVer]);

  const addModalResults = addQuery
    ? Object.keys(window.MOCK_COURSES).filter(k =>
        !allPlacedKeys.has(k) &&
        (k.toLowerCase().includes(addQuery.toLowerCase()) ||
         (window.MOCK_COURSES[k].title || '').toLowerCase().includes(addQuery.toLowerCase())))
    : unplacedCourses.slice(0, 12);

  const ccData = (window.MOCK_CCS.find(c => c.id === (profile.cc || {}).id)) || {};
  const unitCap   = ccData.unitCap   || 18;
  const summerCap = ccData.summerCap || 9;

  return (
    <div className="plan-view">
      {/* Top bar */}
      <div className="plan-topbar">
        <div className="plan-topbar-left">
          <button className="plan-sidebar-toggle" onClick={() => setSidebarOpen(v => !v)} title="Toggle sidebar">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="1.5" rx="0.75" fill="currentColor"/><rect x="2" y="7.25" width="12" height="1.5" rx="0.75" fill="currentColor"/><rect x="2" y="10.5" width="12" height="1.5" rx="0.75" fill="currentColor"/></svg>
          </button>
          <div className="plan-selections">
            {selections.map((sel, i) => (
              <span key={i} className="plan-selection-pill">
                {sel.school.abbr} · {sel.major}
                <button className="plan-pill-x" onClick={() => {
                  onUpdateProfile({ selections: selections.filter((_, j) => j !== i) });
                }}>×</button>
              </span>
            ))}
            <button className="plan-add-school-btn" onClick={() => {}}>+ Add school</button>
          </div>
        </div>
        <div className="plan-topbar-right">
          <span className="plan-stat">{totalUnits} total units</span>
          {gpa !== null && (
            <>
              <span className="plan-stat plan-stat-divider">·</span>
              <span className="plan-stat plan-gpa" style={{ color: gpaColor(gpa), fontWeight: 600 }}
                title={`Cumulative GPA from ${gradedCount} graded course${gradedCount !== 1 ? 's' : ''}`}>
                {gpa} GPA
              </span>
            </>
          )}
          {honorsGpa !== null && (
            <>
              <span className="plan-stat plan-stat-divider">·</span>
              <span className="plan-stat plan-gpa" style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'default' }}
                onMouseEnter={e => e.currentTarget.querySelector('.hgpa-tip').style.opacity = '1'}
                onMouseLeave={e => e.currentTarget.querySelector('.hgpa-tip').style.opacity = '0'}
              >
                <span style={{ color: honorsGpaColor(honorsGpa), fontWeight: 600 }}>{honorsGpa}</span>
                <span style={{ color: 'var(--text-3)', fontWeight: 500, fontSize: '0.78rem' }}>H-GPA</span>
                <span style={{
                  fontSize: '0.65rem', fontWeight: 700,
                  padding: '1px 6px', borderRadius: '999px',
                  background: parseFloat(honorsGpa) >= 3.5 ? 'var(--green-bg)' : parseFloat(honorsGpa) >= 3.25 ? 'var(--orange-bg)' : 'var(--red-bg)',
                  color: honorsGpaColor(honorsGpa),
                }}>{honorsGpaLabel(honorsGpa)}</span>
                {/* clean tooltip */}
                <span className="hgpa-tip" style={{
                  opacity: 0, transition: 'opacity 0.15s',
                  position: 'absolute', top: 'calc(100% + 8px)', right: 0,
                  background: 'var(--bg)', border: '1px solid var(--divider)',
                  borderRadius: 'var(--radius)', padding: '10px 13px',
                  boxShadow: 'var(--shadow)', whiteSpace: 'nowrap',
                  fontSize: '0.76rem', lineHeight: 1.7, color: 'var(--text-2)',
                  pointerEvents: 'none', zIndex: 50,
                }}>
                  <div style={{ fontWeight: 700, color: 'var(--text)', marginBottom: 4 }}>Honors GPA Requirements</div>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
                    {[
                      { label: 'IVC Honors benefits',  min: 3.25 },
                      { label: 'UCLA TAP',              min: 3.50 },
                      { label: 'IVC Honors-to-Honors', min: 3.70 },
                    ].map(({ label, min }) => {
                      const g = parseFloat(honorsGpa);
                      const ok = g >= min;
                      return (
                        <div key={label} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                          <span style={{ color: ok ? 'var(--green)' : 'var(--red)', fontWeight: 700, fontSize: '0.7rem' }}>{ok ? '✓' : '✗'}</span>
                          <span>{label}</span>
                          <span style={{ marginLeft: 'auto', fontWeight: 600, color: 'var(--text)', paddingLeft: 16 }}>{min.toFixed(2)}</span>
                        </div>
                      );
                    })}
                  </div>
                  <div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--divider)', fontSize: '0.72rem', color: 'var(--text-3)' }}>
                    From {honorsGradedCount} graded honors course{honorsGradedCount !== 1 ? 's' : ''}
                  </div>
                </span>
              </span>
            </>
          )}
          <span className="plan-stat plan-stat-divider">·</span>
          <span className="plan-stat">{semIds.length} semesters</span>
          <span className="plan-stat plan-stat-divider">·</span>
          <select className="plan-term-select" value={transferTerm} onChange={e => onUpdateProfile({ transferTerm: e.target.value })}>
            {window.TRANSFER_TERMS.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
          </select>
          <button
            className="plan-export-btn"
            onClick={() => onUpdateProfile({ useSummer: !profile.useSummer })}
            title={profile.useSummer ? 'Hide summer semesters' : 'Add summer semesters'}
            style={{ color: profile.useSummer ? 'var(--accent)' : undefined, borderColor: profile.useSummer ? 'var(--accent)' : undefined }}
          >
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
              <circle cx="7" cy="7" r="3" stroke="currentColor" strokeWidth="1.5"/>
              <path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.93 2.93l1.06 1.06M10.01 10.01l1.06 1.06M2.93 11.07l1.06-1.06M10.01 3.99l1.06-1.06" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
            </svg>
            {profile.useSummer ? 'Summer on' : 'Summer'}
          </button>
          <button className="plan-export-btn" onClick={handleRebalance} title="Re-run the scheduler on all current courses">
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7a5 5 0 0110 0" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><path d="M12 5v2h-2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M12 7a5 5 0 01-10 0" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><path d="M2 9V7h2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
            Rebalance
          </button>
          <button className="plan-export-btn" onClick={handleExportPDF}>
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2v7M4 6l3 3 3-3M2 11h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
            Export PDF
          </button>
          <button className="plan-restart-btn" onClick={onReset}>Restart</button>
        </div>
      </div>

      <div className="plan-body">
        {/* Sidebar */}
        <div className={`plan-sidebar ${sidebarOpen ? '' : 'collapsed'}`}>
            <div className="plan-sidebar-head">
              <div className="plan-sidebar-title">Course Bank</div>
              <div className="plan-sidebar-sub">{unplacedCourses.length} unplaced</div>
            </div>
            <div className="plan-sidebar-search">
              {sidebarLoading
                ? <span className="pulse" style={{ width: 13, height: 13, borderRadius: '50%', display: 'inline-block', background: 'var(--accent)', flexShrink: 0 }} />
                : <svg width="13" height="13" viewBox="0 0 13 13" fill="none"><circle cx="5.5" cy="5.5" r="3.5" stroke="currentColor" strokeWidth="1.4"/><path d="M8.5 8.5l2.5 2.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
              }
              <input placeholder="Search courses…" value={sidebarQuery} onChange={e => setSidebarQuery(e.target.value)} />
            </div>
            <div className="plan-sidebar-list" onDragOver={handleSidebarDragOver} onDrop={(e) => e.preventDefault()}>
              {filteredSidebar.map(k => {
                const c = window.MOCK_COURSES[k] || {};
                const isDragging = draggingKey === k;
                return (
                  <div
                    key={k}
                    className={`plan-sidebar-item ${isDragging ? 'dragging' : ''}`}
                    data-course-key={k}
                    draggable={true}
                    onDragStart={(e) => handleDragStart(e, k, null)}
                    onDragEnd={handleDragEnd}
                  >
                    <span
                      className="plan-sidebar-item-grip"
                      title="Drag to a semester"
                      aria-hidden="true"
                    >
                      <svg width="10" height="14" viewBox="0 0 10 14" fill="none">
                        <circle cx="3" cy="3" r="1.5" fill="currentColor"/><circle cx="7" cy="3" r="1.5" fill="currentColor"/>
                        <circle cx="3" cy="7" r="1.5" fill="currentColor"/><circle cx="7" cy="7" r="1.5" fill="currentColor"/>
                        <circle cx="3" cy="11" r="1.5" fill="currentColor"/><circle cx="7" cy="11" r="1.5" fill="currentColor"/>
                      </svg>
                    </span>
                    <DiffDot courseKey={k} />
                    <span className="plan-sidebar-key">{k}</span>
                    <span className="plan-sidebar-units">{c.units || 3} units</span>
                  </div>
                );
              })}
              {filteredSidebar.length === 0 && <div className="plan-sidebar-empty">All courses placed</div>}
            </div>
          </div>

        {/* Semesters */}
        <div className="plan-sems">
          <button
            className="plan-add-sem-btn"
            onClick={handleAddPrevious}
            style={{ margin: '12px 0 24px' }}
            title="Add a semester before the first one"
          >
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
              <path d="M7 2v10M2 7h10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
            </svg>
            Add previous semester
          </button>
          {semIds.map(semId => (
            <SemesterRow
              key={semId}
              semId={semId}
              courses={semesters[semId] || []}
              courseStatuses={courseStatuses}
              courseGrades={courseGrades}
              isTransferred={isTransferred(semId)}
              onStatusChange={handleStatusChange}
              onGradeChange={handleGradeChange}
              onRemoveCourse={handleRemoveCourse}
              onAddCourse={handleAddCourse}
              onCardDragStart={handleDragStart}
              onCardDragOver={handleCardDragOver}
              onCardDragEnd={handleDragEnd}
              onSemDragOver={handleSemDragOver}
              draggingKey={draggingKey}
              onDeleteSemester={handleDeleteSemester}
              unitCap={unitCap}
              summerCap={summerCap}
            />
          ))}
          <button
            className="plan-add-sem-btn"
            onClick={handleAddSemester}
            title="Add another semester"
          >
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
              <path d="M7 2v10M2 7h10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
            </svg>
            Add another semester
          </button>
        </div>
      </div>

      {/* Add course modal */}
      {addModal && (
        <div className="modal-overlay" onClick={() => setAddModal(null)}>
          <div className="modal-box" onClick={e => e.stopPropagation()}>
            <div className="modal-head">
              <div className="modal-title">Add course to {semLabel(addModal)}</div>
              <button className="modal-close" onClick={() => setAddModal(null)}>×</button>
            </div>
            <div className="modal-search-wrap">
              {addLoading
                ? <span className="pulse" style={{ width: 14, height: 14, borderRadius: '50%', display: 'inline-block', background: 'var(--accent)', flexShrink: 0 }} />
                : <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="6" cy="6" r="4" stroke="currentColor" strokeWidth="1.4"/><path d="M9.5 9.5l2.5 2.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
              }
              <input autoFocus placeholder="Search courses…" value={addQuery} onChange={e => setAddQuery(e.target.value)} />
            </div>
            <div className="modal-results">
              {addModalResults.map(k => {
                const c = window.MOCK_COURSES[k] || {};
                return (
                  <button key={k} className="modal-result-row" onClick={() => confirmAdd(k)}>
                    <DiffDot courseKey={k} />
                    <span className="modal-result-key">{k}</span>
                    <span className="modal-result-title">{c.title}</span>
                    <span className="modal-result-units">{c.units || 3} units</span>
                  </button>
                );
              })}
              {addModalResults.length === 0 && <div className="modal-empty">No courses found</div>}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { PlanView });
