// Tests listing page + main app orchestrator
const { useState: useState_M, useEffect: useEffect_M } = React;

// Views that map to a ?view= URL param — clicked from the main nav and
// therefore participate in browser history. Transient in-test views
// (ResultsView, TestUI, QuestionReview) are driven by other state and
// don't appear in the URL.
const NAV_VIEWS = ["home", "tests", "dashboard", "story", "results-demo"];

function TestsPage({ setView, onStartPaper, onContinuePaper, onBuyCredits, onViewReport }) {
  const { t, lang } = useT();
  const [credits, setCredits] = useState_M(0);
  const [takenById, setTakenById] = useState_M({});
  const [userId, setUserId] = useState_M(null);
  const [participantCounts, setParticipantCounts] = useState_M({});
  // Refresh-tick for in-progress detection — re-evaluates localStorage on render.
  const [, setTick] = useState_M(0);
  useEffect_M(() => {
    const onFocus = () => setTick(t => t + 1);
    window.addEventListener("focus", onFocus);
    return () => window.removeEventListener("focus", onFocus);
  }, []);
  const hasInProgress = (paperName) => {
    try { return !!localStorage.getItem(`epl_inprogress_${paperName}`); } catch { return false; }
  };

  useEffect_M(() => {
    if (!window._sb) return;
    (async () => {
      const { data: { session } } = await window._sb.auth.getSession();
      if (!session) return;
      setUserId(session.user.id);
      const { data: cRow } = await window._sb.from("profiles").select("credits").eq("id", session.user.id).maybeSingle();
      if (cRow && typeof cRow.credits === "number") setCredits(cRow.credits);
      const { data: sessions } = await window._sb.from("test_sessions")
        .select("id, paper_id, score, total, completed_at")
        .eq("user_id", session.user.id).gt("paper_id", 0)
        .order("completed_at", { ascending: false });
      if (sessions) {
        const byId = {};
        for (const s of sessions) if (!byId[s.paper_id]) byId[s.paper_id] = s;
        setTakenById(byId);
      }
      // Participant counts per paper (all users, incl. seeded cohort).
      // Uses RPC with SECURITY DEFINER to bypass RLS on test_sessions.
      const { data: counts } = await window._sb.rpc("get_paper_participant_counts");
      if (counts) {
        const c = {};
        for (const r of counts) c[r.paper_id] = Number(r.n);
        setParticipantCounts(c);
      }
    })();
  }, []);

  const FREE_PAPER_IDS = [1, 2, 3];
  const papers = [1, 2, 3, 4, 5, 6, 7, 8].map((id) => {
    const taken = takenById[id];
    const free = FREE_PAPER_IDS.includes(id);
    return {
      id,
      free,
      name: `Mock paper ${String(id).padStart(2, "0")}`,
      topics: 14, qs: 50, mins: (id === 1 || id === 2 || id === 3) ? 45 : 50,
      status: taken ? "taken" : (free || credits > 0) ? "ready" : "locked",
      sessionId: taken ? taken.id : null,
      score: taken ? taken.score : null,
      total: taken ? taken.total : 50,
    };
  });

  const handleStart = async (p) => {
    if (!userId) return;
    if (!p.free) {
      if (credits <= 0) return;
      const newCredits = credits - 1;
      const { error } = await window._sb.from("profiles").update({ credits: newCredits }).eq("id", userId);
      if (error) { console.error("deduct credit:", error.message); return; }
      setCredits(newCredits);
    }
    onStartPaper(p.name);
  };

  return (
    <main style={{ maxWidth: 1240, margin: "0 auto", padding: "40px 28px" }}>
      <div className="mono" style={{ fontSize: 11, color: "var(--ink-3)", letterSpacing: "0.08em", textTransform: "uppercase" }}>
        {t("nav_tests")}
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 24, alignItems: "end", marginTop: 6 }}>
        <div>
          <h1 style={{ fontSize: 36, letterSpacing: "-0.02em" }}>
            {lang === "ko" ? "실전 기반 50문제 모의고사" : "Real-exam-based 50 questions"}
          </h1>
          <p style={{ color: "var(--ink-2)", marginTop: 8, maxWidth: 640 }}>
            {lang === "ko"
              ? "한 번 시작한 시험은 온라인 또는 인쇄 중 한 가지 모드로만 진행되며, 중간에 바꿀 수 없습니다."
              : "Once started, a paper is locked to one mode — online or print & scan. No re-takes."}
          </p>
        </div>
        <div style={{ display: "flex", alignItems: "stretch", gap: 12 }}>
          <div style={{
            background: "var(--paper-2)", border: "1px solid var(--line)", borderRadius: 12,
            padding: "12px 20px", minWidth: 120, display: "flex", flexDirection: "column", justifyContent: "center",
          }}>
            <div className="mono" style={{ fontSize: 10, color: "var(--ink-3)", letterSpacing: "0.08em", textTransform: "uppercase" }}>
              {t("dash_credits")}
            </div>
            <div className="tabular" style={{ fontSize: 30, fontFamily: "var(--font-serif)", lineHeight: 1.1, marginTop: 4, letterSpacing: "-0.02em" }}>
              {credits}
            </div>
          </div>
          <button className="btn btn-dark" onClick={onBuyCredits} style={{ padding: "0 22px", fontSize: 14 }}>
            {lang === "ko" ? "크레딧 구매 →" : "Buy credits →"}
          </button>
        </div>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginTop: 28 }}>
        {papers.map((p) => {
          const locked = p.status === "locked";
          const taken = p.status === "taken";
          return (
            <div key={p.id} style={{
              background: "white", border: "1px solid var(--line)", borderRadius: 12,
              padding: 20, opacity: locked ? 0.6 : 1,
              display: "flex", flexDirection: "column", gap: 12,
            }}>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                <div className="mono" style={{ fontSize: 10, color: "var(--ink-3)", letterSpacing: "0.08em", textTransform: "uppercase" }}>
                  PAPER {String(p.id).padStart(2, "0")}
                </div>
                {taken && <span className="pill" style={{ color: "var(--emerald)", background: "var(--emerald-tint)", borderColor: "transparent" }}><span className="dot" />{lang === "ko" ? "완료" : "Done"}</span>}
                {p.status === "ready" && !taken && p.free && <span className="pill" style={{ color: "var(--amber)", background: "var(--amber-tint)", borderColor: "transparent" }}>{lang === "ko" ? "무료" : "FREE"}</span>}
                {p.status === "ready" && !p.free && <span className="pill" style={{ color: "var(--indigo)", background: "var(--indigo-tint)", borderColor: "transparent" }}><span className="dot" />{lang === "ko" ? "준비됨" : "Ready"}</span>}
                {locked && <span className="pill">🔒 {lang === "ko" ? "잠김" : "Locked"}</span>}
              </div>
              <div style={{ fontFamily: "var(--font-serif)", fontSize: 22 }}>{p.name}</div>
              <div style={{ display: "flex", gap: 16, fontSize: 12, color: "var(--ink-3)" }}>
                <div><span className="mono tabular" style={{ color: "var(--ink-1)" }}>{p.qs}</span> Q</div>
                <div><span className="mono tabular" style={{ color: "var(--ink-1)" }}>{p.mins}</span> min</div>
                <div><span className="mono tabular" style={{ color: "var(--ink-1)" }}>{participantCounts[p.id] || 0}</span> {lang === "ko" ? "명 완료" : "completed"}</div>
              </div>
              {taken && (
                <div style={{ fontSize: 13, color: "var(--ink-2)" }}>
                  {lang === "ko" ? "점수" : "Score"}: <span className="mono tabular" style={{ color: "var(--ink-1)" }}>{p.score}/{p.total}</span>
                </div>
              )}
              {(() => {
                const inProgress = !taken && !locked && hasInProgress(p.name);
                return (
                  <button
                    onClick={() => {
                      if (taken) { onViewReport && onViewReport(p.sessionId); return; }
                      if (locked) { onBuyCredits(); return; }
                      if (inProgress) { onContinuePaper && onContinuePaper(p.name); return; }
                      handleStart(p);
                    }}
                    className={taken ? "btn btn-ghost" : "btn btn-dark"}
                    style={{ marginTop: "auto", padding: "10px 14px", fontSize: 13 }}>
                    {locked
                      ? (lang === "ko" ? "🔒 크레딧 구매" : "🔒 Buy credits")
                      : taken ? (lang === "ko" ? "리포트 보기" : "View report")
                      : inProgress ? (lang === "ko" ? "이어하기" : "Continue")
                      : (lang === "ko" ? "시작" : "Start")}
                  </button>
                );
              })()}
            </div>
          );
        })}
      </div>
    </main>
  );
}

