// Spellstash Combo Finder. Type a card, see the combos it belongs to. Paste
// a deck, see the combos in it plus the near misses. Signed in, each combo
// checks against the whole collection and shows where every owned piece
// lives, with buy paths for the rest. Anon search works instantly with zero
// account; the ownership overlay is the sign-in unlock, shown through a
// static demo row, never just described. All Commander Spellbook data comes
// through our server (/api/combos/*), never from the browser.
// Contract: graph-way/runs/2026-08-13-combo-finder/brief-final.md.
//
// LAW: selection never fires actions. Tapping a card only opens the popup;
// every mutation (want, own this, buy) sits behind an explicit button.
const { useState, useEffect, useRef, useCallback } = React;
const { CardPopup, WishlistModal, OwnThisModal } = window.ssCardPopup;

const T = {
  accent: "var(--cm-accent)",
  accentFg: "var(--cm-accent-fg)",
  accentSoft: "var(--cm-accent-soft)",
  bg: "var(--bg)", bg2: "var(--bg-2)", bg3: "var(--bg-3)",
  line: "var(--line)",
  ink: "var(--ink)", ink2: "var(--ink-2)", ink3: "var(--ink-3)", ink4: "var(--ink-4)",
  good: "var(--good)",
  serif: "var(--cm-serif)", sans: "var(--cm-sans)", mono: "var(--cm-mono)",
};

// Permanent event names (surface prefix convention). Never rename.
function track(name, props) {
  try { window.plausible && window.plausible(name, { props: props || {} }); } catch (e) { /* best effort */ }
}

// Curated result picker. Keys are the server's API contract.
const RESULT_OPTIONS = [
  { value: "", label: "Any result" },
  { value: "infinite_mana", label: "Infinite mana" },
  { value: "infinite_draw", label: "Infinite draw" },
  { value: "infinite_tokens", label: "Infinite tokens" },
  { value: "infinite_damage", label: "Infinite damage" },
  { value: "infinite_mill", label: "Infinite mill" },
  { value: "infinite_life", label: "Infinite life" },
  { value: "infinite_turns", label: "Infinite turns" },
  { value: "win_the_game", label: "Win the game" },
];

const BUDGET_OPTIONS = [
  { value: "", label: "Any price" },
  { value: "5", label: "Under $5" },
  { value: "20", label: "Under $20" },
  { value: "50", label: "Under $50" },
  { value: "100", label: "Under $100" },
];

// Commander Spellbook bracket tags. The server filters banned combos out of
// every response, so there is no B entry here on purpose.
const BRACKET_TAGS = { R: "Ruthless", S: "Spicy", P: "Powerful", O: "Oddball", C: "Core", E: "Exhibition" };

const PIP_COLORS = { W: "#f5f0d8", U: "#b3cee9", B: "#a89f9b", R: "#e9967a", G: "#93b483", C: "#cfcac4" };
const CI_CHIPS = ["W", "U", "B", "R", "G", "C"];

const SELECT_STYLE = {
  minHeight: 44, borderRadius: 8, border: "1.5px solid var(--line)",
  background: "var(--bg)", color: "var(--ink)",
  font: "600 14px var(--cm-sans)", padding: "0 10px", maxWidth: "100%",
};

