// Spellstash List Compare. Paste any two decklists, see what they share,
// what only the first list runs, and what only the second list runs, with
// quantity differences, type sections, CK prices, and buy paths. Anon gets
// the FULL diff free; signing in adds the ownership layer (which of the
// other list's cards you already own). All parsing and diffing happens
// server-side at POST /api/listcompare; this page never talks to Scryfall.
//
// 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 */ }
}

// Type sections, copied page-locally from bracketizer.jsx (same bucketing as
// synergy.jsx). Card lists longer than a hand get type sections.
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";
}
// Group a FLAT list; entries keep their flat index so the popup arrows walk
// the whole section-ordered list straight through the dividers.
function groupByType(cards) {
  const buckets = {};
  cards.forEach((card, flatIdx) => {
    const k = typeBucket(card.type_line);
    (buckets[k] = buckets[k] || []).push({ card, flatIdx });
  });
  return buckets;
}
const TYPE_RANK = {};
TYPE_SECTIONS.forEach((s, i) => { TYPE_RANK[s.key] = i; });
// Pre-sort into type-section order so the flat list the popup walks matches
// the rendered order exactly (stable sort keeps within-bucket server order).
function sortByType(cards) {
  return [...cards].sort((x, y) => TYPE_RANK[typeBucket(x.type_line)] - TYPE_RANK[typeBucket(y.type_line)]);
}

// Visible FAQ. Must mirror the FAQPage JSON-LD in compare.html word for word.
const FAQS = [
  {
    q: "How do I compare two MTG decklists?",
    a: "Paste one list in each box and hit Compare. You get the cards both lists share, the cards only in each one, and quantity differences, grouped by card type.",
  },
  {
    q: "Can it show cards I already own?",
    a: "Yes. Sign in with a free account and the diff marks the cards you already own. Owned cards show a check instead of a buy link.",
  },
  {
    q: "What list formats work?",
    a: "Plain text, one card per line. Quantities like 4 Lightning Bolt or 1x Sol Ring, set codes, and section headers are fine, they get cleaned up. Exports from Moxfield, Arena, and most deck builders paste in cleanly.",
  },
];

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>
  );
}

// One paste panel: optional name input + the list textarea. Stacks to a
// single column on phones via the parent grid's minmax.
function ListInput({ label, name, onName, text, onText, namePlaceholder, textPlaceholder }) {
  return (
    <div style={{ minWidth: 0 }}>
      <label style={{ font: "700 12px " + T.mono, color: T.ink2, display: "block", marginBottom: "0.3rem" }}>{label}</label>
      <input value={name} onChange={(e) => onName(e.target.value)}
        placeholder={namePlaceholder} aria-label={label + " name"} maxLength={40}
        autoComplete="off"
        style={{
          width: "100%", boxSizing: "border-box", minHeight: 44,
          border: "1.5px solid " + T.line, borderRadius: 8, background: T.bg,
          padding: "8px 12px", font: "600 14px " + T.sans, color: T.ink,
        }} />
      <textarea value={text} onChange={(e) => onText(e.target.value)}
        rows={10} placeholder={textPlaceholder} aria-label={label + " decklist"}
        style={{
          width: "100%", boxSizing: "border-box", marginTop: "0.5rem", 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>
  );
}

// A flat, type-sorted card list rendered under type sub-headers. Tapping a
// name only opens the popup; renderMeta supplies the row's right side.
function TypedCardList({ cards, onOpenCard, renderMeta }) {
  if (!cards.length) return null;
  const buckets = groupByType(cards);
  return (
    <>
      {TYPE_SECTIONS.map((sec) => {
        const entries = buckets[sec.key];
        if (!entries || !entries.length) return null;
        return (
          <div key={sec.key}>
            <div style={{ font: "700 11.5px " + T.mono, color: T.ink3, textTransform: "uppercase", letterSpacing: "0.05em", margin: "0.65rem 0 0.1rem" }}>
              {sec.label} · {entries.length}
            </div>
            {entries.map(({ card, flatIdx }) => (
              <div key={(card.oracle_id || card.name) + "-" + flatIdx}
                style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, padding: "0.15rem 0", borderBottom: "1px dashed " + T.line }}>
                <button type="button" onClick={() => onOpenCard(cards, flatIdx)}
                  aria-label={"Open " + card.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", flex: "1 1 10rem", minWidth: 0,
                  }}>
                  {card.name}
                </button>
                {renderMeta(card)}
              </div>
            ))}
          </div>
        );
      })}
    </>
  );
}