// ---------- Purchase modal ----------
const STRIPE_LINKS = {
  single: "https://buy.stripe.com/test_8x26oHaBN8cr8xp1nB1RC00",
  triple: "https://buy.stripe.com/test_bJebJ111dfET14X4zN1RC01",
  ten:    "https://buy.stripe.com/test_14AdR9aBN2S7aFx6HV1RC02",
};
function PurchaseModal({ onClose, userEmail }) {
  const { lang } = useT();
  const tiers = [
    { credits: 1,  price: "£3.99",  unit: "£3.99 / paper", link: STRIPE_LINKS.single },
    { credits: 3,  price: "£9.99",  unit: "£3.33 / paper", link: STRIPE_LINKS.triple, popular: true, savings: lang === "ko" ? "16% 할인" : "Save 16%" },
    { credits: 10, price: "£29.99", unit: "£3.00 / paper", link: STRIPE_LINKS.ten,    savings: lang === "ko" ? "25% 할인" : "Save 25%" },
  ];
  const buy = (link) => {
    if (link.includes("REPLACE_ME")) {
      alert(lang === "ko"
        ? "Stripe Payment Link이 아직 설정되지 않았습니다. Stripe Dashboard → Payment Links에서 생성 후 main.jsx의 STRIPE_LINKS를 교체해주세요."
        : "Stripe Payment Link not configured yet. Create one in Stripe Dashboard → Payment Links and replace STRIPE_LINKS in main.jsx.");
      return;
    }
    const url = userEmail ? `${link}?prefilled_email=${encodeURIComponent(userEmail)}` : link;
    window.location.href = url;
  };
  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, zIndex: 100,
      background: "color-mix(in oklab, var(--ink-1) 45%, transparent)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 24, overflow: "auto",
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: "var(--paper)", borderRadius: 14, padding: 32, maxWidth: 760, width: "100%",
        boxShadow: "var(--shadow-lg)", maxHeight: "92vh", overflowY: "auto",
      }}>
        <Logo size={20} />
        <h2 style={{ fontSize: 24, marginTop: 16, letterSpacing: "-0.01em" }}>
          {lang === "ko" ? "크레딧 구매" : "Buy credits"}
        </h2>
        <p style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 4 }}>
          {lang === "ko" ? "구독 없음 · 크레딧은 만료되지 않습니다." : "No subscription · credits never expire."}
        </p>

        <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 14, marginTop: 26 }}>
          {tiers.map((tier, i) => (
            <div key={i} style={{
              position: "relative",
              border: tier.popular ? "2px solid var(--indigo)" : "1px solid var(--line)",
              borderRadius: 12, padding: "22px 20px",
              background: tier.popular ? "var(--indigo-tint)" : "white",
              display: "flex", flexDirection: "column", gap: 8,
            }}>
              {tier.popular && (
                <div style={{
                  position: "absolute", top: -11, left: 18,
                  background: "var(--indigo)", color: "var(--paper)",
                  padding: "3px 10px", borderRadius: 999, fontSize: 10,
                  fontFamily: "var(--font-mono)", letterSpacing: "0.08em", textTransform: "uppercase",
                }}>
                  {lang === "ko" ? "추천" : "Most popular"}
                </div>
              )}
              <div style={{ fontFamily: "var(--font-serif)", fontSize: 26, letterSpacing: "-0.02em" }}>
                <span className="tabular">{tier.credits}</span>
                <span style={{ fontSize: 13, color: "var(--ink-3)", marginLeft: 6 }}>
                  {lang === "ko" ? "회" : (tier.credits > 1 ? "papers" : "paper")}
                </span>
              </div>
              <div style={{ fontFamily: "var(--font-serif)", fontSize: 42, letterSpacing: "-0.02em", lineHeight: 1 }} className="tabular">
                {tier.price}
              </div>
              <div className="mono" style={{ fontSize: 11, color: "var(--ink-3)", letterSpacing: "0.04em" }}>
                {tier.unit}
              </div>
              {tier.savings && (
                <div style={{ fontSize: 12, color: "var(--emerald)", fontWeight: 500, marginTop: 2 }}>
                  {tier.savings}
                </div>
              )}
              <button
                className={tier.popular ? "btn btn-dark" : "btn btn-ghost"}
                onClick={() => buy(tier.link)}
                style={{ marginTop: "auto", padding: "11px 14px", justifyContent: "center", fontSize: 14 }}>
                {lang === "ko" ? "결제하기" : "Buy now"}
              </button>
            </div>
          ))}
        </div>

        <div className="mono" style={{ fontSize: 10, color: "var(--ink-3)", marginTop: 20, letterSpacing: "0.04em", textAlign: "center" }}>
          {lang === "ko" ? "Stripe로 안전하게 결제 · VAT 포함" : "Secure checkout via Stripe · VAT included"}
        </div>
      </div>
    </div>
  );
}

// ---------- Story (company story page) ----------
function StoryPage() {
  const { t, lang } = useT();
  return (
    <main style={{ padding: "60px 28px 100px" }}>
      <div style={{ maxWidth: 980, margin: "0 auto" }}>
        <div className="mono" style={{ fontSize: 11, color: "var(--ink-3)", letterSpacing: "0.08em", textTransform: "uppercase" }}>
          {t("story_eyebrow")}
        </div>
        <h1 style={{ fontSize: 48, marginTop: 12, letterSpacing: "-0.02em", lineHeight: 1.1 }}>
          {t("story_title")}
        </h1>
        <div style={{
          marginTop: 32, borderRadius: 18, overflow: "hidden",
          border: "1px solid var(--line)",
          boxShadow: "0 16px 48px -20px color-mix(in oklab, var(--ink-1) 22%, transparent)",
          background: "white",
        }}>
          <img
            src="assets/story-hero.png"
            alt={lang === "ko" ? "데이터 사이언티스트 아빠와 11+ 준비하는 딸" : "A data-scientist dad helping his daughter prepare for the 11+"}
            style={{ display: "block", width: "100%", height: "auto" }}
            loading="lazy"
          />
        </div>
        <div style={{ marginTop: 36, fontSize: 18, color: "var(--ink-1)", lineHeight: 1.7, fontFamily: "var(--font-serif)", fontWeight: 400 }}>
          <p>{t("story_body_1")}</p>
          <p style={{ marginTop: 20 }}>{t("story_body_2")}</p>
        </div>
        <div style={{ marginTop: 40, paddingTop: 32, borderTop: "1px solid var(--line)" }}>
          <div className="mono" style={{ fontSize: 11, color: "var(--ink-3)", letterSpacing: "0.06em" }}>
            {t("story_sig")}
          </div>
        </div>
      </div>
    </main>
  );
}

