/* Progress — Chairside course completion and saved roleplay session history.
 *
 * Reads /api/sessions for the list, /api/sessions/:id for detail (when a row
 * is expanded), and /api/sessions/:id DELETE for removal. All endpoints
 * scope by verified identity server-side; anonymous users see an empty
 * list and cannot delete.
 *
 * No display name editing, no leaderboard, no settings — those are deferred
 * indefinitely. This page is operational: a record of work done.
 */

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

  const SCRIPT_ROLE_LABELS = {
    tc: "TC",
    fd: "FD",
    hyg: "Hyg",
    doc: "Doc",
    own: "Owner",
  };
  const SCRIPT_ROLE_ORDER = ["tc", "fd", "hyg", "doc", "own"];
  const DIFFICULTY_ORDER = ["beginner", "intermediate", "expert"];
  const DIFFICULTY_LABELS = {
    beginner: "Beginner",
    intermediate: "Intermediate",
    expert: "Expert",
  };
  const PAGE_SIZE = 20;

  // ─── Helpers ────────────────────────────────────────────────────────

  function scoreBand({ score, accent, tokens }) {
    if (score === null || score === undefined) return tokens.mute;
    if (score >= 85) return "#1f6f4a";
    if (score >= 60) return accent.c;
    return "#a8443c";
  }

  function roleIdFromScriptId(scriptId) {
    if (typeof scriptId !== "string") return null;
    const prefix = scriptId.split("-")[0];
    return SCRIPT_ROLE_LABELS[prefix] ? prefix : null;
  }

  function formatTimestamp(ms) {
    if (!ms) return "";
    const d = new Date(ms);
    const date = d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
    const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
    return `${date} · ${time}`;
  }

  function formatBreakdown(b) {
    if (!b) return "";
    const parts = [`${b.passed}p`];
    if (b.partial > 0) parts.push(`${b.partial}pa`);
    if (b.missed > 0) parts.push(`${b.missed}m`);
    parts.push(`${b.total}t`);
    return parts.join(" · ");
  }

  // ─── Root mount ─────────────────────────────────────────────────────

  function MyPracticeRoot() {
    const tokens = window.CHAIRSIDE_THEMES.warm;
    const accent = window.CHAIRSIDE_ACCENTS.teal;
    return (
      <>
        <window.ChairsideStyle tokens={tokens} accent={accent} />
        <window.ChairsideHeader
          product="Progress"
          productHref="Chairside-My-Practice.html"
          tokens={tokens}
          accent={accent}
        />
        <MyPracticeApp tokens={tokens} accent={accent} />
      </>
    );
  }

  // ─── App ────────────────────────────────────────────────────────────

  function MyPracticeApp({ tokens, accent }) {
    const [sessions, setSessions] = useState([]);
    const [cursor, setCursor] = useState(null);
    const [hasMore, setHasMore] = useState(false);
    const [loading, setLoading] = useState(true);
    const [loadingMore, setLoadingMore] = useState(false);
    const [error, setError] = useState(null);

    const [roleFilter, setRoleFilter] = window.useChairsideStored("mp.filter.role", "all");
    const [difficultyFilter, setDifficultyFilter] = window.useChairsideStored("mp.filter.diff", "all");
    const [expandedId, setExpandedId] = useState(null);
    const [courseProgress] = window.useSyncedStore("chairside.progress", {});

    // Re-fetch on filter change. Cursor stays null because filters reset
    // pagination — the server applies them, so the cursor from a different
    // filter set wouldn't be coherent.
    const fetchFirstPage = useCallback(async () => {
      setLoading(true);
      setError(null);
      try {
        const params = { limit: PAGE_SIZE };
        if (difficultyFilter !== "all") params.difficulty = difficultyFilter;
        // Role filter is client-side: roleId is derived from scriptId prefix,
        // and the server doesn't index by role. We fetch the full filtered
        // page and prune locally. Acceptable at expected volume.
        const result = await window.RoleplayAPI.listSavedSessions(params);
        setSessions(result.sessions || []);
        setCursor(result.nextCursor || null);
        setHasMore(Boolean(result.nextCursor));
      } catch (e) {
        setError(humanizeError(e));
        setSessions([]);
        setCursor(null);
        setHasMore(false);
      } finally {
        setLoading(false);
      }
    }, [difficultyFilter]);

    useEffect(() => {
      fetchFirstPage();
    }, [fetchFirstPage]);

    const loadMore = useCallback(async () => {
      if (!cursor || loadingMore) return;
      setLoadingMore(true);
      try {
        const params = { limit: PAGE_SIZE, cursor };
        if (difficultyFilter !== "all") params.difficulty = difficultyFilter;
        const result = await window.RoleplayAPI.listSavedSessions(params);
        setSessions((prev) => [...prev, ...(result.sessions || [])]);
        setCursor(result.nextCursor || null);
        setHasMore(Boolean(result.nextCursor));
      } catch (e) {
        setError(humanizeError(e));
      } finally {
        setLoadingMore(false);
      }
    }, [cursor, loadingMore, difficultyFilter]);

    const handleDelete = useCallback(async (sessionId) => {
      // Optimistic remove; revert on failure.
      const previous = sessions;
      setSessions((prev) => prev.filter((s) => s.sessionId !== sessionId));
      if (expandedId === sessionId) setExpandedId(null);
      try {
        await window.RoleplayAPI.deleteSavedSession(sessionId);
      } catch (e) {
        setSessions(previous);
        setError(humanizeError(e));
      }
    }, [sessions, expandedId]);

    // Filter sessions client-side by role.
    const visibleSessions = useMemo(() => {
      if (roleFilter === "all") return sessions;
      return sessions.filter((s) => roleIdFromScriptId(s.scriptId) === roleFilter);
    }, [sessions, roleFilter]);

    const filtersActive = roleFilter !== "all" || difficultyFilter !== "all";
    const total = visibleSessions.length;

    return (
      <main style={{ maxWidth: 1040, margin: "0 auto", padding: "40px 24px 80px" }}>
        <ProgressHeader tokens={tokens} accent={accent} />
        <CourseProgressOverview tokens={tokens} accent={accent} progress={courseProgress} />
        <Header tokens={tokens} accent={accent} total={total} loading={loading} />

        <Filters
          tokens={tokens}
          accent={accent}
          roleFilter={roleFilter}
          setRoleFilter={setRoleFilter}
          difficultyFilter={difficultyFilter}
          setDifficultyFilter={setDifficultyFilter}
        />

        {error && (
          <div style={{
            margin: "16px 0",
            padding: "12px 16px",
            background: "#fcebea",
            border: `1px solid #efc7c2`,
            borderRadius: 10,
            color: "#a8443c",
            fontSize: 13,
          }}>
            {error}
          </div>
        )}

        {loading && sessions.length === 0 ? (
          <LoadingState tokens={tokens} />
        ) : visibleSessions.length === 0 ? (
          <EmptyState
            tokens={tokens}
            accent={accent}
            filtersActive={filtersActive}
            onClearFilters={() => { setRoleFilter("all"); setDifficultyFilter("all"); }}
          />
        ) : (
          <ul style={{ listStyle: "none", padding: 0, margin: "20px 0 0", display: "grid", gap: 10 }}>
            {visibleSessions.map((s) => (
              <SessionRow
                key={s.sessionId}
                session={s}
                tokens={tokens}
                accent={accent}
                expanded={expandedId === s.sessionId}
                onToggleExpand={() => setExpandedId((prev) => (prev === s.sessionId ? null : s.sessionId))}
                onDelete={() => handleDelete(s.sessionId)}
              />
            ))}
          </ul>
        )}

        {hasMore && !loading && (
          <div style={{ marginTop: 20, display: "flex", justifyContent: "center" }}>
            <button
              type="button"
              className="btn"
              onClick={loadMore}
              disabled={loadingMore}
              style={{
                padding: "10px 20px",
                fontSize: 13,
                color: tokens.soft,
                opacity: loadingMore ? 0.55 : 1,
                cursor: loadingMore ? "wait" : "pointer",
              }}
            >
              {loadingMore ? "Loading…" : `Load ${PAGE_SIZE} more sessions`}
            </button>
          </div>
        )}
      </main>
    );
  }

  // ─── Header ─────────────────────────────────────────────────────────

  function ProgressHeader({ tokens, accent }) {
    return (
      <div style={{ marginBottom: 36 }}>
        <div className="eyebrow" style={{ color: accent.c, marginBottom: 8 }}>Your training</div>
        <h1 style={{ fontSize: 42, lineHeight: 1.05, margin: 0 }}>Progress</h1>
        <p style={{ margin: "10px 0 0", color: tokens.soft, fontSize: 15, lineHeight: 1.6, maxWidth: 620 }}>
          Follow your course completion and every saved AI practice session in one place.
        </p>
      </div>
    );
  }

  function CourseProgressOverview({ tokens, accent, progress }) {
    const course = window.CHAIRSIDE_COURSE;
    if (!course || !course.modules) return null;

    const modules = course.modules;
    const lessons = modules.flatMap((module) => module.lessons.map((lesson) => ({ ...lesson, module })));
    const totalLessons = lessons.length;
    const completedLessons = lessons.filter(({ id }) => progress[id] && progress[id].done).length;
    const drillLessons = lessons.filter(({ drill }) => drill);
    const completedDrills = drillLessons.filter(({ id }) => progress[id] && progress[id].drillDone).length;
    const completedModules = modules.filter((module) => module.lessons.every(({ id }) => progress[id] && progress[id].done)).length;
    const percent = totalLessons ? Math.round((completedLessons / totalLessons) * 100) : 0;
    const nextLesson = lessons.find(({ id }) => !(progress[id] && progress[id].done)) || lessons[0];
    const complete = totalLessons > 0 && completedLessons === totalLessons;
    const nextHref = complete
      ? "Chairside.html#view=curriculum"
      : `Chairside.html#view=lesson&module=${nextLesson.module.id}&lesson=${nextLesson.id}`;

    const stats = [
      { value: `${completedLessons}/${totalLessons}`, label: "Lessons" },
      { value: `${completedModules}/${modules.length}`, label: "Modules" },
      { value: `${completedDrills}/${drillLessons.length}`, label: "Drills" },
    ];

    return (
      <section aria-labelledby="course-progress-title" style={{ padding: "28px 0 32px", marginBottom: 42, borderTop: `1px solid ${tokens.line}`, borderBottom: `1px solid ${tokens.line}` }}>
        <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 24, flexWrap: "wrap" }}>
          <div style={{ minWidth: 0, flex: "1 1 420px" }}>
            <div className="eyebrow" style={{ color: accent.c, marginBottom: 8 }}>Chairside course</div>
            <h2 id="course-progress-title" style={{ fontSize: 28, lineHeight: 1.15, margin: 0 }}>{percent}% complete</h2>
            <div style={{ height: 7, marginTop: 18, background: tokens.surface2, borderRadius: 4, overflow: "hidden", maxWidth: 560 }}>
              <div style={{ height: "100%", width: `${percent}%`, background: accent.c, transition: "width .3s ease" }} />
            </div>
            <p style={{ margin: "12px 0 0", color: tokens.soft, fontSize: 14, lineHeight: 1.5 }}>
              {complete ? "All course lessons are complete." : `Next: Module ${nextLesson.module.number}, ${nextLesson.title}`}
            </p>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(72px, 1fr))", gap: 12, flex: "0 1 330px", width: "100%" }}>
            {stats.map((stat) => (
              <div key={stat.label} style={{ paddingLeft: 14, borderLeft: `1px solid ${tokens.line}` }}>
                <div style={{ fontFamily: "Fraunces, serif", fontSize: 25, lineHeight: 1.1, color: tokens.text }}>{stat.value}</div>
                <div style={{ marginTop: 5, color: tokens.mute, fontSize: 11, textTransform: "uppercase", letterSpacing: "0.08em" }}>{stat.label}</div>
              </div>
            ))}
          </div>
        </div>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginTop: 24 }}>
          <a className="btn primary" href={nextHref} style={{ textDecoration: "none" }}>{complete ? "Review modules" : "Continue course"} →</a>
          <a className="btn" href="Chairside.html#view=outcomes" style={{ textDecoration: "none" }}>View course detail →</a>
        </div>
      </section>
    );
  }

  function Header({ tokens, accent, total, loading }) {
    return (
      <div style={{ marginBottom: 24 }}>
        <div className="eyebrow" style={{ color: accent.c, marginBottom: 8 }}>Saved sessions</div>
        <h2 style={{ fontSize: 30, lineHeight: 1.1, margin: 0 }}>AI practice history</h2>
        <div style={{ marginTop: 6, color: tokens.soft, fontSize: 14 }}>
          {loading ? "Loading…" : `${total} ${total === 1 ? "session" : "sessions"}`}
        </div>
      </div>
    );
  }

  // ─── Filters ────────────────────────────────────────────────────────

  function Filters({ tokens, accent, roleFilter, setRoleFilter, difficultyFilter, setDifficultyFilter }) {
    return (
      <div style={{ display: "grid", gap: 10 }}>
        <ChipRow
          tokens={tokens}
          accent={accent}
          ariaLabel="Filter by role"
          value={roleFilter}
          onChange={setRoleFilter}
          options={[
            { id: "all", label: "All" },
            ...SCRIPT_ROLE_ORDER.map((r) => ({ id: r, label: SCRIPT_ROLE_LABELS[r] })),
          ]}
        />
        <ChipRow
          tokens={tokens}
          accent={accent}
          ariaLabel="Filter by difficulty"
          value={difficultyFilter}
          onChange={setDifficultyFilter}
          options={[
            { id: "all", label: "All" },
            ...DIFFICULTY_ORDER.map((d) => ({ id: d, label: DIFFICULTY_LABELS[d] })),
          ]}
        />
      </div>
    );
  }

  function ChipRow({ tokens, accent, options, value, onChange, ariaLabel }) {
    return (
      <div role="group" aria-label={ariaLabel} style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
        {options.map((opt) => {
          const active = value === opt.id;
          return (
            <button
              key={opt.id}
              type="button"
              onClick={() => onChange(opt.id)}
              aria-pressed={active}
              style={{
                padding: "6px 12px",
                fontSize: 12,
                borderRadius: 999,
                border: `1px solid ${active ? accent.c : tokens.line}`,
                background: active ? `color-mix(in srgb, ${accent.c} 10%, transparent)` : "transparent",
                color: active ? accent.c : tokens.soft,
                cursor: "pointer",
                whiteSpace: "nowrap",
              }}
            >
              {opt.label}
            </button>
          );
        })}
      </div>
    );
  }

  // ─── Session row ────────────────────────────────────────────────────

  function SessionRow({ session, tokens, accent, expanded, onToggleExpand, onDelete }) {
    const roleId = roleIdFromScriptId(session.scriptId);
    const roleLabel = roleId ? SCRIPT_ROLE_LABELS[roleId] : "—";
    const difficultyLabel = DIFFICULTY_LABELS[session.difficulty] || session.difficulty;
    const isAbandoned = session.status === "abandoned";
    const band = scoreBand({ score: session.score, accent, tokens });

    return (
      <li
        style={{
          border: `1px solid ${tokens.line}`,
          borderRadius: 12,
          background: tokens.surface,
          overflow: "hidden",
          transition: "border-color .15s",
        }}
      >
        <RowHeader
          session={session}
          tokens={tokens}
          accent={accent}
          expanded={expanded}
          onToggleExpand={onToggleExpand}
          onDelete={onDelete}
          roleLabel={roleLabel}
          difficultyLabel={difficultyLabel}
          isAbandoned={isAbandoned}
          band={band}
        />
        {expanded && (
          <ExpandedDetail
            sessionId={session.sessionId}
            tokens={tokens}
            accent={accent}
          />
        )}
      </li>
    );
  }

  function RowHeader({ session, tokens, accent, expanded, onToggleExpand, onDelete, roleLabel, difficultyLabel, isAbandoned, band }) {
    const [menuOpen, setMenuOpen] = useState(false);
    const [confirmingDelete, setConfirmingDelete] = useState(false);
    const menuRef = useRef(null);

    useEffect(() => {
      if (!menuOpen) return;
      const onDocClick = (e) => {
        if (menuRef.current && !menuRef.current.contains(e.target)) {
          setMenuOpen(false);
        }
      };
      document.addEventListener("mousedown", onDocClick);
      return () => document.removeEventListener("mousedown", onDocClick);
    }, [menuOpen]);

    return (
      <div style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 8, alignItems: "center", padding: "14px 16px" }}>
        <button
          type="button"
          onClick={onToggleExpand}
          aria-expanded={expanded}
          aria-label={expanded ? "Collapse session" : "Expand session"}
          style={{
            background: "none",
            border: "none",
            padding: 0,
            textAlign: "left",
            cursor: "pointer",
            display: "grid",
            gap: 4,
            color: tokens.text,
            minWidth: 0,
          }}
        >
          <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
            <span style={{
              display: "inline-block",
              width: 14,
              fontSize: 12,
              color: tokens.mute,
              transition: "transform .15s",
              transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
            }}>›</span>
            <span style={{
              fontFamily: "Fraunces, serif",
              fontSize: 17,
              lineHeight: 1.25,
              fontWeight: 500,
              letterSpacing: "-0.01em",
              whiteSpace: "nowrap",
              overflow: "hidden",
              textOverflow: "ellipsis",
              minWidth: 0,
            }}>
              {session.scriptTitle || session.scriptId}
            </span>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 10, paddingLeft: 24, fontSize: 12, color: tokens.soft, flexWrap: "wrap" }}>
            <span className="mono" style={{ fontSize: 10, letterSpacing: "0.12em", color: tokens.mute }}>{roleLabel}</span>
            <span style={{ color: tokens.line }}>·</span>
            <span>{difficultyLabel}</span>
            <span style={{ color: tokens.line }}>·</span>
            {isAbandoned ? (
              <span style={{ fontStyle: "italic", color: tokens.mute }}>didn't finish</span>
            ) : (
              <>
                <span style={{ color: band, fontWeight: 500 }}>{session.score}%</span>
                {session.scoreBreakdown && (
                  <span className="mono" style={{ color: tokens.mute, fontSize: 10, letterSpacing: "0.08em" }}>
                    ({formatBreakdown(session.scoreBreakdown)})
                  </span>
                )}
              </>
            )}
            <span style={{ color: tokens.line }}>·</span>
            <span style={{ color: tokens.mute }}>{formatTimestamp(session.startedAt)}</span>
          </div>
        </button>

        <div ref={menuRef} style={{ position: "relative" }}>
          <button
            type="button"
            onClick={(e) => { e.stopPropagation(); setMenuOpen((v) => !v); setConfirmingDelete(false); }}
            aria-label="Session actions"
            aria-haspopup="menu"
            aria-expanded={menuOpen}
            style={{
              background: "transparent",
              border: `1px solid ${tokens.line}`,
              borderRadius: 8,
              width: 28,
              height: 28,
              color: tokens.soft,
              cursor: "pointer",
              display: "inline-flex",
              alignItems: "center",
              justifyContent: "center",
              fontSize: 14,
              lineHeight: 1,
            }}
          >
            ⋯
          </button>
          {menuOpen && (
            <div
              role="menu"
              style={{
                position: "absolute",
                right: 0,
                top: "calc(100% + 6px)",
                minWidth: 220,
                background: tokens.surface,
                border: `1px solid ${tokens.line}`,
                borderRadius: 10,
                boxShadow: "0 12px 32px -10px rgba(0,0,0,.20)",
                zIndex: 20,
                overflow: "hidden",
              }}
            >
              {!confirmingDelete ? (
                <>
                  <a
                    role="menuitem"
                    href={`Chairside-Roles.html#role=${encodeURIComponent(roleIdFromScriptId(session.scriptId) || "tc")}&view=practice&script=${encodeURIComponent(session.scriptId)}`}
                    style={{ display: "block", padding: "10px 14px", fontSize: 13, color: tokens.text }}
                  >
                    Practice this script again
                  </a>
                  <button
                    type="button"
                    role="menuitem"
                    onClick={() => setConfirmingDelete(true)}
                    style={{
                      display: "block",
                      width: "100%",
                      padding: "10px 14px",
                      fontSize: 13,
                      textAlign: "left",
                      background: "none",
                      border: "none",
                      borderTop: `1px solid ${tokens.line}`,
                      color: "#a8443c",
                      cursor: "pointer",
                    }}
                  >
                    Delete this session
                  </button>
                </>
              ) : (
                <div style={{ padding: "12px 14px", display: "grid", gap: 10 }}>
                  <div style={{ fontSize: 13, color: tokens.text, lineHeight: 1.45 }}>
                    Delete this session? This cannot be undone.
                  </div>
                  <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
                    <button
                      type="button"
                      onClick={() => { setConfirmingDelete(false); setMenuOpen(false); }}
                      className="btn ghost"
                      style={{ padding: "6px 12px", fontSize: 12, color: tokens.soft }}
                    >
                      Cancel
                    </button>
                    <button
                      type="button"
                      onClick={() => { setMenuOpen(false); setConfirmingDelete(false); onDelete(); }}
                      style={{
                        padding: "6px 12px",
                        fontSize: 12,
                        background: "#a8443c",
                        color: "#fff",
                        border: "none",
                        borderRadius: 8,
                        cursor: "pointer",
                      }}
                    >
                      Delete
                    </button>
                  </div>
                </div>
              )}
            </div>
          )}
        </div>
      </div>
    );
  }

  // ─── Expanded detail (transcript + eval) ────────────────────────────

  function ExpandedDetail({ sessionId, tokens, accent }) {
    const [detail, setDetail] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      let alive = true;
      setLoading(true);
      setError(null);
      window.RoleplayAPI.getSavedSession(sessionId)
        .then((data) => { if (alive) setDetail(data); })
        .catch((e) => { if (alive) setError(humanizeError(e)); })
        .finally(() => { if (alive) setLoading(false); });
      return () => { alive = false; };
    }, [sessionId]);

    if (loading) {
      return (
        <div style={{ padding: "16px 24px", color: tokens.soft, fontSize: 13, borderTop: `1px solid ${tokens.line}` }}>
          Loading transcript…
        </div>
      );
    }
    if (error) {
      return (
        <div style={{ padding: "16px 24px", color: "#a8443c", fontSize: 13, borderTop: `1px solid ${tokens.line}` }}>
          {error}
        </div>
      );
    }
    if (!detail) return null;

    return (
      <div style={{ borderTop: `1px solid ${tokens.line}`, background: `color-mix(in srgb, ${accent.c} 3%, ${tokens.surface})` }}>
        <Transcript turns={detail.turns} tokens={tokens} accent={accent} />
        {detail.status === "completed" && detail.evaluation ? (
          <ReviewBlock
            evaluation={detail.evaluation}
            score={detail.score}
            breakdown={detail.scoreBreakdown}
            tokens={tokens}
            accent={accent}
            sessionId={sessionId}
            turns={detail.turns}
          />
        ) : (
          <div style={{ padding: "12px 24px 20px", color: tokens.mute, fontSize: 12, fontStyle: "italic" }}>
            Session ended before evaluation
          </div>
        )}
      </div>
    );
  }

  function Transcript({ turns, tokens, accent }) {
    if (!Array.isArray(turns) || turns.length === 0) {
      return (
        <div style={{ padding: "16px 24px", color: tokens.mute, fontSize: 12, fontStyle: "italic" }}>
          No transcript captured.
        </div>
      );
    }
    return (
      <div style={{ padding: "16px 24px", display: "flex", flexDirection: "column", gap: 10 }}>
        {turns.map((t, i) => (
          <TranscriptBubble key={i} turn={t} tokens={tokens} accent={accent} />
        ))}
      </div>
    );
  }

  function TranscriptBubble({ turn, tokens, accent }) {
    const isUser = turn.role === "user";
    return (
      <div style={{ display: "flex", justifyContent: isUser ? "flex-end" : "flex-start" }}>
        <div style={{
          maxWidth: "78%",
          padding: "10px 14px",
          background: isUser ? accent.c : `color-mix(in srgb, ${tokens.text} 5%, ${tokens.surface})`,
          color: isUser ? "#fff" : tokens.text,
          border: isUser ? "none" : `1px solid ${tokens.line}`,
          borderRadius: isUser ? "14px 14px 4px 14px" : "14px 14px 14px 4px",
          fontSize: 14,
          lineHeight: 1.55,
          whiteSpace: "pre-wrap",
        }}>
          {turn.content}
        </div>
      </div>
    );
  }

  function ReviewBlock({ evaluation, score, breakdown, tokens, accent, sessionId, turns }) {
    return (
      <div style={{ padding: "8px 24px 24px" }}>
        <ScoreHeadline score={score} breakdown={breakdown} tokens={tokens} accent={accent} />
        <div className="eyebrow" style={{ color: accent.c, marginBottom: 8, marginTop: 8 }}>Session review</div>
        <p style={{ fontSize: 15, lineHeight: 1.55, color: tokens.text, margin: 0 }}>{evaluation.summary}</p>

        <div className="eyebrow" style={{ color: tokens.mute, marginTop: 22, marginBottom: 10 }}>Rubric</div>
        <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 8 }}>
          {evaluation.rubricScores.map((row, i) => (
            <RubricRow key={i} row={row} tokens={tokens} />
          ))}
        </ul>

        {Array.isArray(evaluation.coachingPoints) && evaluation.coachingPoints.length > 0 && (
          <>
            <div className="eyebrow" style={{ color: tokens.mute, marginTop: 22, marginBottom: 10 }}>Coaching points</div>
            <ol style={{ paddingLeft: 18, margin: 0, display: "grid", gap: 6, color: tokens.text }}>
              {evaluation.coachingPoints.map((p, i) => (
                <li key={i} style={{ fontSize: 14, lineHeight: 1.55 }}>{p}</li>
              ))}
            </ol>
          </>
        )}

        {Array.isArray(evaluation.perTurnNotes) && evaluation.perTurnNotes.length > 0 && Array.isArray(turns) && sessionId && (
          <window.ReplayCoaching
            tokens={tokens}
            accent={accent}
            sessionId={sessionId}
            turns={turns}
            perTurnNotes={evaluation.perTurnNotes}
          />
        )}
      </div>
    );
  }

  function ScoreHeadline({ score, breakdown, tokens, accent }) {
    if (score === null || score === undefined) return null;
    const band = scoreBand({ score, accent, tokens });
    return (
      <div style={{ display: "flex", alignItems: "baseline", gap: 14, marginBottom: 8, flexWrap: "wrap" }}>
        <div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
          <span className="mono" style={{ color: tokens.mute, fontSize: 10, letterSpacing: "0.14em" }}>SCORE</span>
          <span style={{ fontFamily: "Fraunces, serif", fontSize: 30, lineHeight: 1, color: band, fontWeight: 500, letterSpacing: "-0.01em" }}>
            {score}%
          </span>
        </div>
        {breakdown && (
          <span style={{ fontSize: 12, color: tokens.soft, lineHeight: 1.4 }}>
            {breakdown.passed} passed
            {breakdown.partial > 0 && ` · ${breakdown.partial} partial`}
            {breakdown.missed > 0 && ` · ${breakdown.missed} missed`}
            {" · "}{breakdown.total} total
          </span>
        )}
      </div>
    );
  }

  function RubricRow({ row, tokens }) {
    const colors = {
      passed:  { fg: "#1f6f4a", bg: "#e6f3ec", glyph: "✓" },
      partial: { fg: "#8a6a18", bg: "#f8efd4", glyph: "◐" },
      missed:  { fg: "#a8443c", bg: "#fcebea", glyph: "✕" },
    };
    const c = colors[row.status] || colors.partial;
    return (
      <li style={{
        display: "grid", gridTemplateColumns: "28px 1fr", gap: 10,
        padding: "10px 12px",
        background: tokens.surface,
        border: `1px solid ${tokens.line}`,
        borderRadius: 10,
      }}>
        <div style={{
          width: 24, height: 24, borderRadius: "50%",
          background: c.bg, color: c.fg,
          display: "inline-flex", alignItems: "center", justifyContent: "center",
          fontSize: 12, fontWeight: 600,
        }}>{c.glyph}</div>
        <div>
          <div style={{ fontSize: 13, lineHeight: 1.4, color: tokens.text }}>{row.criterion}</div>
          {row.note && (
            <div style={{ fontSize: 12, color: tokens.soft, marginTop: 3, lineHeight: 1.5, fontStyle: "italic" }}>{row.note}</div>
          )}
        </div>
      </li>
    );
  }

  // ─── States ─────────────────────────────────────────────────────────

  function LoadingState({ tokens }) {
    return (
      <div style={{ marginTop: 24, padding: "40px 0", color: tokens.soft, fontSize: 13, textAlign: "center" }}>
        Loading your practice history…
      </div>
    );
  }

  function EmptyState({ tokens, accent, filtersActive, onClearFilters }) {
    if (filtersActive) {
      return (
        <div style={{
          marginTop: 24,
          padding: "32px 24px",
          textAlign: "center",
          border: `1px dashed ${tokens.line}`,
          borderRadius: 12,
          background: tokens.surface,
        }}>
          <p style={{ margin: 0, color: tokens.soft, fontSize: 14 }}>No sessions match these filters.</p>
          <button
            type="button"
            onClick={onClearFilters}
            className="btn"
            style={{ marginTop: 14, padding: "8px 16px", fontSize: 13, color: accent.c, borderColor: accent.c }}
          >
            Clear filters
          </button>
        </div>
      );
    }
    return (
      <div style={{
        marginTop: 24,
        padding: "32px 24px",
        textAlign: "center",
        border: `1px dashed ${tokens.line}`,
        borderRadius: 12,
        background: tokens.surface,
      }}>
        <p style={{ margin: 0, color: tokens.soft, fontSize: 14, lineHeight: 1.5 }}>
          Your practice sessions will appear here after you complete a roleplay.
        </p>
        <a
          href="Chairside-Roles.html"
          className="btn primary"
          style={{ marginTop: 14, display: "inline-flex", padding: "10px 20px", fontSize: 13 }}
        >
          Start a roleplay →
        </a>
      </div>
    );
  }

  function humanizeError(e) {
    if (!e) return "Something went wrong.";
    if (e.status === 429) return "You've hit the practice limit for this hour. Try again later.";
    if (e.status === 503) return "Saved sessions are temporarily unavailable.";
    return "Something went wrong loading your practice history.";
  }

  // ─── Export ─────────────────────────────────────────────────────────

  Object.assign(window, { MyPracticeRoot });
})();