// Names the server could not resolve against Scryfall. Never dropped
// silently; collapsed so they stay out of the way of the real diff.
function UnresolvedSection({ items }) {
  const [open, setOpen] = useState(false);
  if (!items.length) return null;
  return (
    <div style={{ marginTop: "1rem" }}>
      <button type="button" onClick={() => setOpen((o) => !o)} aria-expanded={open}
        style={{
          display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8,
          width: "100%", minHeight: 44, padding: "0.3rem 0", border: "none",
          background: "transparent", cursor: "pointer", textAlign: "left",
          font: "700 13px " + T.mono, color: T.ink2, textTransform: "uppercase", letterSpacing: "0.05em",
        }}>
        <span style={{ minWidth: 0, overflowWrap: "anywhere" }}>Couldn't identify · {items.length}</span>
        <span aria-hidden="true" style={{ flexShrink: 0 }}>{open ? "▲" : "▼"}</span>
      </button>
      {open && (
        <>
          <p style={{ margin: "0 0 0.3rem", color: T.ink3, fontSize: "0.88rem", lineHeight: 1.5 }}>
            These lines read as card names but Scryfall did not recognize them. Check the spelling.
          </p>
          {items.map((u, i) => (
            <div key={u.name + "-" + i}
              style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, padding: "0.25rem 0", minHeight: 44, fontSize: "0.9rem", color: T.ink2 }}>
              <span style={{ minWidth: 0, overflowWrap: "anywhere", fontWeight: 600 }}>{u.name}</span>
              <span style={{ color: T.ink3, fontSize: "0.85rem" }}>{u.where}</span>
            </div>
          ))}
        </>
      )}
    </div>
  );
}