// ---------- Register modal ----------
function RegisterModal({ onClose, onDone }) {
  const { lang } = useT();
  const [form, setForm] = useState_M({
    name: "", email: "", pw: "",
    target_school: "", current_school: "",
    child_year: "", dob: "", postcode: ""
  });
  const [err, setErr] = useState_M(null);
  const [loading, setLoading] = useState_M(false);
  const [pcSuggestions, setPcSuggestions] = useState_M([]);
  const [pcFocused, setPcFocused] = useState_M(false);
  const [postSignup, setPostSignup] = useState_M(false);
  const [countdown, setCountdown] = useState_M(5);

  const update = (field) => (e) => setForm((f) => ({ ...f, [field]: e.target.value }));

  // Postcode autocomplete via postcodes.io
  useEffect_M(() => {
    if (!form.postcode || form.postcode.length < 2) { setPcSuggestions([]); return; }
    const ctrl = new AbortController();
    const timer = setTimeout(() => {
      fetch(`https://api.postcodes.io/postcodes/${encodeURIComponent(form.postcode)}/autocomplete`, { signal: ctrl.signal })
        .then((r) => r.json())
        .then((d) => setPcSuggestions(d.result || []))
        .catch(() => {});
    }, 180);
    return () => { clearTimeout(timer); ctrl.abort(); };
  }, [form.postcode]);

  const submit = async () => {
    if (!form.name.trim()) { setErr(lang === "ko" ? "자녀 이름을 입력해 주세요." : "Enter child's name."); return; }
    if (!form.email.includes("@")) { setErr(lang === "ko" ? "유효한 이메일을 입력해 주세요." : "Enter a valid email."); return; }
    if (form.pw.length < 6) { setErr(lang === "ko" ? "비밀번호는 6자 이상이어야 합니다." : "Password must be 6+ characters."); return; }
    if (!form.current_school.trim()) { setErr(lang === "ko" ? "현재 학교를 입력해 주세요." : "Enter current school."); return; }
    if (!form.child_year) { setErr(lang === "ko" ? "학년을 선택해 주세요." : "Select child's year."); return; }
    if (!form.dob) { setErr(lang === "ko" ? "생년월일을 입력해 주세요." : "Enter date of birth."); return; }
    if (!form.postcode.trim()) { setErr(lang === "ko" ? "우편번호를 입력해 주세요." : "Enter postcode."); return; }
    setLoading(true); setErr(null);
    const profileData = {
      name: form.name.trim(),
      target_school: form.target_school.trim() || null,
      current_school: form.current_school.trim(),
      child_year: form.child_year,
      dob: form.dob,
      postcode: form.postcode.trim(),
    };
    const dupMsg = lang === "ko"
      ? "이미 가입된 이메일입니다. 로그인해 주세요."
      : "This email is already registered. Please log in instead.";

    // Pre-check: call RPC that queries auth.users directly (bypasses enumeration protection).
    // Falls back silently if the function doesn't exist yet.
    if (window._sb) {
      const { data: exists, error: rpcErr } = await window._sb.rpc("check_email_exists", { p_email: form.email });
      if (!rpcErr && exists === true) {
        setErr(dupMsg);
        setLoading(false);
        return;
      }
    }

    const { data, error } = await window._sb.auth.signUp({
      email: form.email,
      password: form.pw,
      options: {
        emailRedirectTo: window.location.origin + window.location.pathname,
        data: profileData,
      }
    });
    if (error) {
      const msg = error.message || "";
      const isDup = /already registered|already exists|duplicate|email.*taken/i.test(msg)
        || error.status === 422 || error.code === "user_already_exists";
      setErr(isDup ? dupMsg : msg);
      setLoading(false);
      return;
    }
    // Fallback: some Supabase configs return user with identities=[] instead of an error
    const identities = data?.user?.identities;
    if (data?.user && (!identities || (Array.isArray(identities) && identities.length === 0))) {
      setErr(dupMsg);
      setLoading(false);
      return;
    }
    if (data.user && data.session) {
      // Email confirmation disabled or already confirmed — create profile and sign in
      await window._sb.from("profiles").insert({ id: data.user.id, ...profileData });
      setLoading(false);
      onDone(data.user);
    } else {
      // Email confirmation required — show message then return home
      setLoading(false);
      setPostSignup(true);
    }
  };

  // Countdown after signup
  useEffect_M(() => {
    if (!postSignup) return;
    if (countdown <= 0) { onClose(); return; }
    const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
    return () => clearTimeout(timer);
  }, [postSignup, countdown]);

  const inputStyle = { padding: "11px 13px", borderRadius: 8, border: "1px solid var(--line-2)", fontSize: 14, fontFamily: "inherit", background: "white", width: "100%", boxSizing: "border-box" };
  const labelStyle = { fontSize: 11, color: "var(--ink-3)", marginBottom: 4, display: "block", fontFamily: "var(--font-mono)", letterSpacing: "0.04em", textTransform: "uppercase" };
  const yearOpts = ["Year 3", "Year 4", "Year 5", "Year 6", "Year 7"];

  if (postSignup) {
    return (
      <div onClick={onClose} style={{
        position: "fixed", inset: 0, zIndex: 100,
        background: "color-mix(in oklab, var(--ink-1) 45%, transparent)",
        display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
      }}>
        <div onClick={(e) => e.stopPropagation()} style={{
          background: "var(--paper)", borderRadius: 14, padding: 36, maxWidth: 440, width: "100%",
          boxShadow: "var(--shadow-lg)", textAlign: "center",
        }}>
          <div style={{
            width: 56, height: 56, borderRadius: "50%", background: "var(--indigo-tint)",
            display: "inline-flex", alignItems: "center", justifyContent: "center", marginBottom: 16,
          }}>
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none">
              <path d="M3 8l9 6 9-6M3 8v10a2 2 0 002 2h14a2 2 0 002-2V8M3 8l9-6 9 6" stroke="var(--indigo)" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </div>
          <h2 style={{ fontSize: 22, marginTop: 4, letterSpacing: "-0.01em" }}>
            {lang === "ko" ? "확인 메일을 보냈어요" : "Check your email"}
          </h2>
          <p style={{ fontSize: 14, color: "var(--ink-2)", marginTop: 12, lineHeight: 1.6 }}>
            {lang === "ko"
              ? <>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 13 }}>{form.email}</span>로 확인 링크를 보냈어요.<br />
                  링크를 클릭하면 바로 대시보드로 이동합니다.
                </>
              : <>
                  We've sent a confirmation link to<br />
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 13 }}>{form.email}</span>.<br />
                  Click it to go straight to your dashboard.
                </>}
          </p>
          <div className="mono" style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 22, letterSpacing: "0.04em" }}>
            {lang === "ko" ? `${countdown}초 후 홈으로 돌아갑니다…` : `Returning home in ${countdown}s…`}
          </div>
        </div>
      </div>
    );
  }

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, zIndex: 100,
      background: "color-mix(in oklab, var(--ink-1) 45%, transparent)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
      overflow: "auto",
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: "var(--paper)", borderRadius: 14, padding: 28, maxWidth: 480, width: "100%",
        boxShadow: "var(--shadow-lg)", maxHeight: "92vh", overflowY: "auto",
      }}>
        <Logo size={20} />
        <h2 style={{ fontSize: 22, marginTop: 16 }}>{lang === "ko" ? "가입하기 — 무료" : "Sign up — free"}</h2>
        <p style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 4 }}>
          {lang === "ko" ? "결제 정보 필요 없음." : "No payment info needed."}
        </p>

        <div style={{ display: "grid", gap: 12, marginTop: 18 }}>
          <div>
            <label style={labelStyle}>{lang === "ko" ? "자녀 이름" : "Child's name"}</label>
            <input value={form.name} onChange={update("name")} placeholder={lang === "ko" ? "예: 지원" : "e.g. Angelina"} style={inputStyle} />
          </div>
          <div>
            <label style={labelStyle}>{lang === "ko" ? "이메일" : "Email"}</label>
            <input value={form.email} onChange={update("email")} type="email" style={inputStyle} />
          </div>
          <div>
            <label style={labelStyle}>{lang === "ko" ? "비밀번호 (6자 이상)" : "Password (6+ chars)"}</label>
            <input value={form.pw} onChange={update("pw")} type="password" style={inputStyle} />
          </div>

          <div style={{ marginTop: 6, paddingTop: 14, borderTop: "1px solid var(--line)" }}>
            <div className="mono" style={{ fontSize: 10, color: "var(--ink-3)", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 10 }}>
              {lang === "ko" ? "학교 및 주소" : "School & address"}
            </div>
          </div>

          <div>
            <label style={labelStyle}>{lang === "ko" ? "타겟 학교" : "Target school"}</label>
            <input value={form.target_school} onChange={update("target_school")} placeholder={lang === "ko" ? "예: Tiffin School" : "e.g. Tiffin School"} style={inputStyle} />
          </div>
          <div>
            <label style={labelStyle}>{lang === "ko" ? "현재 학교" : "Current school"}</label>
            <input value={form.current_school} onChange={update("current_school")} placeholder={lang === "ko" ? "학교명 입력" : "School name"} style={inputStyle} />
          </div>

          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
            <div>
              <label style={labelStyle}>{lang === "ko" ? "학년" : "Year"}</label>
              <select value={form.child_year} onChange={update("child_year")} style={inputStyle}>
                <option value="">—</option>
                {yearOpts.map((y) => <option key={y} value={y}>{y}</option>)}
              </select>
            </div>
            <div>
              <label style={labelStyle}>{lang === "ko" ? "생년월일" : "Date of birth"}</label>
              <input value={form.dob} onChange={update("dob")} type="date" style={inputStyle} />
            </div>
          </div>

          <div style={{ position: "relative" }}>
            <label style={labelStyle}>{lang === "ko" ? "우편번호" : "Postcode"}</label>
            <input
              value={form.postcode}
              onChange={(e) => setForm((f) => ({ ...f, postcode: e.target.value.toUpperCase() }))}
              onFocus={() => setPcFocused(true)}
              onBlur={() => setTimeout(() => setPcFocused(false), 150)}
              placeholder="e.g. SW1A 1AA"
              style={inputStyle}
            />
            {pcFocused && pcSuggestions.length > 0 && (
              <div style={{
                position: "absolute", top: "100%", left: 0, right: 0, marginTop: 4,
                background: "white", border: "1px solid var(--line-2)", borderRadius: 8,
                maxHeight: 180, overflowY: "auto", zIndex: 10, boxShadow: "var(--shadow-lg)",
              }}>
                {pcSuggestions.map((p) => (
                  <div
                    key={p}
                    onMouseDown={() => { setForm((f) => ({ ...f, postcode: p })); setPcSuggestions([]); }}
                    style={{ padding: "9px 13px", fontSize: 13, cursor: "pointer", borderBottom: "1px solid var(--line)" }}
                    onMouseEnter={(e) => e.currentTarget.style.background = "var(--paper-2)"}
                    onMouseLeave={(e) => e.currentTarget.style.background = "white"}
                  >{p}</div>
                ))}
              </div>
            )}
          </div>
        </div>

        {err && <div style={{ marginTop: 12, fontSize: 12, color: "var(--rose)" }}>{err}</div>}
        <button className="btn btn-dark" onClick={submit} disabled={loading} style={{ width: "100%", marginTop: 18, padding: 12, justifyContent: "center", opacity: loading ? 0.6 : 1 }}>
          {loading ? (lang === "ko" ? "가입 중…" : "Signing up…") : (lang === "ko" ? "가입하고 답 보기" : "Register & unlock answers")}
        </button>
      </div>
    </div>
  );
}

