// Spellstash — the /precondemo ungated lander.
// The browsing surface matches the app's demo family (synergy demo anatomy):
// type sections with collapsible headers, card rows with art + mana cost,
// tap any card for the big-image popup with prev/next arrows. The friction
// model per Dwayne's ruling: the verdict TOTAL and the three most expensive
// cards are free (the ad's payoff + the shock); every other per-card price
// is locked behind a free account. Loading theater masks the cold fetch.
const { useState, useEffect, useRef, useCallback } = React;

const money = (n) => (n == null ? "—" : "$" + Number(n).toFixed(2));
const FREE_PRICES = 3; // most-expensive rows whose prices show free

// The price wall is for signed-OUT visitors. A signed-in user already
// converted, so every price shows and the CTAs skip the login hop. Detected
// via the Supabase session token in localStorage (cheap, no SDK on this page;
// the wall is friction, not security).
const SIGNED_IN = (() => {
  try {
    // The app's Supabase client persists its session under this custom key
    // (spellstash-auth.jsx storageKey: "spellstash-auth"), NOT the library's
    // sb-<ref>-auth-token default. Checking only the default meant every
    // signed-in visitor still saw the signed-out wall (his 8-02 repro).
    const raw = localStorage.getItem("spellstash-auth");
    if (raw) {
      const sv = JSON.parse(raw);
      if (sv && (sv.access_token || (sv.currentSession && sv.currentSession.access_token))) return true;
    }
    // Fallback: the library default key, should the custom key ever go away.
    for (let i = 0; i < localStorage.length; i++) {
      const k = localStorage.key(i);
      if (k && /^sb-.*-auth-token$/.test(k)) return true;
    }
  } catch (e) { /* signed-out is the safe default */ }
  return false;
})();

const BASE_PROPS = {};
(() => {
  try {
    const q = new URLSearchParams(window.location.search);
    const uc = (q.get("utm_content") || "").trim();
    if (uc) BASE_PROPS.utm_content = uc;
    if (q.get("utm_source")) BASE_PROPS.utm_source = q.get("utm_source");
    const ua = navigator.userAgent || "";
    BASE_PROPS.webview = /wv|FBAN|FBAV|Instagram|Reddit/i.test(ua) ? "yes" : "no";
  } catch (e) { /* attribution never breaks the page */ }
})();
function track(name, props) {
  const merged = Object.assign({}, BASE_PROPS, props || {});
  try { window.plausible && window.plausible(name, { props: merged }); } catch (e) { /* no-op */ }
}

function signupHref(slug) {
  if (SIGNED_IN) return "/collection?precon=" + slug;
  return "/login?signup=1&next=" + encodeURIComponent("/collection?precon=" + slug);
}
function goSignup(slug, cta) {
  try { localStorage.setItem("spellstash.precon.claim", slug); } catch (e) { /* best effort */ }
  const url = signupHref(slug);
  let done = false;
  const go = () => { if (done) return; done = true; window.location.href = url; };
  try {
    if (window.plausible) {
      window.plausible("PreconDemoSignupClick", { props: Object.assign({}, BASE_PROPS, { precon: slug, cta }), callback: go });
      setTimeout(go, 600);
      return;
    }
  } catch (e) { /* fall through */ }
  go();
}

// ── Mana cost chips: "{2}{U}{B}" → 2 U B pips ────────────────────────────────
const MANA_COLORS = { W: "#f8f0d8", U: "#bcd9ef", B: "#c9c2bd", R: "#efb9a2", G: "#bcd8c3" };
function ManaCost({ cost }) {
  if (!cost) return null;
  const syms = String(cost).split(" // ")[0].match(/\{([^}]+)\}/g) || [];
  if (!syms.length) return null;
  return (
    <span style={{ display: "inline-flex", gap: 2, verticalAlign: "middle" }}>
      {syms.map((s, i) => {
        const v = s.slice(1, -1);
        const bg = MANA_COLORS[v] || "#d8d2c2";
        return (
          <span key={i} style={{
            width: 16, height: 16, borderRadius: 999, background: bg, color: "#241f16",
            font: "700 10px var(--mono)", display: "inline-flex", alignItems: "center",
            justifyContent: "center", border: "1px solid rgba(0,0,0,.18)",
          }}>{v.length > 2 ? "•" : v}</span>
        );
      })}
    </span>
  );
}