function CompareApp() {
  const [session, sessionLoading] = window.ssAuth.useSession();

  const [aName, setAName] = useState("");
  const [bName, setBName] = useState("");
  const [aText, setAText] = useState("");
  const [bText, setBText] = useState("");
  const [res, setRes] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

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

  const didInit = useRef(false);
  useEffect(() => {
    if (sessionLoading || didInit.current) return;
    didInit.current = true;
    track("ListCompareView", { signed: session ? "yes" : "no" });
  }, [sessionLoading, session]);

  const runCompare = useCallback(async () => {
    if (loading) return;
    if (!aText.trim()) { setError("Paste the first list."); setRes(null); return; }
    if (!bText.trim()) { setError("Paste the second list."); setRes(null); return; }
    setLoading(true); setError(null); setRes(null);
    const aLines = aText.split("\n").filter((l) => l.trim()).length;
    const bLines = bText.split("\n").filter((l) => l.trim()).length;
    try {
      const payload = { a_text: aText, b_text: bText };
      if (aName.trim()) payload.a_name = aName.trim().slice(0, 40);
      if (bName.trim()) payload.b_name = bName.trim().slice(0, 40);
      const r = await window.ssAuth.authedFetch("/api/listcompare", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) {
        throw new Error(r.status === 429
          ? "That's a lot of comparing. Give it a minute and run it again."
          : r.status === 503
            ? "Compare is catching its breath. Try again in a minute."
            : (body.error || "Compare hiccuped. Try again in a moment."));
      }
      setRes(body);
      const t = body.totals || {};
      track("ListCompareRun", {
        a_lines: aLines,
        b_lines: bLines,
        shared: t.shared_unique || 0,
        only_a: t.only_a_unique || 0,
        only_b: t.only_b_unique || 0,
      });
    } catch (e) {
      setError(e.message || "Compare hiccuped. Try again in a moment.");
    }
    setLoading(false);
  }, [loading, aText, bText, aName, bName]);

  // 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,
          shared: (prev.shared || []).map(stamp),
          only_a: (prev.only_a || []).map(stamp),
          only_b: (prev.only_b || []).map(stamp),
        }
      : prev);
    setModal((prev) => (prev ? { ...prev, cards: prev.cards.map(stamp) } : prev));
  }, []);

  const totals = res && res.totals ? res.totals : null;
  const aInfo = res && res.a ? res.a : { name: "Deck A" };
  const bInfo = res && res.b ? res.b : { name: "Deck B" };
  const degraded = !!(res && res.owned_degraded);
  const showOwnedPills = !!session && !degraded;

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

  // Popup order IS the section's type-sorted order (combos.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. ck_price + buy_url always pass through, owned cards included.
  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 sharedCards = res ? sortByType((res.shared || []).filter((c) => !c.unresolved)) : [];
  const onlyACards = res ? sortByType((res.only_a || []).filter((c) => !c.unresolved)) : [];
  const onlyBCards = res ? sortByType((res.only_b || []).filter((c) => !c.unresolved)) : [];
  const unresolvedItems = res
    ? [
        ...(res.shared || []).filter((c) => c.unresolved).map((c) => ({ name: c.name, where: "in both lists" })),
        ...(res.only_a || []).filter((c) => c.unresolved).map((c) => ({ name: c.name, where: "in " + aInfo.name })),
        ...(res.only_b || []).filter((c) => c.unresolved).map((c) => ({ name: c.name, where: "in " + bInfo.name })),
      ]
    : [];
  // Rendered counts are RESOLVED cards only, matching the rows actually on
  // screen; unresolved names live in their own group with its own count.
  // Server totals include unresolved rows, so headers built from totals would
  // count rows the sections never render.
  const sharedShown = sharedCards.length;
  const onlyAShown = onlyACards.length;
  const onlyBShown = onlyBCards.length;
  // "Same lists" only when nothing is unique, nothing failed to resolve, AND
  // no shared card differs in quantity: 4 Bolts vs 2 Bolts is not the same deck.
  const identical = !!totals
    && onlyAShown === 0 && onlyBShown === 0 && unresolvedItems.length === 0
    && sharedShown > 0
    && !sharedCards.some((c) => c.qty_diff || (c.qty_a || 0) !== (c.qty_b || 0));

  const sectionHead = {
    font: "700 13px " + T.mono, color: T.ink2, margin: "1.2rem 0 0.2rem",
    textTransform: "uppercase", letterSpacing: "0.05em",
  };
  const qtyStyle = { font: "600 12px " + T.mono, color: T.ink3, flexShrink: 0 };
  const chipStyle = {
    border: "1px solid " + T.line, borderRadius: 999, padding: "0.15rem 0.5rem",
    font: "600 11.5px " + T.mono, color: T.ink2,
  };
  const ownPill = {
    background: T.accentSoft, color: T.good, border: "1px solid " + T.good,
    borderRadius: 999, padding: "0.15rem 0.55rem", font: "600 11.5px " + T.mono,
  };
  const buyLink = {
    color: T.accent, fontWeight: 700, fontSize: "0.85rem",
    minHeight: 44, display: "inline-flex", alignItems: "center",
  };

  // Only-list row meta: owned cards trade the buy CTA for the pill; the buy
  // path stays one tap away in the popup (owned cards keep buy paths).
  const buyMeta = (c) => {
    if (showOwnedPills && c.owned === true) {
      return <span style={ownPill}>✓ you own this</span>;
    }
    if (typeof c.ck_price === "number" && c.buy_url) {
      return (
        <a href={c.buy_url} target="_blank" rel="noopener noreferrer"
          onClick={() => track("ListCompareBuyClick", { card: c.name, price: c.ck_price, owned: "no" })}
          style={buyLink}>
          ${c.ck_price.toFixed(2)} at Card Kingdom
        </a>
      );
    }
    if (c.buy_url) {
      return (
        <>
          <span style={{ color: T.ink3, fontSize: "0.85rem" }}>price unavailable</span>
          <a href={c.buy_url} target="_blank" rel="noopener noreferrer"
            onClick={() => track("ListCompareBuyClick", { card: c.name, price: null, owned: "no" })}
            style={buyLink}>
            Buy at Card Kingdom
          </a>
        </>
      );
    }
    return null;
  };
  const onlyMeta = (c) => (
    <>
      <span style={qtyStyle}>x{c.qty}</span>
      {buyMeta(c)}
    </>
  );
  const sharedMeta = (c) => (
    <>
      <span style={qtyStyle}>x{c.qty_a} · x{c.qty_b}</span>
      {(c.qty_diff || c.qty_a !== c.qty_b) && (
        <span style={chipStyle}>{aInfo.name} runs {c.qty_a}, {bInfo.name} runs {c.qty_b}</span>
      )}
    </>
  );

  const costFooter = (name, total, priced, unique) => (
    typeof total === "number" && priced > 0 ? (
      <p style={{ font: "700 12px " + T.mono, color: T.accent, margin: "0.5rem 0 0" }}>
        Adding {name}'s cards costs about ${total.toFixed(2)} at Card Kingdom ({priced} of {unique} priced).
      </p>
    ) : null
  );

  const cardsWord = (n) => (n === 1 ? "card" : "cards");

  return (
    <window.ssShell.AppShell active="compare" session={session}>
      <main className="cm-radar">
        <header className="cm-radar-head">
          <div className="cm-radar-head-main">
            <div className="cm-eyebrow">List Compare <span className="cm-beta-chip">BETA</span></div>
            <h1 className="cm-h1 cm-h1--sm">Compare two decklists</h1>
            <p className="cm-radar-lead">
              Paste two lists. See what they share, what each one adds, and
              what the swap costs. Signed in, you also see which cards you
              already own.
            </p>
          </div>
        </header>

        <div className="cm-radar-addbar syn-box">
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: "0.8rem" }}>
            <ListInput label="List A"
              name={aName} onName={setAName}
              text={aText} onText={setAText}
              namePlaceholder="Deck A, e.g. My build"
              textPlaceholder={"1 Lightning Bolt\n4 Monastery Swiftspear\nPaste the first list. One card per line."} />
            <ListInput label="List B"
              name={bName} onName={setBName}
              text={bText} onText={setBText}
              namePlaceholder="Deck B, e.g. The netdeck"
              textPlaceholder={"1 Lightning Bolt\n4 Dragon's Rage Channeler\nPaste the second list. One card per line."} />
          </div>
          <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={loading} onClick={runCompare}>
              Compare
            </button>
          </div>
          <p style={{ margin: "0.5rem 0 0", color: T.ink3, fontSize: "0.85rem" }}>
            Up to 400 unique cards per list. Quantities and set codes are fine, they get cleaned up.
          </p>
        </div>

        {loading && <p className="cm-radar-status-msg">Comparing lists.</p>}
        {error && <p className="cm-radar-status-msg is-err">{error}</p>}

        {res && !error && totals && (
          <div className="cm-radar-listwrap">
            {identical ? (
              <p className="cm-radar-sum">
                These lists are the same <b>{sharedShown.toLocaleString()}</b> {cardsWord(sharedShown)}.
              </p>
            ) : (
              <p className="cm-radar-sum">
                {aInfo.name} and {bInfo.name} share <b>{sharedShown.toLocaleString()}</b> {cardsWord(sharedShown)}.{" "}
                {aInfo.name} adds <b>{onlyAShown.toLocaleString()}</b>.{" "}
                {bInfo.name} adds <b>{onlyBShown.toLocaleString()}</b>.
              </p>
            )}
            {session && degraded && (
              <p style={{ color: T.ink3, fontSize: "0.9rem", margin: "0.6rem 0 0" }}>Ownership check is catching its breath.</p>
            )}

            <div style={sectionHead}>Both lists · {sharedShown.toLocaleString()}</div>
            {sharedShown === 0 && (
              <p className="cm-radar-status-msg">These lists share no cards.</p>
            )}
            <TypedCardList cards={sharedCards} onOpenCard={openCard} renderMeta={sharedMeta} />

            {!identical && (
              <>
                <div style={sectionHead}>Only in {aInfo.name} · {onlyAShown.toLocaleString()}</div>
                {onlyAShown === 0 && (
                  <p className="cm-radar-status-msg">Nothing is only in {aInfo.name}.</p>
                )}
                <TypedCardList cards={onlyACards} onOpenCard={openCard} renderMeta={onlyMeta} />
                {costFooter(aInfo.name, totals.only_a_ck_total, totals.only_a_priced || 0, onlyAShown)}

                <div style={sectionHead}>Only in {bInfo.name} · {onlyBShown.toLocaleString()}</div>
                {onlyBShown === 0 && (
                  <p className="cm-radar-status-msg">Nothing is only in {bInfo.name}.</p>
                )}
                <TypedCardList cards={onlyBCards} onOpenCard={openCard} renderMeta={onlyMeta} />
                {costFooter(bInfo.name, totals.only_b_ck_total, totals.only_b_priced || 0, onlyBShown)}
              </>
            )}

            <UnresolvedSection items={unresolvedItems} />

            {!session && !sessionLoading && (
              <div style={{ border: "1.5px solid " + T.accent, background: T.accentSoft, borderRadius: 10, padding: "0.8rem 0.9rem", margin: "1.2rem 0 0" }}>
                <p style={{ margin: "0 0 0.55rem", color: T.ink, fontSize: "0.92rem", lineHeight: 1.5 }}>
                  Sign in and this diff shows which cards you already own.
                </p>
                <a className="cm-btn cm-btn--primary" href="/login?signup=1&next=%2Fcompare"
                  onClick={() => track("ListCompareSignupCta")}
                  style={{ minHeight: 44, display: "inline-flex", alignItems: "center" }}>
                  Create a free account
                </a>
              </div>
            )}
          </div>
        )}

        <FaqSection />

        <p className="cm-radar-note">
          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>
      </main>
      {modal != null && popupCards.length > 0 && (
        <CardPopup cards={popupCards} startIndex={modal.idx}
          onClose={() => setModal(null)}
          isWishlisted={(oid) => wishlisted.has(oid)}
          onWant={(popupCard) => {
            track("ListCompareWantClick", { 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("ListCompareBuyClick", { 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("ListCompareWishAdded", { count })} />
      )}
      {ownModal != null && (
        <OwnThisModal card={ownModal}
          onOwned={(ownedCard) => markOwned(ownedCard.name)}
          onClose={() => setOwnModal(null)} />
      )}
    </window.ssShell.AppShell>
  );
}

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