// ---------- Login modal ----------
function LoginModal({ onClose, onDone, onSwitchToSignup }) {
  const { lang } = useT();
  const [email, setEmail] = useState_M("");
  const [pw, setPw] = useState_M("");
  const [err, setErr] = useState_M(null);
  const [notice, setNotice] = useState_M(null);
  const [loading, setLoading] = useState_M(false);
  const submit = async () => {
    if (!email.includes("@")) { setErr(lang === "ko" ? "유효한 이메일을 입력해 주세요." : "Enter a valid email."); return; }
    if (pw.length < 4) { setErr(lang === "ko" ? "비밀번호를 입력해 주세요." : "Enter your password."); return; }
    setLoading(true); setErr(null); setNotice(null);
    const { data, error } = await window._sb.auth.signInWithPassword({ email, password: pw });
    if (error) { setErr(error.message); setLoading(false); return; }
    setLoading(false);
    onDone(data.user);
  };
  const forgotPassword = async () => {
    if (!email.includes("@")) {
      setErr(lang === "ko"
        ? "재설정 링크를 받을 이메일을 먼저 입력해 주세요."
        : "Enter your email first to receive the reset link.");
      return;
    }
    setLoading(true); setErr(null); setNotice(null);
    const { error } = await window._sb.auth.resetPasswordForEmail(email, {
      redirectTo: window.location.origin + window.location.pathname,
    });
    setLoading(false);
    if (error) { setErr(error.message); return; }
    setNotice(lang === "ko"
      ? `${email}로 비밀번호 재설정 링크를 보냈어요. 메일함을 확인해 주세요.`
      : `Sent a reset link to ${email}. Check your inbox.`);
  };
  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, zIndex: 100,
      background: "color-mix(in oklab, var(--ink-1) 45%, transparent)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: "var(--paper)", borderRadius: 14, padding: 32, maxWidth: 420, width: "100%",
        boxShadow: "var(--shadow-lg)",
      }}>
        <Logo size={20} />
        <h2 style={{ fontSize: 24, marginTop: 20, letterSpacing: "-0.01em" }}>
          {lang === "ko" ? "다시 오신 것을 환영합니다" : "Welcome back"}
        </h2>
        <p style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 6 }}>
          {lang === "ko" ? "로그인하고 리포트 · 크레딧을 확인하세요." : "Log in to pick up reports and credits."}
        </p>
        <div style={{ display: "grid", gap: 12, marginTop: 22 }}>
          <input value={email} onChange={(e) => setEmail(e.target.value)}
            placeholder={lang === "ko" ? "이메일" : "Email"} type="email" autoFocus
            style={{
              padding: "12px 14px", borderRadius: 8, border: "1px solid var(--line-2)",
              fontSize: 14, fontFamily: "inherit", background: "white",
            }} />
          <input value={pw} onChange={(e) => setPw(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && submit()}
            placeholder={lang === "ko" ? "비밀번호" : "Password"} type="password"
            style={{
              padding: "12px 14px", borderRadius: 8, border: "1px solid var(--line-2)",
              fontSize: 14, fontFamily: "inherit", background: "white",
            }} />
        </div>
        {err && (
          <div style={{ marginTop: 10, fontSize: 12, color: "var(--rose)" }}>{err}</div>
        )}
        {notice && (
          <div style={{ marginTop: 10, fontSize: 12, color: "var(--emerald)" }}>{notice}</div>
        )}
        <div style={{ marginTop: 8, textAlign: "right" }}>
          <button onClick={forgotPassword} disabled={loading} style={{
            border: "none", background: "transparent", fontSize: 12, color: "var(--ink-3)",
            cursor: "pointer", fontFamily: "inherit", textDecoration: "underline",
            opacity: loading ? 0.5 : 1,
          }}>{lang === "ko" ? "비밀번호를 잊으셨나요?" : "Forgot password?"}</button>
        </div>
        <button className="btn btn-dark" onClick={submit} disabled={loading} style={{
          width: "100%", marginTop: 14, padding: 12, justifyContent: "center", opacity: loading ? 0.6 : 1
        }}>
          {loading ? (lang === "ko" ? "로그인 중…" : "Logging in…") : (lang === "ko" ? "로그인" : "Log in")}
        </button>
        <div style={{
          marginTop: 20, paddingTop: 18, borderTop: "1px solid var(--line)",
          fontSize: 13, color: "var(--ink-3)", textAlign: "center",
        }}>
          {lang === "ko" ? "계정이 없으신가요? " : "No account yet? "}
          <button onClick={onSwitchToSignup} style={{
            border: "none", background: "transparent", color: "var(--indigo)",
            cursor: "pointer", fontFamily: "inherit", fontSize: 13, fontWeight: 500, textDecoration: "underline",
          }}>
            {lang === "ko" ? "무료로 가입하기" : "Sign up free"}
          </button>
        </div>
      </div>
    </div>
  );
}

// ---------- Save-or-discard dialog shown when user hits browser back mid-test ----------
function TestExitDialog({ lang, onSave, onDiscard, onDismiss }) {
  return (
    <div onClick={onDismiss} style={{
      position: "fixed", inset: 0, zIndex: 200,
      background: "color-mix(in oklab, var(--ink-1) 55%, transparent)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: "var(--paper)", borderRadius: 14, padding: 28, maxWidth: 440, width: "100%",
        boxShadow: "var(--shadow-lg)",
      }}>
        <h2 style={{ fontSize: 22, letterSpacing: "-0.01em", marginTop: 0 }}>
          {lang === "ko" ? "시험을 중단하시겠어요?" : "Leave the test?"}
        </h2>
        <p style={{ fontSize: 14, color: "var(--ink-2)", marginTop: 10, lineHeight: 1.55 }}>
          {lang === "ko"
            ? "지금까지 푼 문제와 남은 시간을 저장하면 나중에 이어서 풀 수 있어요. 저장하지 않으면 다음에 처음부터 다시 시작해야 합니다."
            : "If you save, we'll keep your answers and the remaining time so you can resume later. If you discard, next time you'll start over from scratch."}
        </p>
        <div style={{ display: "flex", gap: 10, marginTop: 22 }}>
          <button onClick={onDiscard} className="btn btn-ghost" style={{ flex: 1, padding: 12, justifyContent: "center" }}>
            {lang === "ko" ? "저장 안 함" : "Discard"}
          </button>
          <button onClick={onSave} className="btn btn-dark" style={{ flex: 1, padding: 12, justifyContent: "center" }}>
            {lang === "ko" ? "저장하고 나가기" : "Save progress"}
          </button>
        </div>
        <button onClick={onDismiss} style={{
          display: "block", margin: "14px auto 0", border: "none", background: "transparent",
          color: "var(--ink-3)", fontSize: 12, fontFamily: "inherit", cursor: "pointer", textDecoration: "underline",
        }}>
          {lang === "ko" ? "취소 (시험 계속)" : "Cancel (stay in test)"}
        </button>
      </div>
    </div>
  );
}