// ── Type sections (same bucketing as the synergy demo / app stats) ───────────
const TYPE_SECTIONS = [
  { key: "creature", label: "Creatures" },
  { key: "instant", label: "Instants" },
  { key: "sorcery", label: "Sorceries" },
  { key: "artifact", label: "Artifacts" },
  { key: "enchantment", label: "Enchantments" },
  { key: "planeswalker", label: "Planeswalkers" },
  { key: "battle", label: "Battles" },
  { key: "land", label: "Lands" },
  { key: "other", label: "Other" },
];
function typeBucket(typeLine) {
  const t = String(typeLine || "").split(" // ")[0].toLowerCase();
  if (!t) return "other";
  if (t.includes("creature")) return "creature";
  if (t.includes("land")) return "land";
  if (t.includes("instant")) return "instant";
  if (t.includes("sorcery")) return "sorcery";
  if (t.includes("artifact")) return "artifact";
  if (t.includes("enchantment")) return "enchantment";
  if (t.includes("planeswalker")) return "planeswalker";
  if (t.includes("battle")) return "battle";
  return "other";
}
function groupByType(cards) {
  const buckets = {};
  cards.forEach((card, flatIdx) => {
    const k = typeBucket(card.type_line);
    (buckets[k] = buckets[k] || []).push({ card, flatIdx });
  });
  return buckets;
}

// Staged loading theater over one real fetch; the wait reads as work being
// done, because it is.
const STAGES = [
  "Pulling the official decklist…",
  "Matching every printing…",
  "Pricing each card at Card Kingdom…",
  "Totaling the bill…",
];
function LoadingTheater({ label }) {
  const [stage, setStage] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setStage((s) => Math.min(s + 1, STAGES.length - 1)), 2600);
    return () => clearInterval(t);
  }, []);
  return (
    <div className="pd-card" style={{ marginTop: 16, textAlign: "center", padding: "28px 16px" }}>
      <div className="pd-mono pd-dim" style={{ fontSize: 12, marginBottom: 10 }}>{label}</div>
      <div style={{ fontSize: 18, fontWeight: 900 }}>{STAGES[stage]}</div>
      <div className="pd-bar-track"><div className="pd-bar-fill" style={{ width: ((stage + 1) / STAGES.length) * 90 + "%" }} /></div>
      <div className="pd-dim" style={{ fontSize: 12, marginTop: 8 }}>Real prices take a few seconds. Worth it.</div>
    </div>
  );
}

// ── One card row, synergy-demo anatomy: art, name, mana + type chips, rail ──
function CardRow({ card, priceFree, slug, onOpen }) {
  return (
    <div className="crow">
      {card.image_small || card.image_normal
        ? <img className="th" src={card.image_small || card.image_normal} alt="" loading="lazy" style={{ cursor: "pointer" }} onClick={onOpen} />
        : <div className="ph" style={{ cursor: "pointer" }} onClick={onOpen}>⬡</div>}
      <div className="mid">
        <div className="nm" style={{ cursor: "pointer" }} onClick={onOpen} role="button" tabIndex={0}
          onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpen(); } }}>
          {card.qty > 1 ? card.qty + "× " : ""}{card.name}
        </div>
        <div className="schips">
          <ManaCost cost={card.mana_cost} />
          {card.type_line ? <span className="schip schip--type">{String(card.type_line).split(" — ")[0]}</span> : null}
        </div>
      </div>
      <div className="rail">
        {priceFree
          ? (card.buy_url
            ? <a className="buy" href={card.buy_url} target="_blank" rel="noopener noreferrer"
                onClick={() => track("PreconDemoBuyClick", { card: card.name, price: card.ck_price })}>
                {typeof card.ck_price === "number" ? money(card.ck_price) + " at Card Kingdom" : "Buy at Card Kingdom"}
              </a>
            : <span className="noprice">{typeof card.ck_price === "number" ? money(card.ck_price) : "no CK price"}</span>)
          : <button type="button" className="pill pill--lock" onClick={() => goSignup(slug, "price_lock")}
              title="A free account shows every price and crosses off the cards you already own.">
              $ •.•• 🔒
            </button>}
      </div>
    </div>
  );
}