// Shared card-name autocomplete against /api/combos/autocomplete. Every
// suggestion has combos by construction, so the badge doubles as proof the
// pick will land somewhere.
function NameAutocomplete({ value, onChange, onPick, onEnter, placeholder, ariaLabel }) {
  const [sugs, setSugs] = useState([]);
  const [open, setOpen] = useState(false);
  const [hi, setHi] = useState(-1);
  const skipRef = useRef(false);

  useEffect(() => {
    if (skipRef.current) { skipRef.current = false; return; }
    const q = String(value || "").trim();
    if (q.length < 2) { setSugs([]); setOpen(false); setHi(-1); return; }
    let aborted = false;
    const t = setTimeout(async () => {
      try {
        const r = await window.ssAuth.authedFetch("/api/combos/autocomplete?q=" + encodeURIComponent(q.slice(0, 80)));
        if (!r.ok || aborted) return;
        const body = await r.json().catch(() => ({}));
        const arr = Array.isArray(body.suggestions) ? body.suggestions : [];
        if (aborted) return;
        setSugs(arr);
        setOpen(arr.length > 0);
        setHi(-1);
      } catch (e) { /* keystroke garnish, never surface */ }
    }, 250);
    return () => { aborted = true; clearTimeout(t); };
  }, [value]);

  const pick = (name) => {
    skipRef.current = true;
    setOpen(false); setSugs([]); setHi(-1);
    onPick(name);
  };

  const onKeyDown = (e) => {
    if (e.key === "Escape") { setOpen(false); setHi(-1); return; }
    if (!open || !sugs.length) {
      if (e.key === "Enter") { e.preventDefault(); if (onEnter) onEnter(); }
      return;
    }
    if (e.key === "ArrowDown") { e.preventDefault(); setHi((h) => (h + 1) % sugs.length); }
    else if (e.key === "ArrowUp") { e.preventDefault(); setHi((h) => (h <= 0 ? sugs.length - 1 : h - 1)); }
    else if (e.key === "Enter") {
      e.preventDefault();
      if (hi >= 0 && sugs[hi]) pick(sugs[hi].name);
      else if (onEnter) onEnter();
    }
  };

  return (
    <div style={{ position: "relative" }}>
      <input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        onKeyDown={onKeyDown}
        onBlur={() => setTimeout(() => setOpen(false), 120)}
        placeholder={placeholder}
        aria-label={ariaLabel}
        autoComplete="off"
        style={{
          width: "100%", boxSizing: "border-box", minHeight: 48,
          border: "2px solid " + T.ink, borderRadius: 10,
          background: T.bg, padding: "10px 14px",
          font: "600 16px " + T.sans, color: T.ink,
        }}
      />
      {open && sugs.length > 0 && (
        <div role="listbox" style={{
          position: "absolute", left: 0, right: 0, top: "100%", zIndex: 40,
          marginTop: 4, background: T.bg2, border: "1px solid " + T.line,
          borderRadius: 10, boxShadow: "0 10px 30px rgba(0,0,0,0.25)",
          maxHeight: 320, overflowY: "auto",
        }}>
          {sugs.map((s, i) => (
            <button key={s.name} type="button" role="option" aria-selected={i === hi}
              onMouseDown={(e) => { e.preventDefault(); pick(s.name); }}
              onMouseEnter={() => setHi(i)}
              style={{
                display: "flex", alignItems: "center", gap: 8, width: "100%",
                minHeight: 44, padding: "0.45rem 0.8rem", border: "none",
                borderBottom: "1px dashed " + T.line, cursor: "pointer",
                background: i === hi ? T.accentSoft : "transparent",
                color: T.ink, textAlign: "left",
              }}>
              <span style={{ flex: 1, minWidth: 0, overflowWrap: "anywhere", fontSize: "0.95rem", fontWeight: 600 }}>{s.name}</span>
              {typeof s.combo_count === "number" && (
                <span style={{ font: "600 11px " + T.mono, color: T.ink3, flexShrink: 0 }}>
                  {s.combo_count.toLocaleString()} {s.combo_count === 1 ? "combo" : "combos"}
                </span>
              )}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// Color identity pips on the meta row.
function Pips({ letters }) {
  const list = String(letters || "").toUpperCase().split("").filter((l) => PIP_COLORS[l]);
  if (!list.length) return null;
  return (
    <span style={{ display: "inline-flex", gap: 3, alignItems: "center" }} aria-label={"Color identity " + list.join("")}>
      {list.map((l, i) => (
        <span key={l + "-" + i} title={l} style={{
          width: 12, height: 12, borderRadius: 999, background: PIP_COLORS[l],
          border: "1px solid rgba(0,0,0,0.35)", display: "inline-block",
        }} />
      ))}
    </span>
  );
}

// Overlapping thumbnails, up to four, "+N" chip beyond. Tapping one only
// opens the popup (selection never fires actions).
function CardThumbs({ cards, onOpen }) {
  const shown = cards.slice(0, 4);
  const extra = cards.length - shown.length;
  return (
    <div style={{ display: "flex", alignItems: "center" }}>
      {shown.map((c, i) => (
        <button key={(c.oracle_id || c.name) + "-" + i} type="button"
          onClick={() => onOpen(i)}
          aria-label={"Open " + c.name}
          style={{
            border: "none", background: "transparent", padding: 0, cursor: "pointer",
            marginLeft: i > 0 ? -16 : 0, position: "relative", zIndex: 4 - i,
            borderRadius: 6,
          }}>
          {c.image_small
            ? <img src={c.image_small} alt={c.name} loading="lazy"
                style={{ width: 60, display: "block", borderRadius: "4.75% / 3.5%", background: T.bg3, boxShadow: "0 1px 4px rgba(0,0,0,0.35)" }} />
            : <div style={{ width: 60, aspectRatio: "5 / 7", background: T.bg3, borderRadius: 6, display: "flex", alignItems: "center", justifyContent: "center", color: T.ink3, font: "600 9px " + T.mono, padding: 3, overflow: "hidden" }}>{c.name}</div>}
        </button>
      ))}
      {extra > 0 && (
        <span style={{ marginLeft: 6, font: "700 12px " + T.mono, color: T.ink3, background: T.bg3, borderRadius: 999, padding: "0.25rem 0.55rem" }}>+{extra}</span>
      )}
    </div>
  );
}

// Signed-in ownership strip: the verdict Spellstash adds on top of the combo
// data. Owned pieces show where they live; missing pieces get a price and a
// buy path. Never rendered when the ownership check is degraded.
function OwnershipStrip({ cards }) {
  const owned = cards.filter((c) => c.owned);
  const missing = cards.filter((c) => !c.owned);
  const allPriced = missing.length > 0 && missing.every((c) => typeof c.ck_price === "number");
  const finishSum = allPriced ? missing.reduce((s, c) => s + c.ck_price, 0) : null;
  return (
    <div style={{ marginTop: "0.55rem", padding: "0.55rem 0.7rem", background: T.bg, border: "1px solid " + T.line, borderRadius: 8 }}>
      <div style={{ font: "700 12px " + T.mono, color: T.ink2, marginBottom: "0.3rem" }}>
        You own {owned.length} of {cards.length}
      </div>
      {cards.map((c, i) => (
        <div key={(c.oracle_id || c.name) + "-" + i} style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", padding: "0.2rem 0", fontSize: "0.9rem", color: T.ink2 }}>
          {c.owned ? (
            <>
              <span aria-hidden="true" style={{ color: T.good, fontWeight: 700 }}>✓</span>
              <span style={{ minWidth: 0, overflowWrap: "anywhere" }}>{c.name}</span>
              {Array.isArray(c.locations) && c.locations.length > 0 && (
                <span style={{ color: T.ink3, fontSize: "0.85rem" }}>
                  in {c.locations[0].container_name}
                  {c.locations_more > 0 ? " and " + c.locations_more + " more" : ""}
                </span>
              )}
            </>
          ) : (
            <>
              <span style={{ minWidth: 0, overflowWrap: "anywhere" }}>{c.name}</span>
              <span style={{ color: T.ink3, fontSize: "0.85rem" }}>missing</span>
              {typeof c.ck_price === "number" && c.buy_url && (
                <a href={c.buy_url} target="_blank" rel="noopener noreferrer"
                  onClick={() => track("ComboFinderBuyClick", { card: c.name, price: c.ck_price, owned: "no" })}
                  style={{ color: T.accent, fontWeight: 700, fontSize: "0.85rem", minHeight: 44, display: "inline-flex", alignItems: "center" }}>
                  ${c.ck_price.toFixed(2)} at Card Kingdom
                </a>
              )}
            </>
          )}
        </div>
      ))}
      {finishSum != null && (
        <div style={{ font: "700 12px " + T.mono, color: T.accent, marginTop: "0.3rem" }}>
          Finish for ${finishSum.toFixed(2)}
        </div>
      )}
    </div>
  );
}

// One combo, verdict first. The almost flag switches the ownership strip for
// the deck-mode missing-pieces treatment.
function ComboRow({ combo, session, ownedDegraded, onOpenCard, almost }) {
  const cards = Array.isArray(combo.cards) ? combo.cards : [];
  const tag = BRACKET_TAGS[combo.bracket_tag];
  const templates = Array.isArray(combo.requires_templates) ? combo.requires_templates : [];
  const showStrip = !!session && !ownedDegraded && !almost && cards.length > 0;
  const missing = Array.isArray(combo.missing) ? combo.missing : [];
  const elsewhere = {};
  (Array.isArray(combo.has_elsewhere) ? combo.has_elsewhere : []).forEach((c) => {
    if (c && c.name) elsewhere[c.name] = c;
  });
  return (
    <article style={{ padding: "0.85rem 0", borderBottom: "1px solid " + T.line }}>
      <CardThumbs cards={cards} onOpen={(i) => onOpenCard(cards, i, ownedDegraded)} />
      <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", marginTop: "0.4rem" }}>
        {cards.map((c, i) => (
          <React.Fragment key={(c.oracle_id || c.name) + "-" + i}>
            {i > 0 && <span style={{ color: T.ink3, padding: "0 0.35rem" }}>+</span>}
            <button type="button" onClick={() => onOpenCard(cards, i, ownedDegraded)}
              aria-label={"Open " + c.name}
              style={{ border: "none", background: "transparent", padding: "0.3rem 0", cursor: "pointer", color: T.ink, font: "700 15px " + T.serif, minHeight: 44, textAlign: "left", overflowWrap: "anywhere" }}>
              {c.name}
            </button>
          </React.Fragment>
        ))}
      </div>
      {combo.produces && (
        <div style={{ color: T.accent, fontWeight: 700, fontSize: "0.95rem", lineHeight: 1.4, overflowWrap: "anywhere" }}>{combo.produces}</div>
      )}
      <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "0.6rem", marginTop: "0.35rem", font: "600 12px " + T.mono, color: T.ink3 }}>
        <Pips letters={combo.color_identity} />
        {tag && (
          <span title="Commander Spellbook bracket tag" style={{ border: "1px solid " + T.line, borderRadius: 999, padding: "0.15rem 0.5rem" }}>{tag}</span>
        )}
        {combo.popularity > 0 && <span>In {combo.popularity.toLocaleString()} decks</span>}
        {typeof combo.price_cents === "number" && <span>${(combo.price_cents / 100).toFixed(2)}</span>}
      </div>
      {templates.length > 0 && (
        <div style={{ color: T.ink3, fontSize: "0.85rem", marginTop: "0.3rem" }}>
          Also needs: {templates.join(", ")}, any card that fits
        </div>
      )}
      {showStrip && <OwnershipStrip cards={cards} />}
      {almost && missing.length > 0 && (
        <div style={{ marginTop: "0.55rem", padding: "0.55rem 0.7rem", background: T.bg, border: "1px solid " + T.line, borderRadius: 8 }}>
          {missing.map((c, i) => {
            const owns = c && c.name ? elsewhere[c.name] : null;
            const loc = owns && Array.isArray(owns.located_at) && owns.located_at.length > 0
              ? owns.located_at[0].container_name : null;
            return (
              <div key={(c.oracle_id || c.name) + "-" + i} style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", padding: "0.2rem 0", fontSize: "0.9rem", color: T.ink2 }}>
                <span style={{ font: "700 10.5px " + T.mono, color: "#b3372b", border: "1px solid #b3372b", borderRadius: 999, padding: "0.12rem 0.45rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>Missing</span>
                <span style={{ minWidth: 0, overflowWrap: "anywhere" }}>{c.name}</span>
                {typeof c.ck_price === "number" && c.buy_url && (
                  <a href={c.buy_url} target="_blank" rel="noopener noreferrer"
                    onClick={() => track("ComboFinderBuyClick", { card: c.name, price: c.ck_price, owned: "no" })}
                    style={{ color: T.accent, fontWeight: 700, fontSize: "0.85rem", minHeight: 44, display: "inline-flex", alignItems: "center" }}>
                    ${c.ck_price.toFixed(2)} at Card Kingdom
                  </a>
                )}
                {loc && (
                  <span style={{ background: T.accentSoft, color: T.good, border: "1px solid " + T.good, borderRadius: 999, padding: "0.15rem 0.55rem", font: "600 11.5px " + T.mono }}>
                    You own this. It lives in {loc}.
                  </span>
                )}
              </div>
            );
          })}
        </div>
      )}
      <div style={{ marginTop: "0.4rem" }}>
        <a href={combo.spellbook_url} target="_blank" rel="noopener"
          onClick={() => track("ComboFinderClickout", { combo: String(combo.combo_id || "") })}
          style={{ font: "600 12.5px " + T.mono, color: T.ink3, textDecoration: "underline", minHeight: 44, display: "inline-flex", alignItems: "center" }}>
          Full steps on Commander Spellbook
        </a>
      </div>
    </article>
  );
}

// Anon demo: SHOW the ownership overlay with a real combo (Basalt Monolith +
// Forsaken Monument) instead of describing it. Static by design; hidden the
// moment a session exists.
function DemoPanel() {
  return (
    <div style={{ border: "1.5px solid " + T.accent, background: T.accentSoft, borderRadius: 10, padding: "0.8rem 0.9rem", margin: "0.8rem 0" }}>
      <span style={{ font: "700 10.5px " + T.mono, color: T.accent, textTransform: "uppercase", letterSpacing: "0.07em", border: "1px solid " + T.accent, borderRadius: 999, padding: "0.15rem 0.5rem" }}>Example</span>
      <div style={{ marginTop: "0.55rem", padding: "0.55rem 0.7rem", background: T.bg, border: "1px solid " + T.line, borderRadius: 8, fontSize: "0.9rem", color: T.ink2, lineHeight: 1.6 }}>
        <div style={{ font: "700 12px " + T.mono, color: T.ink2 }}>You own 1 of 2</div>
        <div><span aria-hidden="true" style={{ color: T.good, fontWeight: 700 }}>✓</span> Basalt Monolith is in Trade Binder.</div>
        <div>Forsaken Monument is missing, $4.99 at Card Kingdom.</div>
      </div>
      <p style={{ margin: "0.6rem 0 0.55rem", color: T.ink, fontSize: "0.92rem", lineHeight: 1.5 }}>
        Sign in and combos check against your whole collection. Owned pieces
        show the exact box they live in.
      </p>
      <a className="cm-btn cm-btn--primary" href="/login?signup=1&next=%2Fcombos"
        onClick={() => track("ComboFinderSignupCta")}
        style={{ minHeight: 44, display: "inline-flex", alignItems: "center" }}>
        Create a free account
      </a>
    </div>
  );
}

// Visible FAQ. Must mirror the FAQPage JSON-LD in combos.html word for word.
const FAQS = [
  {
    q: "How do I find the combos in my Commander deck?",
    a: "Switch to Check a deck, add your commander, paste your list, and hit Find combos in this deck. You get the combos your deck can run now, plus the ones that are one or two cards away.",
  },
  {
    q: "Where does the combo data come from?",
    a: "Commander Spellbook, the community combo database. Each combo here links back to their site for full steps. If a combo is missing there, it will be missing here too.",
  },
  {
    q: "Can Spellstash show which combo pieces I already own?",
    a: "Yes. Sign in with a free account and combos check against your whole collection. Owned pieces show the exact deck, binder, or box they live in.",
  },
  {
    q: "What do the bracket labels mean?",
    a: "They are Commander Spellbook's bracket tags for how hard a combo pushes a deck under the Commander Brackets system. Ruthless means bracket 4 territory. Treat them as a guide, not a ruling.",
  },
];

function FaqSection() {
  return (
    <section style={{ marginTop: "2rem" }} aria-label="Frequently asked questions">
      <h2 style={{ font: "900 15px " + T.mono, color: T.ink2, textTransform: "uppercase", letterSpacing: "0.06em", margin: "0 0 0.6rem" }}>FAQ</h2>
      {FAQS.map((f) => (
        <div key={f.q} style={{ margin: "0 0 0.9rem" }}>
          <div style={{ fontWeight: 700, color: T.ink, fontSize: "0.95rem", marginBottom: "0.2rem" }}>{f.q}</div>
          <p style={{ margin: 0, color: T.ink2, fontSize: "0.92rem", lineHeight: 1.55 }}>{f.a}</p>
        </div>
      ))}
    </section>
  );
}

function CombosApp() {
  const [session, sessionLoading] = window.ssAuth.useSession();
  const [mode, setMode] = useState("search");

  // Search state.
  const [card, setCard] = useState("");
  const [ci, setCi] = useState([]); // uppercase letters; C is exclusive
  const [resultFilter, setResultFilter] = useState("");
  const [budget, setBudget] = useState("");
  const [res, setRes] = useState(null);
  const [ran, setRan] = useState(null); // params the loaded pages ran under
  const [page, setPage] = useState(1);
  const [searching, setSearching] = useState(false);
  const [error, setError] = useState(null);
  const [moreError, setMoreError] = useState(null);

  // Deck state.
  const [commander, setCommander] = useState("");
  const [deckText, setDeckText] = useState("");
  const [deckRes, setDeckRes] = useState(null);
  const [checking, setChecking] = useState(false);
  const [deckError, setDeckError] = useState(null);

  // Popup state.
  const [modal, setModal] = useState(null); // { cards, idx }
  const [wishlisted, setWishlisted] = useState(() => new Set());
  const [wishlistModal, setWishlistModal] = useState(null);
  const [ownModal, setOwnModal] = useState(null);

  // Latest search wins: a pick made while the mount-time feed is still loading
  // must run, and its response must not be clobbered by the stale request.
  const seqRef = useRef(0);
  const runSearch = useCallback(async (params, pageNum) => {
    const seq = ++seqRef.current;
    setSearching(true);
    if (pageNum === 1) { setError(null); setMoreError(null); setRes(null); }
    else setMoreError(null);
    try {
      const qs = [];
      if (params.card) qs.push("card=" + encodeURIComponent(params.card));
      if (params.ci) qs.push("ci=" + encodeURIComponent(params.ci));
      if (params.result) qs.push("result=" + encodeURIComponent(params.result));
      if (params.budget) qs.push("budget=" + encodeURIComponent(params.budget));
      qs.push("page=" + pageNum);
      const r = await window.ssAuth.authedFetch("/api/combos/search?" + qs.join("&"));
      const body = await r.json().catch(() => ({}));
      if (seq !== seqRef.current) return;
      if (!r.ok) {
        throw new Error(r.status === 503
          ? "Combo data is catching its breath. Try again in a minute."
          : (body.error || "Combo search hiccuped. Try again in a moment."));
      }
      setRan(params);
      setPage(pageNum);
      setRes((prev) => pageNum > 1 && prev
        ? { ...body, combos: [...prev.combos, ...body.combos] }
        : body);
      track("ComboFinderSearch", {
        has_card: params.card ? "yes" : "no",
        ci: params.ci || "",
        result: params.result || "",
        budget: params.budget || "",
        page: pageNum,
      });
    } catch (e) {
      if (seq !== seqRef.current) return;
      if (pageNum === 1) { setError(e.message || "Combo search hiccuped. Try again in a moment."); setRes(null); }
      else setMoreError(e.message || "Couldn't load more. Try again.");
    }
    if (seq === seqRef.current) setSearching(false);
  }, []);

  const buildParams = useCallback((cardOverride) => ({
    card: String(cardOverride != null ? cardOverride : card).trim().slice(0, 120),
    ci: ci.length ? ci.join("").toLowerCase() : "",
    result: resultFilter,
    budget: budget,
  }), [card, ci, resultFilter, budget]);

  const commit = useCallback((cardOverride) => {
    runSearch(buildParams(cardOverride), 1);
  }, [buildParams, runSearch]);

  // Land on the popular feed instantly; wait for the session so a signed-in
  // visitor's very first page carries the ownership overlay.
  const didInit = useRef(false);
  useEffect(() => {
    if (sessionLoading || didInit.current) return;
    didInit.current = true;
    track("ComboFinderView", { signed: session ? "yes" : "no" });
    runSearch({ card: "", ci: "", result: "", budget: "" }, 1);
  }, [sessionLoading, session, runSearch]);

  // C means colorless and cannot mix with colors: tapping C clears the color
  // chips, tapping any color clears C.
  const toggleCi = (letter) => {
    setCi((prev) => {
      if (letter === "C") return prev.includes("C") ? [] : ["C"];
      const colors = prev.filter((x) => x !== "C");
      return colors.includes(letter) ? colors.filter((x) => x !== letter) : [...colors, letter];
    });
  };

  const runDeck = useCallback(async () => {
    if (checking) return;
    const cmd = commander.trim().slice(0, 120);
    if (!cmd) { setDeckError("Add a commander first."); setDeckRes(null); return; }
    if (!deckText.trim()) { setDeckError("Paste your list first."); setDeckRes(null); return; }
    setChecking(true); setDeckError(null); setDeckRes(null);
    const lines = deckText.split("\n").filter((l) => l.trim()).length;
    try {
      const r = await window.ssAuth.authedFetch("/api/combos/deck", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ commander: cmd, deck_text: deckText }),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) {
        throw new Error(r.status === 503
          ? "Combo data is catching its breath. Try again in a minute."
          : (body.error || "Combo search hiccuped. Try again in a moment."));
      }
      setDeckRes(body);
      track("ComboFinderDeckCheck", { lines });
    } catch (e) {
      setDeckError(e.message || "Combo search hiccuped. Try again in a moment.");
    }
    setChecking(false);
  }, [checking, commander, deckText]);

  // Own-this filing marks the card owned everywhere it appears, including the
  // open popup, without refetching.
  const markOwned = useCallback((name) => {
    const stamp = (c) => (c.name === name ? { ...c, owned: true } : c);
    setRes((prev) => prev
      ? { ...prev, combos: (prev.combos || []).map((cb) => ({ ...cb, cards: (cb.cards || []).map(stamp) })) }
      : prev);
    setDeckRes((prev) => prev
      ? {
          ...prev,
          included: (prev.included || []).map((cb) => ({ ...cb, cards: (cb.cards || []).map(stamp) })),
          almost_included: (prev.almost_included || []).map((cb) => ({ ...cb, cards: (cb.cards || []).map(stamp) })),
        }
      : prev);
    setModal((prev) => (prev ? { ...prev, cards: prev.cards.map(stamp) } : prev));
  }, []);

  const openCard = useCallback((cards, idx, degraded) => setModal({ cards, idx, degraded: !!degraded }), []);

  // Popup order IS the combo's card order (lookup.jsx pattern). When the
  // ownership check was degraded, a missing owned flag means "unknown", not
  // "not owned": keep buy paths but never offer Want/Own off a degraded read.
  const popupCards = modal
    ? modal.cards.map((c) => ({
        name: c.name,
        image_normal: c.image_normal || c.image_small || null,
        oracle_id: c.oracle_id || null,
        type_line: c.type_line || null,
        owned: !modal.degraded && c.owned === true,
        locations: [],
        ck_price: typeof c.ck_price === "number" ? c.ck_price : null,
        buy_url: c.buy_url || null,
        wantable: !!session && !modal.degraded && !c.owned && !!c.oracle_id,
        clippable: false,
      }))
    : [];

  const combos = res && Array.isArray(res.combos) ? res.combos : [];
  const isPopular = !ran || (!ran.card && !ran.ci && !ran.result && !ran.budget);
  const included = deckRes && Array.isArray(deckRes.included) ? deckRes.included : [];
  const almostIncluded = deckRes && Array.isArray(deckRes.almost_included) ? deckRes.almost_included : [];

  const sectionHead = {
    font: "700 13px " + T.mono, color: T.ink2, margin: "1rem 0 0.2rem",
    textTransform: "uppercase", letterSpacing: "0.05em",
  };

  const searchRows = [];
  combos.forEach((cb, i) => {
    searchRows.push(
      <ComboRow key={(cb.combo_id || "combo") + "-" + i} combo={cb} session={session}
        ownedDegraded={!!(res && res.owned_degraded)} onOpenCard={openCard} />
    );
  });
  if (!session && !sessionLoading && searchRows.length > 0) {
    searchRows.splice(Math.min(3, searchRows.length), 0, <DemoPanel key="demo-panel" />);
  }

  const searchView = (
    <>
      <div className="cm-radar-addbar syn-box">
        <NameAutocomplete value={card} onChange={setCard}
          onPick={(name) => { setCard(name); commit(name); }}
          onEnter={() => commit()}
          placeholder="Card name. Try Basalt Monolith"
          ariaLabel="Search combos by card name" />
        <div style={{ display: "flex", flexWrap: "wrap", gap: "0.45rem", alignItems: "center", marginTop: "0.6rem" }}>
          {CI_CHIPS.map((l) => {
            const on = ci.includes(l);
            return (
              <button key={l} type="button" onClick={() => toggleCi(l)} aria-pressed={on}
                title={l === "C" ? "Colorless" : l}
                style={{
                  minWidth: 44, minHeight: 44, borderRadius: 999, cursor: "pointer",
                  border: on ? "2px solid " + T.accent : "1.5px solid " + T.line,
                  background: on ? T.accentSoft : T.bg,
                  color: T.ink, font: "700 14px " + T.mono,
                  display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 5,
                }}>
                <span aria-hidden="true" style={{ width: 12, height: 12, borderRadius: 999, background: PIP_COLORS[l], border: "1px solid rgba(0,0,0,0.35)" }} />
                {l}
              </button>
            );
          })}
        </div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: "0.45rem", marginTop: "0.6rem", alignItems: "center" }}>
          <select value={resultFilter} onChange={(e) => setResultFilter(e.target.value)}
            aria-label="Combo result" style={SELECT_STYLE}>
            {RESULT_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
          <select value={budget} onChange={(e) => setBudget(e.target.value)}
            aria-label="Combo budget" style={SELECT_STYLE}>
            {BUDGET_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
          <button type="button" className="cm-btn cm-btn--primary" style={{ minHeight: 44 }}
            disabled={searching} onClick={() => commit()}>
            Search
          </button>
        </div>
      </div>

      {searching && !res && (
        <>
          <p className="cm-radar-status-msg">Searching combos.</p>
          <div aria-hidden="true">
            {[0, 1, 2].map((i) => (
              <div key={i} style={{ height: 110, background: T.bg3, borderRadius: 10, margin: "0.6rem 0", opacity: 0.6 }} />
            ))}
          </div>
        </>
      )}
      {error && <p className="cm-radar-status-msg is-err">{error}</p>}

      {res && !error && (
        <div className="cm-radar-listwrap">
          <div style={sectionHead}>
            {isPopular
              ? "Popular combos right now"
              : (res.total === 1 ? "1 combo found" : (res.total || 0).toLocaleString() + " combos found")}
          </div>
          {session && res.owned_degraded && (
            <p style={{ color: T.ink3, fontSize: "0.9rem", margin: "0.4rem 0 0" }}>Ownership check is catching its breath.</p>
          )}
          {combos.length === 0 && (
            <p className="cm-radar-status-msg">No combos match that mix. Loosen a filter or try another card.</p>
          )}
          {searchRows}
          {moreError && <p className="cm-radar-status-msg is-err">{moreError}</p>}
          {page < 5 && page * 25 < (res.total || 0) && (
            <div style={{ margin: "1rem 0" }}>
              <button type="button" className="cm-btn cm-btn--ghost" style={{ minHeight: 44, width: "100%" }}
                disabled={searching}
                onClick={() => runSearch(ran, page + 1)}>
                {searching ? "Loading…" : "Load more"}
              </button>
            </div>
          )}
        </div>
      )}
    </>
  );

  const deckView = (
    <>
      <div className="cm-radar-addbar syn-box">
        <label style={{ font: "700 12px " + T.mono, color: T.ink2, display: "block", marginBottom: "0.3rem" }}>Commander</label>
        <NameAutocomplete value={commander} onChange={setCommander}
          onPick={(name) => setCommander(name)}
          onEnter={() => {}}
          placeholder="Zada, Hedron Grinder"
          ariaLabel="Commander name" />
        <textarea value={deckText} onChange={(e) => setDeckText(e.target.value)}
          rows={10}
          placeholder={"1 Basalt Monolith\n1 Rings of Brighthearth\nPaste your list. One card per line."}
          aria-label="Deck list"
          style={{
            width: "100%", boxSizing: "border-box", marginTop: "0.6rem", resize: "vertical",
            border: "2px solid " + T.ink, borderRadius: 10, background: T.bg,
            padding: "12px 14px", font: "600 14px " + T.mono, color: T.ink, lineHeight: 1.5,
          }} />
        <div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.6rem", flexWrap: "wrap" }}>
          <button type="button" className="cm-btn cm-btn--primary" style={{ minHeight: 44 }}
            disabled={checking} onClick={runDeck}>
            Find combos in this deck
          </button>
        </div>
        <p style={{ margin: "0.5rem 0 0", color: T.ink3, fontSize: "0.85rem" }}>
          Up to 400 cards. Quantities and set codes are fine, they get cleaned up.
        </p>
      </div>

      {checking && <p className="cm-radar-status-msg">Searching combos.</p>}
      {deckError && <p className="cm-radar-status-msg is-err">{deckError}</p>}

      {deckRes && !deckError && (
        <div className="cm-radar-listwrap">
          {session && deckRes.owned_degraded && (
            <p style={{ color: T.ink3, fontSize: "0.9rem", margin: "0.6rem 0 0" }}>Ownership check is catching its breath.</p>
          )}
          <div style={sectionHead}>Combos in this deck · {(included.length + (deckRes.included_overflow || 0)).toLocaleString()}</div>
          {included.length === 0 && (
            <p className="cm-radar-status-msg">No complete combos in this list yet.</p>
          )}
          {included.map((cb, i) => (
            <ComboRow key={(cb.combo_id || "inc") + "-" + i} combo={cb} session={session}
              ownedDegraded={!!deckRes.owned_degraded} onOpenCard={openCard} />
          ))}
          {deckRes.included_overflow > 0 && (
            <p style={{ color: T.ink3, fontSize: "0.9rem", margin: "0.5rem 0" }}>
              and {deckRes.included_overflow.toLocaleString()} more
            </p>
          )}
          <div style={sectionHead}>Almost there · {(almostIncluded.length + (deckRes.almost_included_overflow || 0)).toLocaleString()}</div>
          {almostIncluded.length === 0 && (
            <p className="cm-radar-status-msg">Nothing is one card away in this list.</p>
          )}
          {almostIncluded.map((cb, i) => (
            <ComboRow key={(cb.combo_id || "alm") + "-" + i} combo={cb} session={session}
              ownedDegraded={!!deckRes.owned_degraded} onOpenCard={openCard} almost />
          ))}
          {deckRes.almost_included_overflow > 0 && (
            <p style={{ color: T.ink3, fontSize: "0.9rem", margin: "0.5rem 0" }}>
              and {deckRes.almost_included_overflow.toLocaleString()} more
            </p>
          )}
        </div>
      )}
    </>
  );

  const body = () => {
    if (sessionLoading) return <p className="cm-radar-status-msg">Loading…</p>;
    return (
      <>
        <header className="cm-radar-head">
          <div className="cm-radar-head-main">
            <div className="cm-eyebrow">Combo Finder <span className="cm-beta-chip">BETA</span></div>
            <h1 className="cm-h1 cm-h1--sm">MTG Combo Finder</h1>
            <p className="cm-radar-lead">
              Search Commander combos by card, color, result, and budget. Signed
              in, each combo shows the pieces you own, the box or deck they live
              in, and the price to finish it.
            </p>
          </div>
        </header>

        <div style={{ display: "flex", gap: "0.5rem", margin: "0 0 0.9rem", flexWrap: "wrap" }}>
          <button type="button" className={mode === "search" ? "cm-btn cm-btn--primary" : "cm-btn cm-btn--ghost"}
            style={{ minHeight: 44 }} aria-pressed={mode === "search"}
            onClick={() => setMode("search")}>
            Search combos
          </button>
          <button type="button" className={mode === "deck" ? "cm-btn cm-btn--primary" : "cm-btn cm-btn--ghost"}
            style={{ minHeight: 44 }} aria-pressed={mode === "deck"}
            onClick={() => setMode("deck")}>
            Check a deck
          </button>
        </div>

        {mode === "search" ? searchView : deckView}

        <FaqSection />

        <p className="cm-radar-note">
          Combo data from Commander Spellbook, the community combo database. Each
          combo links back to their page for full steps and prerequisites.
          Search powered by Scryfall (scryfall.com). Spellstash is not produced by or endorsed by Scryfall.
          Prices from Card Kingdom, refreshed daily. Spellstash may earn a commission on Card Kingdom purchases.
          Spellstash is unaffiliated with Wizards of the Coast. Magic: The Gathering and all card data are
          property of Wizards of the Coast LLC.
        </p>
      </>
    );
  };

  return (
    <window.ssShell.AppShell active="combos" session={session}>
      <main className="cm-radar">
        {body()}
      </main>
      {modal != null && popupCards.length > 0 && (
        <CardPopup cards={popupCards} startIndex={modal.idx}
          onClose={() => setModal(null)}
          isWishlisted={(oid) => wishlisted.has(oid)}
          onWant={(popupCard) => {
            track("ComboFinderWantClick", { card: popupCard.name });
            setWishlistModal({ cards: [{ oracle_id: popupCard.oracle_id, name: popupCard.name }] });
          }}
          onOwnThis={(popupCard) => setOwnModal({ name: popupCard.name, oracle_id: popupCard.oracle_id })}
          onBuyClick={(popupCard) => track("ComboFinderBuyClick", { card: popupCard.name, price: popupCard.ck_price, owned: popupCard.owned ? "yes" : "no" })}
          suspendKeys={wishlistModal != null || ownModal != null} />
      )}
      {wishlistModal != null && (
        <WishlistModal cards={wishlistModal.cards}
          onAdded={(ids) => setWishlisted((prev) => { const next = new Set(prev); ids.forEach((id) => next.add(id)); return next; })}
          onClose={() => setWishlistModal(null)}
          onTrack={(count) => track("ComboFinderWishAdded", { count })} />
      )}
      {ownModal != null && (
        <OwnThisModal card={ownModal}
          onOwned={(ownedCard) => markOwned(ownedCard.name)}
          onClose={() => setOwnModal(null)} />
      )}
    </window.ssShell.AppShell>
  );
}

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