// ---------- Password reset modal (opened on Supabase PASSWORD_RECOVERY) ----------
function PasswordResetModal({ onDone }) {
  const { lang } = useT();
  const [pw, setPw] = useState_M("");
  const [pw2, setPw2] = useState_M("");
  const [err, setErr] = useState_M(null);
  const [loading, setLoading] = useState_M(false);
  const submit = async () => {
    if (pw.length < 6) { setErr(lang === "ko" ? "비밀번호는 6자 이상이어야 합니다." : "Password must be 6+ characters."); return; }
    if (pw !== pw2) { setErr(lang === "ko" ? "비밀번호가 일치하지 않습니다." : "Passwords don't match."); return; }
    setLoading(true); setErr(null);
    const { error } = await window._sb.auth.updateUser({ password: pw });
    setLoading(false);
    if (error) { setErr(error.message); return; }
    // Sign out so the user explicitly logs in with the new password.
    try { await window._sb.auth.signOut(); } catch {}
    onDone();
  };
  return (
    <div style={{
      position: "fixed", inset: 0, zIndex: 100,
      background: "color-mix(in oklab, var(--ink-1) 45%, transparent)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
    }}>
      <div style={{
        background: "var(--paper)", borderRadius: 14, padding: 32, maxWidth: 420, width: "100%",
        boxShadow: "var(--shadow-lg)",
      }}>
        <Logo size={20} />
        <h2 style={{ fontSize: 22, marginTop: 20, letterSpacing: "-0.01em" }}>
          {lang === "ko" ? "새 비밀번호 설정" : "Set a new password"}
        </h2>
        <p style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 6 }}>
          {lang === "ko" ? "새 비밀번호를 두 번 입력해 주세요." : "Enter your new password twice."}
        </p>
        <div style={{ display: "grid", gap: 12, marginTop: 22 }}>
          <input value={pw} onChange={(e) => setPw(e.target.value)} type="password" autoFocus
            placeholder={lang === "ko" ? "새 비밀번호" : "New password"}
            style={{ padding: "12px 14px", borderRadius: 8, border: "1px solid var(--line-2)", fontSize: 14, fontFamily: "inherit", background: "white" }} />
          <input value={pw2} onChange={(e) => setPw2(e.target.value)} type="password"
            onKeyDown={(e) => e.key === "Enter" && submit()}
            placeholder={lang === "ko" ? "새 비밀번호 확인" : "Confirm new password"}
            style={{ padding: "12px 14px", borderRadius: 8, border: "1px solid var(--line-2)", fontSize: 14, fontFamily: "inherit", background: "white" }} />
        </div>
        {err && <div style={{ marginTop: 10, fontSize: 12, color: "var(--rose)" }}>{err}</div>}
        <button className="btn btn-dark" onClick={submit} disabled={loading} style={{
          width: "100%", marginTop: 16, padding: 12, justifyContent: "center", opacity: loading ? 0.6 : 1,
        }}>
          {loading ? (lang === "ko" ? "변경 중…" : "Updating…") : (lang === "ko" ? "비밀번호 변경" : "Update password")}
        </button>
      </div>
    </div>
  );
}