// ── Card popup: big image, arrows walk the flat list, price or lock ──────────
function DemoCardModal({ cards, startIndex, slug, freeSet, onClose }) {
  const list = Array.isArray(cards) && cards.length ? cards : [];
  const [idx, setIdx] = useState(() => Math.min(Math.max(startIndex || 0, 0), Math.max(list.length - 1, 0)));
  const card = list[idx] || {};
  const goPrev = useCallback(() => setIdx((i) => Math.max(0, i - 1)), []);
  const goNext = useCallback(() => setIdx((i) => Math.min(list.length - 1, i + 1)), [list.length]);

  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      else if (e.key === "ArrowLeft") goPrev();
      else if (e.key === "ArrowRight") goNext();
    };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
  }, [onClose, goPrev, goNext]);

  const img = card.image_normal || card.image_small;
  const priceFree = freeSet.has(card.name);
  // DFC flip: probe the /back/ URL; single-faced cards 404 it silently.
  const [showBack, setShowBack] = useState(false);
  const [hasBack, setHasBack] = useState(false);
  const backSrc = img && img.indexOf("/front/") >= 0 ? img.replace("/front/", "/back/") : null;
  useEffect(() => {
    setShowBack(false); setHasBack(false);
    if (!backSrc) return;
    let live = true;
    const probe = new Image();
    probe.onload = () => { if (live) setHasBack(true); };
    probe.src = backSrc;
    return () => { live = false; };
  }, [backSrc]);
  return (
    <div className="cp-backdrop" onClick={onClose}>
      <div className="cp-wrap" onClick={(e) => e.stopPropagation()}>
        <button type="button" className="cp-close" onClick={onClose} aria-label="Close">✕</button>
        <div className="cp-box">
          <div className="cp-imgwrap">
            {img ? <img className="cp-img" src={showBack && hasBack && backSrc ? backSrc : img} alt={card.name} /> : <div className="ph" style={{ minHeight: 220 }}>⬡</div>}
            {hasBack && (
              <button type="button" className="pd-buy" style={{ position: "absolute", bottom: 8, right: 8, background: "rgba(255,253,246,.9)", borderRadius: 999, padding: "6px 12px" }}
                onClick={() => setShowBack((s) => !s)}>
                {showBack ? "Front ⟲" : "Flip ⟳"}
              </button>
            )}
            {list.length > 1 && (
              <React.Fragment>
                <button type="button" className="cp-nav cp-prev" onClick={goPrev} disabled={idx === 0} aria-label="Previous card">‹</button>
                <button type="button" className="cp-nav cp-next" onClick={goNext} disabled={idx === list.length - 1} aria-label="Next card">›</button>
                <span className="cp-count">{idx + 1} / {list.length}</span>
              </React.Fragment>
            )}
          </div>
          <div className="cp-sheet">
            <div className="cp-name">{card.name}</div>
            {card.type_line && <div className="cp-type">{card.type_line}</div>}
            <div className="schips" style={{ marginTop: 8 }}>
              <ManaCost cost={card.mana_cost} />
            </div>
            <div className="cp-actions">
              {priceFree && card.buy_url ? (
                <a className="buy" href={card.buy_url} target="_blank" rel="noopener noreferrer"
                  onClick={() => track("PreconDemoBuyClick", { card: card.name, price: card.ck_price })}>
                  {typeof card.ck_price === "number" ? money(card.ck_price) + " at Card Kingdom" : "Buy at Card Kingdom"}
                </a>
              ) : priceFree ? (
                <span className="noprice">{typeof card.ck_price === "number" ? money(card.ck_price) : "no CK price"}</span>
              ) : (
                <button type="button" className="pill pill--lock" onClick={() => goSignup(slug, "modal_lock")}>
                  $ •.•• 🔒 free account shows the price
                </button>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function App() {
  const [precons, setPrecons] = useState([]);
  const [filter, setFilter] = useState("");
  const [picked, setPicked] = useState(null);
  const [loading, setLoading] = useState(false);
  const [err, setErr] = useState(null);
  const [r, setR] = useState(null);
  const [collapsed, setCollapsed] = useState(new Set());
  const [modalIdx, setModalIdx] = useState(null);
  const wallTracked = useRef(false);

  useEffect(() => {
    track("PreconDemoView");
    (async () => {
      try {
        const res = await fetch("/api/precon-demo/list");
        const data = await res.json();
        setPrecons(data.precons || []);
      } catch (e) { setErr("Couldn't load the precon list. Refresh to retry."); }
    })();
  }, []);

  const run = async (p) => {
    if (!p) return;
    setPicked(p); setErr(null); setLoading(true); setR(null); setModalIdx(null);
    setCollapsed(new Set());
    wallTracked.current = false;
    try {
      const res = await fetch("/api/precon-demo/check?slug=" + encodeURIComponent(p.slug));
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || "HTTP " + res.status);
      if (data.status === "unavailable") throw new Error("Price data is catching its breath. Try again in a minute.");
      setR(data);
      track("PreconDemoCheck", { precon: p.slug, total: Math.round(data.verdict.singles_total) });
      if (!wallTracked.current) { wallTracked.current = true; track("PreconDemoWallShown", { precon: p.slug }); }
      try {
        const q = new URLSearchParams(window.location.search);
        q.set("precon", p.slug);
        history.replaceState(null, "", window.location.pathname + "?" + q.toString());
      } catch (e) { /* cosmetic */ }
    } catch (e) { setErr(e.message); }
    setLoading(false);
  };

  useEffect(() => {
    try {
      const pre = new URLSearchParams(window.location.search).get("precon");
      if (pre && precons.length > 0 && !r && !loading && !picked) {
        const p = precons.find((x) => x.slug === pre) || { slug: pre, label: pre };
        run(p);
      }
    } catch (e) { /* ignore */ }
  }, [precons]); // eslint-disable-line

  const q = filter.trim().toLowerCase();
  const filtered = q
    ? precons.filter((p) => (p.label + " " + (p.commander || "")).toLowerCase().includes(q))
    : precons;

  // The three most expensive cards show real prices; everything else locks.
  const freeSet = r
    ? (SIGNED_IN
      ? new Set(r.cards.map((c) => c.name))
      : new Set([...r.cards].sort((a, b) => (b.ck_price ?? -1) - (a.ck_price ?? -1)).slice(0, FREE_PRICES).map((c) => c.name)))
    : new Set();
  const toggleType = (key) => setCollapsed((prev) => {
    const next = new Set(prev);
    if (next.has(key)) next.delete(key); else next.add(key);
    return next;
  });

  return (
    <div className="pd-wrap">
      <div className="pd-mono pd-dim" style={{ fontSize: 12, letterSpacing: ".08em" }}>SPELLSTASH · PRECON CHECK</div>
      <h1 className="pd-h1">Never buy a precon without pricing the singles first.</h1>
      <p style={{ fontSize: 16, lineHeight: 1.5 }}>
        Sealed prices drift. The cards inside do not lie. Pick any Commander precon
        and see what the whole deck costs as singles at Card Kingdom.
        <b> Free, right now, no account.</b>
      </p>
      <p className="pd-straight">
        The straight part: prices are Card Kingdom's, one store, refreshed daily.
        Basic lands are not counted, they are pennies. And this page cannot see
        your collection, so every number assumes you own none of it. That last
        part matters in a minute.
      </p>

      <div className="pd-card" style={{ marginTop: 14 }}>
        <div className="pd-mono pd-dim" style={{ fontSize: 12, marginBottom: 8 }}>WHICH PRECON?</div>
        <input
          className="pd-input"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          placeholder={precons.length ? "Search " + precons.length + " precons…" : "Loading precons…"}
        />
        {q || !picked ? (
          <div className="pd-picker">
            {filtered.slice(0, 30).map((p) => (
              <button key={p.slug} type="button"
                className={"pd-precon-row" + (picked && picked.slug === p.slug ? " pd-precon-row--picked" : "")}
                onClick={() => { setFilter(""); run(p); }}>
                <span style={{ fontWeight: 700 }}>{p.label}</span>
                {p.commander ? <span className="pd-dim" style={{ fontSize: 12, marginLeft: 8 }}>{p.commander}</span> : null}
              </button>
            ))}
            {filtered.length === 0 && q ? <div className="pd-dim" style={{ padding: 10 }}>No precons match "{filter}".</div> : null}
          </div>
        ) : null}
        {err ? <div style={{ color: "#a33", marginTop: 10 }}>{err}</div> : null}
      </div>

      {loading ? <LoadingTheater label={(picked && picked.label) || "…"} /> : null}

      {r ? (
        <React.Fragment>
          <div className="pd-card pd-verdict" style={{ marginTop: 16 }}>
            <div className="pd-mono pd-dim" style={{ fontSize: 12 }}>THE VERDICT</div>
            <div style={{ fontSize: 24, fontWeight: 900, margin: "6px 0" }}>
              {r.name} is {money(r.verdict.singles_total)} in singles.
            </div>
            <div className="pd-dim" style={{ fontSize: 13 }}>
              {r.verdict.priced} of {r.verdict.of} cards priced at Card Kingdom, commander included.
              One order, one shipping charge, not twelve.
            </div>
          </div>

          <div className="pd-tease" style={{ marginTop: 12 }}>
            <div style={{ fontSize: 17, fontWeight: 900, marginBottom: 6 }}>
              That is the price if you own none of it.
            </div>
            <div style={{ fontSize: 14.5, lineHeight: 1.55, marginBottom: 12 }}>
              {SIGNED_IN
                ? "You probably own some of it. Open it in your Spellstash and it crosses off every card already sitting in your boxes before you buy it twice. The real number is always lower than this one."
                : "You probably own some of it. Sign up free, load what you own, and Spellstash crosses off every card already sitting in your boxes before you buy it twice. The real number is always lower than this one."}
            </div>
            <button className="pd-btn" onClick={() => goSignup(r.slug, "verdict")}>See my real number →</button>
          </div>

          {/* Commander + the three priciest cards, then the deck by type. */}
          <div style={{ marginTop: 16 }}>
            <div className="pd-mono pd-dim" style={{ fontSize: 12, marginBottom: 4 }}>THE EXPENSIVE TRUTH</div>
            {r.commanders.map((c) => (
              <div className="crow" key={c.name}>
                {c.image_normal ? <img className="th" src={c.image_normal} alt="" loading="lazy" /> : <div className="ph">⬡</div>}
                <div className="mid">
                  <div className="nm">{c.name}</div>
                  <div className="schips"><span className="schip schip--type">Commander</span></div>
                </div>
                <div className="rail"><span className="noprice">{typeof c.ck_price === "number" ? money(c.ck_price) : "no CK price"}</span></div>
              </div>
            ))}
            {[...r.cards].sort((a, b) => (b.ck_price ?? -1) - (a.ck_price ?? -1)).slice(0, FREE_PRICES).map((c) => (
              <CardRow key={c.name} card={c} priceFree={true} slug={r.slug}
                onOpen={() => setModalIdx(r.cards.findIndex((x) => x.name === c.name))} />
            ))}

            <div className="pd-mono pd-dim" style={{ fontSize: 12, margin: "14px 0 4px" }}>
              {SIGNED_IN ? "THE WHOLE DECK, BY TYPE" : "THE WHOLE DECK, BY TYPE · prices unlock with a free account"}
            </div>
            {(() => {
              const buckets = groupByType(r.cards);
              return TYPE_SECTIONS.map((sec) => {
                const entries = buckets[sec.key];
                if (!entries || entries.length === 0) return null;
                const open = !collapsed.has(sec.key);
                return (
                  <div key={sec.key}>
                    <button type="button" className="sec-h" onClick={() => toggleType(sec.key)} aria-expanded={open}>
                      <span aria-hidden="true" style={{ color: "var(--brand)" }}>{open ? "▾" : "▸"}</span>
                      {sec.label}
                      <span style={{ color: "var(--ink-3)", fontWeight: 600 }}>{entries.length}</span>
                    </button>
                    {open && entries.map((en) => (
                      <CardRow key={(en.card.oracle_id || en.card.name) + "-" + en.flatIdx}
                        card={en.card} priceFree={freeSet.has(en.card.name)} slug={r.slug}
                        onOpen={() => setModalIdx(en.flatIdx)} />
                    ))}
                  </div>
                );
              });
            })()}
          </div>
        </React.Fragment>
      ) : null}

      <footer>
        Spellstash is unaffiliated with Wizards of the Coast. Magic: The Gathering
        and all card data are property of Wizards of the Coast LLC. Prices from
        Card Kingdom, refreshed daily. Spellstash may earn a commission on
        Card Kingdom purchases.
        <div style={{ marginTop: 6 }}>
          <a href="/" className="pd-dim">Spellstash</a> · <a href="/precons" className="pd-dim">All precon prices</a> · <a href="/privacy" className="pd-dim">Privacy</a> · <a href="/terms" className="pd-dim">Terms</a>
        </div>
      </footer>

      {r ? (
        <div className="pd-sticky">
          <span><b>Spellstash crosses off what you already own.</b></span>
          <a href={signupHref(r.slug)} onClick={(e) => { e.preventDefault(); goSignup(r.slug, "sticky"); }}>See my real number</a>
        </div>
      ) : null}

      {r && modalIdx != null ? (
        <DemoCardModal cards={r.cards} startIndex={modalIdx} slug={r.slug} freeSet={freeSet}
          onClose={() => setModalIdx(null)} />
      ) : null}
    </div>
  );
}

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