// ---------- Root app ----------
function App() {
  const [lang, setLang] = useState_M(() => {
    try { return localStorage.getItem("epl_lang") || "en"; } catch { return "en"; }
  });
  const [view, _setView] = useState_M(() => {
    try {
      const params = new URLSearchParams(window.location.search);
      const urlView = params.get("view");
      if (urlView && NAV_VIEWS.includes(urlView)) return urlView;
      return localStorage.getItem("epl_view") || "home";
    } catch { return "home"; }
  });
  // Wrap setView so nav changes push an entry into browser history — that way
  // the back/forward buttons step through visited views instead of escaping
  // the SPA. Call with { replace: true } to swap the current entry instead.
  const setView = React.useCallback((v, opts) => {
    const replace = opts && opts.replace;
    try {
      const url = new URL(window.location.href);
      if (NAV_VIEWS.includes(v)) url.searchParams.set("view", v);
      else url.searchParams.delete("view");
      // Strip any leftover hash (e.g. #access_token= from email confirm)
      url.hash = "";
      if (replace) history.replaceState({ view: v }, "", url);
      else if (url.toString() !== window.location.href) history.pushState({ view: v }, "", url);
    } catch {}
    _setView(v);
  }, []);
  const [mode, setMode] = useState_M(null); // online | paper
  const [pendingPaper, setPendingPaper] = useState_M(null);
  const [activePaper, setActivePaper] = useState_M(null);
  const [answers, setAnswers] = useState_M(null);
  const [showTestExitDialog, setShowTestExitDialog] = useState_M(false);
  // Refs for the popstate handler (which is installed once and would
  // otherwise see stale closure values).
  const inTestRef = React.useRef(false);
  const testStateRef = React.useRef(null);

  // Keep the inTest ref in sync + drop a sentinel history entry on entry so
  // the first browser-back press fires popstate and our interceptor runs.
  const inTestNow = !!(activePaper && answers === null && mode === "online");
  useEffect_M(() => {
    inTestRef.current = inTestNow;
    if (inTestNow) {
      try { history.pushState({ inTest: true }, ""); } catch {}
    }
  }, [inTestNow]);

  // Sync view when user clicks browser back/forward.
  useEffect_M(() => {
    const handler = () => {
      try {
        // If a test is live and not yet submitted, intercept back and
        // prompt save/cancel instead of discarding the work silently.
        if (inTestRef.current) {
          try {
            const url = new URL(window.location.href);
            history.pushState({ inTest: true }, "", url);
          } catch {}
          setShowTestExitDialog(true);
          return;
        }
        const params = new URLSearchParams(window.location.search);
        const urlView = params.get("view");
        const next = (urlView && NAV_VIEWS.includes(urlView)) ? urlView : "home";
        _setView(next);
        // Clear transient in-test state so the nav view actually renders.
        setActivePaper(null);
        setAnswers(null);
        setMode(null);
        setHistoricalReport(null);
        setReviewIndex(null);
        setRestoredSampleQs(null);
      } catch {}
    };
    window.addEventListener("popstate", handler);
    return () => window.removeEventListener("popstate", handler);
  }, []);
  const [savedSessionId, setSavedSessionId] = useState_M(null);
  const [historicalReport, setHistoricalReport] = useState_M(null);
  const [reviewIndex, setReviewIndex] = useState_M(null);

  // If URL has ?session=<id>, auto-load the corresponding report (deep-link support).
  // Also reacts to back/forward — popstate handler clears historicalReport when
  // the URL changes away from a report; this effect re-loads it if the URL is
  // restored to a report via forward/history.
  useEffect_M(() => {
    try {
      const params = new URLSearchParams(window.location.search);
      const sessionParam = params.get("session");
      if (sessionParam && !historicalReport && window._sb) {
        viewReport(sessionParam);
      }
    } catch {}
  }, [view]);
  const [showRegister, setShowRegister] = useState_M(false);
  const [showLogin, setShowLogin] = useState_M(false);
  const [showPurchase, setShowPurchase] = useState_M(false);
  const [showPasswordReset, setShowPasswordReset] = useState_M(false);
  const [fixedQs, setFixedQs] = useState_M(null);
  const [fixedQsLoading, setFixedQsLoading] = useState_M(false);
  const [restoredSampleQs, setRestoredSampleQs] = useState_M(null);
  const [authed, setAuthed] = useState_M(false);
  const [user, setUser] = useState_M(null);
  const [userName, setUserName] = useState_M("");
  const [showTweaks, setShowTweaks] = useState_M(false);

  // Supabase auth state + email confirmation redirect
  useEffect_M(() => {
    if (!window._sb) return;
    // If URL hash contains access_token (email-confirm or magic-link callback), route to dashboard on first SIGNED_IN
    let pendingEmailRedirect = window.location.hash.includes("access_token");
    window._sb.auth.getSession().then(({ data: { session } }) => {
      if (session) { setAuthed(true); setUser(session.user); }
    });
    const { data: { subscription } } = window._sb.auth.onAuthStateChange((event, session) => {
      // Password recovery link clicked — show the "set new password" modal
      // before doing anything else. Supabase has already exchanged the token
      // for a transient session; we let the user set a new password, then
      // sign them out so they log in fresh.
      if (event === "PASSWORD_RECOVERY") {
        setShowPasswordReset(true);
        try { history.replaceState(null, "", window.location.pathname); } catch {}
        return;
      }
      if (session) {
        setAuthed(true);
        setUser(session.user);
        if (pendingEmailRedirect && event === "SIGNED_IN") {
          // Restore sample result if user had one before signing up
          let restored = false;
          try {
            const raw = localStorage.getItem("epl_pending_sample");
            if (raw) {
              const p = JSON.parse(raw);
              if (p && Array.isArray(p.answers) && Array.isArray(p.questions) && p.answers.length > 0) {
                setRestoredSampleQs(p.questions);
                setActivePaper({ name: "SAMPLE · 10 Q", isSample: true });
                setAnswers(p.answers);
                setMode("online");
                restored = true;
              }
            }
          } catch {}
          // Replace the history entry that has #access_token — we don't
          // want back to return to the raw email-confirm URL.
          if (restored) {
            // ResultsView will render from restored state; strip the hash in place.
            try { history.replaceState({ view }, "", window.location.pathname + window.location.search); } catch {}
          } else {
            setView("dashboard", { replace: true });
          }
          pendingEmailRedirect = false;
        }
      } else {
        setAuthed(false);
        setUser(null);
        setUserName("");
      }
    });
    return () => subscription.unsubscribe();
  }, []);

  // Load child's name from profile whenever user changes; upsert from user_metadata if missing
  useEffect_M(() => {
    if (!user || !window._sb) { setUserName(""); return; }
    (async () => {
      const { data: profile, error: fetchErr } = await window._sb
        .from("profiles").select("name").eq("id", user.id).maybeSingle();
      if (fetchErr) console.error("profile fetch:", fetchErr.message);
      if (profile) { setUserName(profile.name || ""); return; }

      const meta = user.user_metadata || {};
      const { error: upErr } = await window._sb.from("profiles").upsert({
        id: user.id,
        name: meta.name || null,
        target_school: meta.target_school || null,
        current_school: meta.current_school || null,
        child_year: meta.child_year || null,
        dob: meta.dob || null,
        postcode: meta.postcode || null,
      }, { onConflict: "id" });
      if (upErr) console.error("profile upsert:", upErr.message);
      setUserName(meta.name || "");
    })();
  }, [user && user.id]);

  const doLogin = (u) => { setAuthed(true); setUser(u); setShowLogin(false); setView("dashboard"); };
  const doLogout = async () => { await window._sb.auth.signOut(); setAuthed(false); setUser(null); setUserName(""); setView("home"); };

  useEffect_M(() => { try { localStorage.setItem("epl_lang", lang); } catch {} }, [lang]);
  useEffect_M(() => { try { localStorage.setItem("epl_view", view); } catch {} }, [view]);

  // Edit-mode / Tweaks plumbing
  useEffect_M(() => {
    const onMsg = (e) => {
      const d = e.data || {};
      if (d.type === "__activate_edit_mode") setShowTweaks(true);
      if (d.type === "__deactivate_edit_mode") setShowTweaks(false);
    };
    window.addEventListener("message", onMsg);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMsg);
  }, []);

  const t = (k) => (STRINGS[lang] && STRINGS[lang][k]) || STRINGS.en[k] || k;

  // Paper 05 is wired to the Sutton engine (50 parameter-varied questions, auto-generated per session).
  // Load fixed questions from DB for non-sample papers.
  // For Paper 01 (variant system): first reads paper_config for the active variant,
  // then loads paper_questions filtered by (paper_id, variant_id).
  // For other papers: loads by paper_id only (no variant).
  useEffect_M(() => {
    if (!activePaper || activePaper.isSample) { setFixedQs(null); setFixedQsLoading(false); return; }
    const paperId = parseInt((activePaper.name.match(/\d+/) || ["0"])[0]);
    if (!paperId || !window._sb) { setFixedQs(null); setFixedQsLoading(false); return; }
    setFixedQsLoading(true);
    (async () => {
      const LTRS = ["A","B","C","D","E"];

      // For Papers 01/02 resolve the active variant first; other papers use variant 1.
      const isVariantPaper = (paperId === 1 || paperId === 2 || paperId === 3);
      let variantId = 1;
      if (isVariantPaper) {
        const { data: cfg } = await window._sb.from("paper_config")
          .select("active_variant_id").eq("paper_id", paperId).maybeSingle();
        variantId = (cfg && cfg.active_variant_id) ? cfg.active_variant_id : 1;
      }

      const query = window._sb.from("paper_questions")
        .select("*").eq("paper_id", paperId).order("question_index");
      if (isVariantPaper) query.eq("variant_id", variantId);
      const { data } = await query;

      const rowToQ = q => ({
        id: q.template_id || (q.question_index + 1),
        topicId: q.topic_id || 1,
        variantId: q.variant_id || 1,
        _html: true,
        _diagramHtml: q.diagram_html || "",
        _questionHtml: q.question_html || "",
        en: q.prompt_html || "",
        ko: q.prompt_html || "",
        optionsLabeled: (q.choices || []).map(c => c.html || c.value || ""),
        correct: q.correct_index ?? 0,
        solutionEn: `Answer: ${q.answer_key}`,
        solutionKo: `정답: ${q.answer_key}`,
      });

      const liveEngine = paperId === 1 ? window.Paper01Engine
                       : paperId === 2 ? window.Paper02Engine
                       : paperId === 3 ? window.Paper03Engine
                       : window.SuttonEngine;
      if (data && data.length > 0) {
        setFixedQs(data.map(rowToQ));
      } else if (liveEngine) {
        const all = isVariantPaper ? liveEngine.generateAll(variantId) : liveEngine.generateAll();
        const qRows = all.map((s, i) => {
          let correctIdx = typeof s.correctIdx === "number" ? s.correctIdx : -1;
          if (correctIdx < 0 && s.choices) {
            correctIdx = s.choices.findIndex(c => String(c.value) === String(s.answerKey));
            if (correctIdx < 0) correctIdx = s.choices.findIndex(c => String(c.html) === String(s.answerKey));
          }
          if (correctIdx < 0) correctIdx = 0;
          return {
            paper_id: paperId,
            variant_id: variantId,
            question_index: i,
            template_id: s.id,
            topic_id: s.topicId || 1,
            prompt_html: s.promptHtml || "",
            question_html: s.questionHtml || "",
            diagram_html: s.diagramHtml || "",
            choices: s.choices || [],
            answer_key: String(s.answerKey),
            correct_index: correctIdx,
            correct_letter: LTRS[correctIdx],
          };
        });
        // Save fire-and-forget
        window._sb.from("paper_questions").insert(qRows).then(() => {
          const keyRows = qRows.map(q => ({
            paper_id: paperId, variant_id: variantId,
            question_index: q.question_index, correct: q.correct_letter,
          }));
          window._sb.from("paper_keys")
            .delete().eq("paper_id", paperId).eq("variant_id", variantId)
            .then(() => window._sb.from("paper_keys").insert(keyRows));
        });
        setFixedQs(qRows.map(rowToQ));
      } else {
        setFixedQs(PLACEHOLDER_QUESTIONS);
      }
      setFixedQsLoading(false);
    })();
  }, [activePaper && activePaper.name, activePaper && activePaper.isSample]);

  const questions = React.useMemo(() => {
    if (!activePaper) return PLACEHOLDER_QUESTIONS;
    if (activePaper.isSample) {
      if (restoredSampleQs) return restoredSampleQs;
      return (window.buildSuttonQuestions ? window.buildSuttonQuestions(10) : PLACEHOLDER_QUESTIONS);
    }
    return fixedQs || PLACEHOLDER_QUESTIONS;
  }, [activePaper && activePaper.name, activePaper && activePaper.isSample, fixedQs, restoredSampleQs]);

  const startSample = () => { setRestoredSampleQs(null); setActivePaper({ name: "SAMPLE · 10 Q", isSample: true }); setAnswers(null); setSavedSessionId(null); setMode("online"); };
  const startResultsDemo = () => {
    const demo = PLACEHOLDER_QUESTIONS.map((q, i) => i < 8 ? q.correct : (q.correct + 1) % q.optionsLabeled.length);
    setActivePaper({ name: "Demo preview", isSample: false });
    setAnswers(demo);
    setView("results-demo");
  };
  // Auto-initialize demo state when navigating directly to ?view=results-demo
  useEffect_M(() => {
    if (view === "results-demo" && !activePaper && answers === null) startResultsDemo();
  }, [view]);
  const clearSessionUrl = React.useCallback(() => {
    try {
      const url = new URL(window.location.href);
      if (url.searchParams.has("session")) {
        url.searchParams.delete("session");
        history.replaceState({}, "", url);
      }
    } catch {}
  }, []);

  const viewReport = async (sessionId) => {
    if (!window._sb) return;
    // Push URL so report is bookmarkable / shareable and survives refresh.
    try {
      const url = new URL(window.location.href);
      url.searchParams.set("session", sessionId);
      if (url.toString() !== window.location.href) {
        history.pushState({ session: sessionId }, "", url);
      }
    } catch {}
    const { data: sess, error: sessErr } = await window._sb.from("test_sessions").select("*").eq("id", sessionId).single();
    if (sessErr || !sess) { console.error("load session:", sessErr && sessErr.message); return; }
    const { data: qa, error: qaErr } = await window._sb.from("question_answers").select("*").eq("session_id", sessionId).order("question_index");
    if (qaErr || !qa) { console.error("load answers:", qaErr && qaErr.message); return; }

    // Load paper_questions for the SAME variant the student took. Falls back to
    // any-variant fetch only if variant_id wasn't recorded on the session.
    // NB: paper_questions is mutable (paperN-print.html DELETE+INSERTs whenever
    // an admin regenerates), so we never trust its `correct_index` for scoring —
    // we use `question_answers.correct_idx` (snapshot at submit time) instead.
    let pqMap = {};
    if (sess.paper_id > 0) {
      const variantId = sess.variant_id || 1;
      let q = window._sb.from("paper_questions")
        .select("question_index, correct_index, prompt_html, question_html, diagram_html, choices, topic_id")
        .eq("paper_id", sess.paper_id);
      if (variantId) q = q.eq("variant_id", variantId);
      const { data: pqs } = await q.order("question_index");
      if (pqs) pqs.forEach(q => { pqMap[q.question_index] = q; });
    }

    const qs = qa.map((r, i) => {
      const idx = r.question_index ?? i;
      const pq = pqMap[idx];
      const choices = pq?.choices || [];
      // Always trust the question_answers snapshot for `correct` so the report
      // score keeps matching test_sessions.score even after paper_questions
      // is regenerated. Fall back to pq only if no snapshot was recorded.
      const correctIdx = (r.correct_idx != null) ? r.correct_idx : (pq?.correct_index ?? 0);
      return {
        id: r.question_id || (idx + 1),
        topicId: parseInt(pq?.topic_id || r.topic_id) || 1,
        correct: correctIdx,
        _html: true,
        _diagramHtml: pq?.diagram_html || "",
        _questionHtml: pq?.question_html || "",
        en: pq?.prompt_html || `Question ${idx + 1}`,
        ko: pq?.prompt_html || `Question ${idx + 1}`,
        optionsLabeled: choices.length ? choices.map(c => c.html || c.value) : ["—","—","—","—","—"],
        solutionEn: "", solutionKo: "",
      };
    });
    const ans = qa.map((r) => r.selected ?? null);
    const paperName = sess.paper_id === 0
      ? (lang === "ko" ? "샘플 · 10문제" : "Sample · 10 Q")
      : `Mock paper ${String(sess.paper_id).padStart(2, "0")}`;
    setHistoricalReport({ questions: qs, answers: ans, paperName, paperId: sess.paper_id });
  };
  const startPaper = (name) => {
    setPendingPaper(name); setSavedSessionId(null);
    testStartedAtRef.current = null;  // fresh timestamp on next test screen render
  };
  const pickMode = (m) => { setMode(m); if (pendingPaper) { setActivePaper({ name: pendingPaper, isSample: false }); setPendingPaper(null); } };
  // Resume an in-progress online test directly: skip the online/print picker and
  // jump straight to TestUI, which restores from localStorage via initialState.
  const continuePaper = (name) => {
    setSavedSessionId(null);
    setAnswers(null);
    setActivePaper({ name, isSample: false });
    setMode("online");
    testStartedAtRef.current = null;  // record resume time as started_at
  };
  const finishOnline = (ans) => {
    setAnswers(ans);
    // NB: don't clear testStartedAtRef here — the insert effect reads it on
    // the next render (triggered by setAnswers). It's reset on a fresh test
    // start in `startPaper` / `continuePaper`, and after a successful insert.
    // Clear any in-progress snapshot now that the test has been submitted.
    if (activePaper) {
      try { localStorage.removeItem(`epl_inprogress_${activePaper.name}`); } catch {}
    }
    // Stash sample result so it survives the email-confirm page reload.
    // The migration effect below reads this after the user is signed in.
    if (activePaper && activePaper.isSample) {
      try {
        const qFull = questions.map((q) => ({
          id: q.id || null,
          topicId: q.topicId || null,
          correct: q.correct,
          en: q.en || "",
          ko: q.ko || "",
          optionsLabeled: q.optionsLabeled || [],
          solutionEn: q.solutionEn || "",
          solutionKo: q.solutionKo || "",
          _html: q._html || false,
          _diagramHtml: q._diagramHtml || "",
          _questionHtml: q._questionHtml || "",
        }));
        localStorage.setItem("epl_pending_sample", JSON.stringify({
          answers: ans, questions: qFull, savedAt: new Date().toISOString(),
        }));
      } catch {}
    }
  };

  // Persist results to Supabase as soon as we have (user + source-of-result).
  // Source is either (a) localStorage: user finished the sample logged-out
  // then email-confirmed and the page reloaded, or (b) React state: user
  // finished a paper in this tab.
  //
  // Guard: the insert is async and deps can change while it's in flight
  // (e.g. getSession sets user on render 1, then onAuthStateChange sets
  // activePaper/answers on render 2 — without the ref, the effect fires
  // twice and inserts twice before savedSessionId propagates).
  const inflightInsertRef = React.useRef(false);
  const testStartedAtRef = React.useRef(null);
  useEffect_M(() => {
    if (!window._sb || !user || savedSessionId || inflightInsertRef.current) return;

    let source = null;
    // Priority 1: localStorage — consume + clear synchronously so a later
    // render doesn't re-read it.
    try {
      const raw = localStorage.getItem("epl_pending_sample");
      if (raw) {
        const p = JSON.parse(raw);
        if (p && Array.isArray(p.answers) && Array.isArray(p.questions) && p.answers.length === p.questions.length) {
          localStorage.removeItem("epl_pending_sample");
          source = {
            paperId: 0,
            variantId: 1,
            total: p.answers.length,
            score: p.answers.filter((a, i) => a === p.questions[i].correct).length,
            rows: p.answers.map((sel, i) => ({
              question_index: i,
              question_id: p.questions[i].id || (i + 1),
              topic_id: String(p.questions[i].topicId || ""),
              selected: sel,
              correct_idx: p.questions[i].correct,
              is_correct: sel === p.questions[i].correct,
            })),
          };
        }
      }
    } catch {}

    // Priority 2: React state — mock papers only. Samples go through
    // localStorage exclusively to avoid double-insert across tabs: if
    // another tab consumed localStorage first, an open original tab
    // with React state still populated would otherwise insert again
    // when its onAuthStateChange fires via shared-session sync.
    if (!source && activePaper && !activePaper.isSample && answers) {
      source = {
        paperId: parseInt((activePaper.name.match(/\d+/) || ["0"])[0]),
        variantId: (questions[0] && questions[0].variantId) || 1,
        total: questions.length,
        score: answers.filter((a, i) => a === questions[i].correct).length,
        rows: answers.map((sel, i) => ({
          question_index: i,
          question_id: questions[i].id || (i + 1),
          topic_id: String(questions[i].topicId || ""),
          selected: sel,
          correct_idx: questions[i].correct,
          is_correct: sel === questions[i].correct,
        })),
      };
    }
    if (!source) return;

    inflightInsertRef.current = true;
    let cancelled = false;
    (async () => {
      try {
        // Snapshot the student's percentile against the cohort at submission
        // time, so the dashboard history row keeps a meaningful rank even if
        // the cohort grows (or shrinks) later.
        let percentile = null;
        try {
          const { data: scores, error: rpcErr } = await window._sb
            .rpc("get_paper_score_distribution", { p_paper_id: source.paperId });
          if (!rpcErr && Array.isArray(scores) && scores.length > 0) {
            const lower = scores.filter((r) => r.score < source.score).length;
            percentile = Math.min(99, Math.max(1, Math.round((lower / scores.length) * 100)));
          }
        } catch {}

        const { data: sess, error: sessErr } = await window._sb.from("test_sessions")
          .insert({
            user_id: user.id, paper_id: source.paperId, variant_id: source.variantId || 1,
            score: source.score, total: source.total,
            started_at: testStartedAtRef.current,
            completed_at: new Date().toISOString(), percentile,
          })
          .select().single();
        if (cancelled) return;
        if (sessErr || !sess) { console.error("session insert:", sessErr && sessErr.message); return; }
        setSavedSessionId(sess.id);
        testStartedAtRef.current = null;  // reset now that the timestamp has been persisted
        const rows = source.rows.map((r) => ({ ...r, session_id: sess.id }));
        const { error: rowsErr } = await window._sb.from("question_answers").insert(rows);
        if (rowsErr) console.error("answers insert:", rowsErr.message);
      } finally {
        inflightInsertRef.current = false;
      }
    })();
    return () => { cancelled = true; };
  }, [user && user.id, activePaper && activePaper.name, answers, savedSessionId]);
  const finishScan = () => {
    // Auto-generate answers with plausible accuracy for demo
    const rand = questions.map((q) =>
      Math.random() < 0.72 ? q.correct : (q.correct + 1 + Math.floor(Math.random() * 3)) % q.optionsLabeled.length);
    setAnswers(rand);
  };

  // Render precedence: active flows first
  if (activePaper && answers === null && mode === "online" && fixedQsLoading) {
    return (
      <LangContext.Provider value={{ lang, setLang, t }}>
        <div style={{ minHeight: "100vh", background: "var(--paper)", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "var(--font-serif)", fontSize: 18, color: "var(--ink-3)" }}>
          Loading paper…
        </div>
      </LangContext.Provider>
    );
  }

  if (activePaper && answers === null && mode === "online") {
    // Capture test start time once per session (not on every re-render).
    if (!testStartedAtRef.current) testStartedAtRef.current = new Date().toISOString();
    // Resume previously saved in-progress state for this paper, if any.
    let initialState = null;
    try {
      const raw = localStorage.getItem(`epl_inprogress_${activePaper.name}`);
      if (raw) {
        const p = JSON.parse(raw);
        if (p && typeof p.idx === "number" && Array.isArray(p.answers)) initialState = p;
      }
    } catch {}
    return (
      <LangContext.Provider value={{ lang, setLang, t }}>
        <TestUI
          questions={questions}
          isSample={activePaper.isSample}
          durationMin={(() => {
            if (activePaper.isSample) return 10;
            const pid = parseInt((activePaper.name.match(/\d+/) || ["0"])[0]);
            return (pid === 1 || pid === 2 || pid === 3) ? 45 : 50;
          })()}
          paperName={activePaper.name}
          onFinish={finishOnline}
          onExit={() => { setActivePaper(null); setAnswers(null); setMode(null); }}
          initialState={initialState}
          stateRef={testStateRef}
        />
        {showTestExitDialog && (
          <TestExitDialog
            lang={lang}
            onSave={() => {
              try {
                localStorage.setItem(
                  `epl_inprogress_${activePaper.name}`,
                  JSON.stringify(testStateRef.current || {})
                );
              } catch {}
              inTestRef.current = false;
              setShowTestExitDialog(false);
              setActivePaper(null); setAnswers(null); setMode(null);
              try { history.back(); } catch {}
            }}
            onDiscard={() => {
              try { localStorage.removeItem(`epl_inprogress_${activePaper.name}`); } catch {}
              inTestRef.current = false;
              setShowTestExitDialog(false);
              setActivePaper(null); setAnswers(null); setMode(null);
              try { history.back(); } catch {}
            }}
            onDismiss={() => setShowTestExitDialog(false)}
          />
        )}
      </LangContext.Provider>
    );
  }

  if (activePaper && answers === null && mode === "paper") {
    const paperId = parseInt((activePaper.name.match(/\d+/) || ["0"])[0]);
    const printUrl = paperId === 1 ? "paper1-print.html"
                   : paperId === 2 ? "paper2-print.html"
                   : paperId === 3 ? "paper3-print.html"
                   : paperId > 0 ? `paper5-print.html?paper=${paperId}` : null;
    const paperDurationMin = (paperId === 1 || paperId === 2 || paperId === 3) ? 45 : 50;
    return (
      <LangContext.Provider value={{ lang, setLang, t }}>
        <ScanUpload
          paperName={activePaper.name}
          onDone={finishScan}
          onCancel={() => { setActivePaper(null); setMode(null); }}
          printUrl={printUrl}
          durationMin={paperDurationMin}
          previewQuestions={fixedQs ? fixedQs.slice(0, 5) : null}
        />
      </LangContext.Provider>
    );
  }

  if (activePaper && answers !== null && reviewIndex === null) {
    return (
      <LangContext.Provider value={{ lang, setLang, t }}>
        <ResultsView
          questions={questions} answers={answers}
          paperName={activePaper.name} isSample={activePaper.isSample}
          authed={authed} userName={userName}
          savedSessionId={savedSessionId}
          onReviewQ={(i) => setReviewIndex(i)}
          onRegister={() => setShowRegister(true)}
          onLogin={() => setShowLogin(true)}
          onGoDashboard={() => { setRestoredSampleQs(null); setActivePaper(null); setAnswers(null); setMode(null); setView("dashboard"); }}
          onGoTests={() => { setRestoredSampleQs(null); setActivePaper(null); setAnswers(null); setMode(null); setView("tests"); }}
          onExit={() => {
            const wasDemo = activePaper?.name === "Demo preview";
            setRestoredSampleQs(null); setActivePaper(null); setAnswers(null); setMode(null);
            if (wasDemo) setView("home");
          }}
        />
        {showRegister && <RegisterModal onClose={() => setShowRegister(false)} onDone={(u) => { setShowRegister(false); doLogin(u); }} />}
        {showLogin && <LoginModal onClose={() => setShowLogin(false)} onDone={(u) => doLogin(u)} onSwitchToSignup={() => { setShowLogin(false); setShowRegister(true); }} />}
      </LangContext.Provider>
    );
  }

  if (historicalReport) {
    if (reviewIndex !== null) {
      return (
        <LangContext.Provider value={{ lang, setLang, t }}>
          <QuestionReview
            questions={historicalReport.questions} answers={historicalReport.answers}
            qIndex={reviewIndex}
            paperId={historicalReport.paperId}
            onBack={() => setReviewIndex(null)}
            onChangeIndex={(i) => setReviewIndex(i)}
          />
        </LangContext.Provider>
      );
    }
    return (
      <LangContext.Provider value={{ lang, setLang, t }}>
        <ResultsView
          questions={historicalReport.questions} answers={historicalReport.answers}
          paperName={historicalReport.paperName} isSample={historicalReport.paperId === 0}
          authed={true} userName={userName}
          onReviewQ={(i) => setReviewIndex(i)}
          onGoDashboard={() => { clearSessionUrl(); setHistoricalReport(null); setReviewIndex(null); setView("dashboard"); }}
          onGoTests={() => { clearSessionUrl(); setHistoricalReport(null); setReviewIndex(null); setView("tests"); }}
          onExit={() => { clearSessionUrl(); setHistoricalReport(null); setReviewIndex(null); }}
        />
      </LangContext.Provider>
    );
  }

  if (reviewIndex !== null) {
    const reviewPaperId = activePaper && !activePaper.isSample
      ? parseInt((activePaper.name.match(/\d+/) || ["0"])[0])
      : 0;
    return (
      <LangContext.Provider value={{ lang, setLang, t }}>
        <QuestionReview
          questions={questions} answers={answers}
          qIndex={reviewIndex}
          paperId={reviewPaperId}
          onBack={() => setReviewIndex(null)}
          onChangeIndex={(i) => setReviewIndex(i)}
        />
      </LangContext.Provider>
    );
  }

  // Normal navigation views
  return (
    <LangContext.Provider value={{ lang, setLang, t }}>
      <div data-screen-label={view}>
        <Nav view={view} setView={setView} isLoggedIn={authed} userName={userName} onLogin={() => setShowLogin(true)} onSignup={() => setShowRegister(true)} onLogout={doLogout} />
        {view === "home" && <LandingPage
          onBuy={() => setShowPurchase(true)}
          setView={(v) => {
            if (v === "sample") startSample();
            else if (v === "results-demo") startResultsDemo();
            else setView(v);
          }} />}
        {view === "tests" && <TestsPage setView={setView} onStartPaper={startPaper} onContinuePaper={continuePaper} onBuyCredits={() => setShowPurchase(true)} onViewReport={viewReport} />}
        {view === "dashboard" && <UserDashboard setView={setView} onViewReport={viewReport} onBuyCredits={() => setShowPurchase(true)} refreshKey={savedSessionId} />}
        {view === "story" && <StoryPage />}
        <Footer />
      </div>

      {/* Mode chooser modal */}
      {pendingPaper && (
        <ModeChooser paperName={pendingPaper} onPick={pickMode} onCancel={() => setPendingPaper(null)} />
      )}

      {/* Login modal */}
      {showLogin && (
        <LoginModal
          onClose={() => setShowLogin(false)}
          onDone={(u) => doLogin(u)}
          onSwitchToSignup={() => { setShowLogin(false); setShowRegister(true); }}
        />
      )}

      {/* Purchase modal */}
      {showPurchase && (
        <PurchaseModal
          onClose={() => setShowPurchase(false)}
          userEmail={user && user.email}
        />
      )}

      {/* Password reset modal (opens on Supabase PASSWORD_RECOVERY event) */}
      {showPasswordReset && (
        <PasswordResetModal
          onDone={() => {
            setShowPasswordReset(false);
            setAuthed(false); setUser(null); setUserName("");
            setShowLogin(true);
          }}
        />
      )}

      {/* Register modal (top-level, reachable from Login modal "Sign up free") */}
      {showRegister && !activePaper && (
        <RegisterModal
          onClose={() => setShowRegister(false)}
          onDone={(u) => { setShowRegister(false); doLogin(u); }}
        />
      )}

      {/* Tweaks panel */}
      {showTweaks && (
        <div style={{
          position: "fixed", bottom: 20, right: 20, zIndex: 200,
          background: "white", border: "1px solid var(--line)", borderRadius: 12,
          padding: 18, width: 280, boxShadow: "var(--shadow-lg)",
        }}>
          <div className="mono" style={{ fontSize: 10, color: "var(--ink-3)", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 10 }}>
            Tweaks
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-3)", marginBottom: 6 }}>{lang === "ko" ? "언어" : "Language"}</div>
          <div style={{ display: "flex", gap: 6 }}>
            {[{ id: "en", label: "English" }, { id: "ko", label: "한국어" }].map(o => (
              <button key={o.id} onClick={() => setLang(o.id)} style={{
                flex: 1, padding: "8px 10px", borderRadius: 6,
                border: "1px solid " + (lang === o.id ? "var(--ink-1)" : "var(--line)"),
                background: lang === o.id ? "var(--ink-1)" : "white",
                color: lang === o.id ? "var(--paper)" : "var(--ink-1)",
                fontFamily: "inherit", fontSize: 12, cursor: "pointer",
              }}>{o.label}</button>
            ))}
          </div>
        </div>
      )}
    </LangContext.Provider>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
