/* AcquiCup — tournament screens: Group Standings + Knockout Bracket.
   Both are derived purely from window.ACQ.MATCHES (clientMatch shape) + TEAMS,
   so they reflect whatever fixtures/results the API or seed has loaded. */
const { useState, useEffect, useRef, useMemo } = React;

/* ===================== GROUP STANDINGS ===================== */
// Build league tables from finished group-stage matches. Football points:
// win = 3, draw = 1, loss = 0. Tiebreak: Pts, GD, GF, then code (stable).
function computeGroupTables(groupMatches) {
  const groups = {}; // groupKey -> { code -> stats }
  for (const m of groupMatches) {
    const g = m.group || "—";
    groups[g] = groups[g] || {};
    for (const code of [m.home, m.away]) {
      groups[g][code] = groups[g][code] || { code, P: 0, W: 0, D: 0, L: 0, GF: 0, GA: 0, Pts: 0 };
    }
    if (m.status === "finished" && m.homeScore != null && m.awayScore != null) {
      const h = groups[g][m.home], a = groups[g][m.away];
      const hs = +m.homeScore, as = +m.awayScore;
      h.P++; a.P++; h.GF += hs; h.GA += as; a.GF += as; a.GA += hs;
      if (hs > as) { h.W++; a.L++; h.Pts += 3; }
      else if (hs < as) { a.W++; h.L++; a.Pts += 3; }
      else { h.D++; a.D++; h.Pts++; a.Pts++; }
    }
  }
  return Object.keys(groups).sort().map((g) => ({
    group: g,
    rows: Object.values(groups[g])
      .map((t) => ({ ...t, GD: t.GF - t.GA }))
      .sort((x, y) => y.Pts - x.Pts || y.GD - x.GD || y.GF - x.GF || x.code.localeCompare(y.code)),
  }));
}

function GroupTable({ title, rows, showQ }) {
  return (
    <div className="card" style={{ overflow: "hidden" }}>
      <div className="row between" style={{ padding: "12px 16px", borderBottom: "1px solid var(--line-2)" }}>
        <span style={{ fontWeight: 800, fontSize: 15 }}>{title}</span>
        <span className="muted" style={{ fontSize: 11.5 }}>{rows.length} teams</span>
      </div>
      <div style={{ overflowX: "auto" }}>
        <table className="lb gtbl">
          <thead><tr>
            <th style={{ width: 26 }}></th>
            <th>Team</th>
            <th className="num">P</th>
            <th className="num">W</th>
            <th className="num">D</th>
            <th className="num">L</th>
            <th className="num">GD</th>
            <th className="num">Pts</th>
          </tr></thead>
          <tbody>
            {rows.map((t, i) => (
              <tr key={t.code} className={showQ && i < 2 ? "qrow" : ""}
                  style={showQ && i === 2 ? { boxShadow: "inset 3px 0 0 color-mix(in srgb, var(--accent) 45%, transparent)" } : undefined}
                  title={showQ ? (i < 2 ? "Advances: top two" : i === 2 ? "May advance: one of the eight best third-placed teams" : "") : undefined}>
                <td><span className="rank-num muted" style={{ fontSize: 13 }}>{i + 1}</span></td>
                <td>
                  <div className="row gap-10" style={{ minWidth: 0 }}>
                    <TeamBadge code={t.code} size="sm" />
                    <span className="nowrap" style={{ fontWeight: 600, fontSize: 13.5, overflow: "hidden", textOverflow: "ellipsis" }}>{T(t.code).name}</span>
                  </div>
                </td>
                <td className="mono num">{t.P}</td>
                <td className="mono num">{t.W}</td>
                <td className="mono num">{t.D}</td>
                <td className="mono num">{t.L}</td>
                <td className="mono num">{t.GD > 0 ? "+" + t.GD : t.GD}</td>
                <td className="mono num" style={{ fontWeight: 700 }}>{t.Pts}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function GroupStandings({ store, go, openMatch }) {
  const A = window.ACQ;
  const gsAll = A.MATCHES.filter((m) => m.stage === "Group Stage");
  // All group-stage fixtures form the tables (the bootstrap no longer mixes a demo
  // set with a real import, so there's no provider-prefix to disambiguate).
  const tables = computeGroupTables(gsAll).filter((t) => t.group !== "—");
  const hasKO = A.MATCHES.some((m) => m.stage && m.stage !== "Group Stage");

  return (
    <div className="col gap-24">
      <PageHead eyebrow="Tournament" title="Standings" subWidth="none"
        sub="Group tables and the knockout bracket. Groups award 3 points for a win, 1 for a draw; the top two of each group, plus the eight best third-placed teams, advance to the Round of 32." />

      {tables.length === 0 ? (
        <div className="card card-pad col center gap-10" style={{ padding: 48, textAlign: "center" }}>
          <Icon name="table" size={30} style={{ color: "var(--ink-soft)" }} />
          <span style={{ fontWeight: 700, fontSize: 16 }}>No group-stage matches yet</span>
          <span className="muted" style={{ fontSize: 13.5, maxWidth: 420 }}>Standings appear here as soon as group fixtures are loaded.</span>
        </div>
      ) : (
        <div className="col gap-12">
          <SubHead title="Group stage" subWidth="none" sub="Ranked by points, then goal difference, then goals for. Top two of each group, plus the eight best third-placed teams, advance to the Round of 32." />
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(min(100%,340px),1fr))", gap: 16, alignItems: "start" }}>
            {tables.map((t) => (
              <GroupTable key={t.group} title={"Group " + t.group} rows={t.rows} showQ={t.rows.length >= 4} />
            ))}
          </div>
        </div>
      )}

      {hasKO && (
        <>
          <hr className="hr" style={{ margin: "4px 0" }} />
          <KnockoutBracket store={store} go={go} openMatch={openMatch} embedded />
        </>
      )}
    </div>
  );
}

/* ===================== KNOCKOUT BRACKET ===================== */
// Order knockout rounds. Keyword-based so it works across naming variants
// ("Round of 16", "Quarter-finals", "Semi-finals", "3rd Place Final", "Final").
function roundRank(stage) {
  const s = String(stage || "").toLowerCase();
  if (s.includes("round of 64")) return 0;
  if (s.includes("round of 32")) return 1;
  if (s.includes("round of 16")) return 2;
  if (s.includes("quarter")) return 3;
  if (s.includes("semi")) return 4;
  if (s.includes("3rd") || s.includes("third")) return 5;
  if (s.includes("final")) return 6; // plain "Final" (checked after the others)
  return 7;
}

// Returns { stages:[...names sorted], winnerOf(match) }.
// winnerOf uses the SCORE for decisive matches; for draws (penalty shootouts,
// whose on-pitch scoreline we store) it infers the winner by ADVANCEMENT — the
// finalist who also appears in a later round. The ultimate champion of a drawn
// final can't be inferred (nobody plays after), so it returns null there.
function bracketModel(ko) {
  const stages = [...new Set(ko.map((m) => m.stage))].sort((a, b) => roundRank(a) - roundRank(b) || a.localeCompare(b));
  const idx = Object.fromEntries(stages.map((s, i) => [s, i]));
  // Track the furthest round each team reached, EXCLUDING the 3rd-place play-off
  // (reaching it means you lost your semifinal, not that you advanced). This lets
  // us infer the winner of a drawn tie as the team that genuinely progressed.
  const teamMaxStage = {};
  for (const m of ko) {
    if (roundRank(m.stage) === 5) continue;
    const si = idx[m.stage];
    for (const c of [m.home, m.away]) teamMaxStage[c] = Math.max(teamMaxStage[c] ?? -1, si);
  }
  function winnerOf(m) {
    if (m.status !== "finished" || m.homeScore == null || m.awayScore == null) return null;
    const o = outcomeOf(m.homeScore, m.awayScore);
    if (o === "home") return m.home;
    if (o === "away") return m.away;
    // Drawn on the pitch → decided by the penalty shootout when we have its score
    // (this is the only way to name a drawn FINAL, where nobody plays afterwards).
    if (m.penHome != null && m.penAway != null && m.penHome !== m.penAway)
      return m.penHome > m.penAway ? m.home : m.away;
    // Otherwise infer from who reached a later round (handles older data lacking pens).
    const si = idx[m.stage];
    if ((teamMaxStage[m.home] ?? -1) > si) return m.home;
    if ((teamMaxStage[m.away] ?? -1) > si) return m.away;
    return null; // drawn with no shootout score and no later round — genuinely undetermined
  }
  return { stages, winnerOf };
}

// parseSlot(code) — interpret a knockout participant. Resolved teams (non-empty iso)
// are { kind:"team" }. Otherwise the team doc's NAME encodes the bracket slot:
//   "1st Group C" / "2nd Group A" → a group finishing position (fillable from the
//      live group table); "3rd Group A/B/C/D/F" → an AMBIGUOUS best-third slot (left
//      as a label); "Winner Match 73" / "Winner Quarter-final 1" / "Winner Semi-final 1"
//      / "Loser Semi-final 1" → a feeder that references an earlier match.
function parseSlot(code) {
  const t = T(code);
  if (t.iso) return { kind: "team", code };
  const n = String(t.name || "").trim();
  let m;
  if ((m = n.match(/^([12])(?:st|nd|rd|th)?\s+Group\s+([A-L])$/i)))
    return { kind: "group", pos: +m[1], group: m[2].toUpperCase() };
  if (/^3rd\s+Group/i.test(n)) return { kind: "third", name: n };
  if ((m = n.match(/^Winner\s+Match\s+(\d+)$/i))) return { kind: "feeder", ref: "M" + m[1] };
  if ((m = n.match(/^Winner\s+Quarter-?final\s+(\d+)$/i))) return { kind: "feeder", ref: "QF" + m[1] };
  if ((m = n.match(/^Winner\s+Semi-?final\s+(\d+)$/i))) return { kind: "feeder", ref: "SF" + m[1] };
  if ((m = n.match(/^Loser\s+Semi-?final\s+(\d+)$/i))) return { kind: "feeder", ref: "LSF" + m[1] };
  return { kind: "placeholder", name: n, code };
}

// One participant row. Three visual modes: a resolved real team (flag + name, with
// winner tick / champion trophy + score); a TENTATIVE entrant — the team currently
// holding that group position, shown muted/italic with a slot chip (e.g. "1C"); or an
// unresolved placeholder (the slot's descriptive name in muted text).
function KoTeamRow({ side, m, winner, showScore, settled, championCode, tentativeCode }) {
  const code = side === "home" ? m.home : m.away;
  const score = side === "home" ? m.homeScore : m.awayScore;
  const slot = parseSlot(code);
  const real = slot.kind === "team";

  if (!real && tentativeCode) {
    const tag = slot.kind === "group" ? slot.pos + slot.group : "";
    return (
      <div className="ko-team ko-tentative"
        title={T(code).name + ", currently " + (slot.pos === 1 ? "1st" : "2nd") + " in Group " + slot.group}>
        <span className="ko-mark"></span>
        <TeamBadge code={tentativeCode} size="sm" />
        <span className="nm grow nowrap" style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{T(tentativeCode).name}</span>
        {tag && <span className="ko-slot-tag" title="Tentative: based on current group standings">{tag}</span>}
      </div>
    );
  }
  if (!real) {
    return (
      <div className="ko-team ko-placeholder" title={T(code).name}>
        <span className="ko-mark"></span>
        <TeamBadge code={code} size="sm" />
        <span className="nm grow nowrap muted" style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{T(code).name}</span>
      </div>
    );
  }

  const win = !!winner && winner === code;
  const lose = !!winner && winner !== code && settled;
  const champ = !!championCode && championCode === code;
  return (
    <div className={"ko-team" + (win ? " ko-win" : "") + (lose ? " ko-lose" : "")}>
      {/* fixed-width left slot marks the winner — a tick (every round, incl. the
          final) or a gold trophy for the champion; empty slots keep rows aligned */}
      <span className="ko-mark">
        {champ
          ? <Icon name="trophy" size={13} fill style={{ color: "var(--gold)" }} />
          : win
            ? <Icon name="check" size={13} stroke={2.6} style={{ color: "var(--pos-tx)" }} />
            : null}
      </span>
      <TeamBadge code={code} size="sm" />
      <span className="nm grow nowrap" style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{T(code).name}</span>
      {showScore && <span className="score-num ko-score">{score == null ? "–" : score}</span>}
    </div>
  );
}

// Shared inner content of a knockout match card (two team rows + a meta row).
// Used by both the in-flow KoCard (mobile / 3rd place) and the absolutely-
// positioned cards in the desktop BracketTree, so a match looks identical
// everywhere. `compact` tightens the spacing to fit the tree's fixed card height.
function KoCardInner({ m, winner, compact, championCode, tentative }) {
  const tz = useTimezone();
  const settled = m.status === "finished";
  const live = m.status === "live";
  const showScore = settled || live;
  const drawn = settled && outcomeOf(m.homeScore, m.awayScore) === "draw";
  const hasPens = drawn && m.penHome != null && m.penAway != null;
  // a card is "tentative" while it still shows a projected (not yet qualified) entrant
  const anyTentative = !settled && !live && !!(tentative && (tentative.home || tentative.away));
  return (
    <>
      <KoTeamRow side="home" m={m} winner={winner} showScore={showScore} settled={settled} championCode={championCode} tentativeCode={tentative && tentative.home} />
      <div style={{ height: compact ? 4 : 6 }}></div>
      <KoTeamRow side="away" m={m} winner={winner} showScore={showScore} settled={settled} championCode={championCode} tentativeCode={tentative && tentative.away} />
      <div className="row between" style={{ marginTop: compact ? 5 : 9 }}>
        <span className="muted" style={{ fontSize: 10.5 }}>{fmtMatchDate(m, tz)}</span>
        {live
          ? <StatusPill status="live" minute={m.minute} />
          : anyTentative
            ? <span className="ko-tentative-tag" title="Projected line-up: based on the current group standings, not yet decided">
                <span className="dot"></span>Tentative
              </span>
            : !settled
              ? <StatusPill status={m.status} minute={m.minute} />
              : drawn
                ? <span className="badge" style={{ fontSize: 9.5, padding: "1px 7px" }}>{hasPens ? m.penHome + "–" + m.penAway + " pens" : "Penalties"}</span>
                : null}
      </div>
    </>
  );
}

function KoCard({ m, winner, onOpen, className, championCode, tentative }) {
  return (
    <div className={"ko-card pointer" + (className ? " " + className : "")} onClick={() => onOpen && onOpen(m)}>
      <KoCardInner m={m} winner={winner} championCode={championCode} tentative={tentative} />
    </div>
  );
}

/* ---------------------------------------------------------------------------
   BRACKET MODEL — turn a flat list of knockout matches into an ordered binary
   tree that can be drawn as a real bracket, BEFORE any match is played.

   The structure is read from the placeholder participant names the import stores:
   a knockout side reading "Winner Match 73" / "Winner Quarter-final 1" / "Winner
   Semi-final 1" names the exact feeder match (resolved via sortOrder, = FIFA match
   number − 1, for "Match N"; by ordinal within the round for QF/SF). So the whole
   tree — Round of 32 leaves → Final — is known structurally from the fixtures, and
   every connector draws immediately, not only once winners are determined.

   When the data carries NO such references (e.g. a finished tournament imported with
   real teams throughout, or the demo seed), we fall back to inferring pairings from
   ADVANCEMENT: a match's winner reappears in exactly one next-round match (its
   parent). Either way matches 2k and 2k+1 of a round feed match k of the next, so a
   parent sits at the vertical midpoint of its pair. Nothing is ever fabricated. */
function buildBracketTree(ko, groupTables) {
  const { stages, winnerOf } = bracketModel(ko);
  const byStage = {};
  ko.forEach((m) => { (byStage[m.stage] = byStage[m.stage] || []).push(m); });
  // stable order within a round: fixture number, then date, then id
  const bySort = (a, b) =>
    (a.sortOrder ?? 0) - (b.sortOrder ?? 0) ||
    (a.date || "").localeCompare(b.date || "") ||
    String(a.id).localeCompare(String(b.id));
  for (const s of stages) byStage[s].sort(bySort);

  const mainStages = stages.filter((s) => roundRank(s) !== 5); // drop 3rd-place play-off
  const thirdStage = stages.find((s) => roundRank(s) === 5);
  const R = mainStages.length;
  const rounds = mainStages.map((s) => byStage[s].slice()); // rounds[r] = matches in round r
  const finalRound = R - 1;
  const teamsOf = (m) => [m.home, m.away];

  // ---- explicit feeder graph from placeholder references --------------------
  const byNumber = new Map();
  ko.forEach((m) => byNumber.set((m.sortOrder ?? -1) + 1, m)); // FIFA match number → match
  const quarterFinals = ko.filter((m) => roundRank(m.stage) === 3).sort(bySort);
  const semiFinals = ko.filter((m) => roundRank(m.stage) === 4).sort(bySort);
  const resolveFeeder = (ref) => {
    if (!ref) return null;
    if (ref[0] === "M") return byNumber.get(+ref.slice(1)) || null;
    if (ref.startsWith("QF")) return quarterFinals[+ref.slice(2) - 1] || null;
    if (ref.startsWith("SF")) return semiFinals[+ref.slice(2) - 1] || null;
    return null; // LSF (3rd-place) is off the main tree
  };
  const feedersOf = (m) => {
    const h = parseSlot(m.home), a = parseSlot(m.away);
    return {
      home: h.kind === "feeder" ? resolveFeeder(h.ref) : null,
      away: a.kind === "feeder" ? resolveFeeder(a.ref) : null,
    };
  };
  let explicit = false;
  for (const m of ko) { const f = feedersOf(m); if (f.home || f.away) { explicit = true; break; } }

  // ---- order matches so feeders are adjacent (home-feeder above away-feeder) -
  const orderedByRound = mainStages.map(() => []);
  const linksResolved = mainStages.map(() => false);

  if (explicit && R >= 1 && rounds[finalRound].length > 0) {
    const feedById = {};
    ko.forEach((m) => { feedById[m.id] = feedersOf(m); });
    orderedByRound[finalRound] = rounds[finalRound].slice();
    for (let r = finalRound; r >= 1; r--) {
      const next = [];
      for (const p of orderedByRound[r]) {
        const f = feedById[p.id] || {};
        if (f.home) next.push(f.home);
        if (f.away) next.push(f.away);
      }
      const claimed = new Set(next.map((m) => m.id));
      const orphans = rounds[r - 1].filter((m) => !claimed.has(m.id));
      orderedByRound[r - 1] = next.concat(orphans);
    }
    // A boundary draws when every parent has BOTH structural feeders adjacent.
    for (let r = 0; r < finalRound; r++) {
      const parents = orderedByRound[r + 1], kids = orderedByRound[r];
      let ok = parents.length > 0 && kids.length === parents.length * 2;
      for (let k = 0; ok && k < parents.length; k++) {
        const f = feedById[parents[k].id] || {};
        const c0 = kids[2 * k], c1 = kids[2 * k + 1];
        if (!(f.home && f.away && c0 && c1 && f.home.id === c0.id && f.away.id === c1.id)) ok = false;
      }
      linksResolved[r] = ok;
    }
  } else {
    // Fallback: infer pairings from advancement (winner reappears one round up).
    let built = false;
    if (R >= 2 && rounds[finalRound].length > 0) {
      built = true;
      let level = rounds[finalRound].slice();
      orderedByRound[finalRound] = level.slice();
      for (let r = finalRound; r >= 1; r--) {
        const prev = rounds[r - 1];
        const next = [];
        for (const p of level) {
          const homeChild = prev.find((c) => { const w = winnerOf(c); return w && w === p.home; });
          const awayChild = prev.find((c) => { const w = winnerOf(c); return w && w === p.away; });
          if (homeChild) next.push(homeChild);
          if (awayChild) next.push(awayChild);
        }
        const claimed = new Set(next.map((m) => m.id));
        const orphans = prev.filter((m) => !claimed.has(m.id));
        orderedByRound[r - 1] = next.concat(orphans);
        level = next;
      }
    }
    if (!built) mainStages.forEach((s, r) => { orderedByRound[r] = rounds[r].slice(); });
    for (let r = 0; r < finalRound; r++) {
      const parents = orderedByRound[r + 1], kids = orderedByRound[r];
      let ok = parents.length > 0 && kids.length === parents.length * 2;
      for (let k = 0; ok && k < parents.length; k++) {
        const p = parents[k], c0 = kids[2 * k], c1 = kids[2 * k + 1];
        const w0 = c0 && winnerOf(c0), w1 = c1 && winnerOf(c1);
        const set = new Set(teamsOf(p));
        if (!(w0 && w1 && set.has(w0) && set.has(w1) && w0 !== w1)) ok = false;
      }
      linksResolved[r] = ok;
    }
  }

  // ---- tentative R32 entrants from the live group tables --------------------
  // A "1st/2nd Group X" slot is filled with whoever currently holds that position,
  // but only once Group X has kicked off (so we never show a meaningless alphabetical
  // guess). Ambiguous "3rd Group …" slots and match-feeders are left as labels.
  const tentativeFor = (code) => {
    const slot = parseSlot(code);
    if (slot.kind !== "group") return null;
    const tbl = groupTables && groupTables[slot.group];
    if (!tbl || !tbl.played) return null;
    const row = tbl.rows[slot.pos - 1];
    return row ? row.code : null;
  };
  const tentativeByMatch = {};
  let anyTentative = false;
  ko.forEach((m) => {
    const home = tentativeFor(m.home), away = tentativeFor(m.away);
    tentativeByMatch[m.id] = { home, away };
    if (home || away) anyTentative = true;
  });

  // The champion is named only by a genuine, decisive Final (null on a drawn/
  // undecided final, where the stored scoreline can't crown a winner).
  const hasFinalStage = R >= 1 && roundRank(mainStages[finalRound]) === 6;
  const finalMatch = hasFinalStage && rounds[finalRound].length ? rounds[finalRound][0] : null;
  const champion = finalMatch ? winnerOf(finalMatch) : null;

  return {
    mainStages, thirdStage, byStage, rounds, orderedByRound, linksResolved,
    winnerOf, finalMatch, champion, tentativeByMatch, anyTentative,
    geom: { CARD_W: 190, COL_GAP: 48, H: 82, P: 90 },
  };
}

// Pure geometry: vertical centre of every match. Full boundaries use the exact
// midpoint of the feeder pair; partial boundaries average whatever feeders
// resolve, else stack. No DOM measurement — coordinates are correct on first paint.
function computeSlots(tree) {
  const { orderedByRound, linksResolved, winnerOf, geom } = tree;
  const { H, P, CARD_W, COL_GAP } = geom;
  const R = orderedByRound.length;
  const colX = (r) => r * (CARD_W + COL_GAP);
  const yByRound = orderedByRound.map(() => []);
  if (R === 0) return { yByRound, colX, stageWidth: 0, stageHeight: 0 };

  // Total vertical span is set by the first (widest) round; every other round's
  // matches spread evenly across that same span. For a full bracket this reduces
  // exactly to the midpoint geometry; for a sparse/partial round it stays bounded
  // (no exponential blow-up) so unresolved cards never fly off the layout.
  const span = (orderedByRound[0].length || 1) * P;
  orderedByRound[0].forEach((m, k) => { yByRound[0][k] = (k + 0.5) * P; });
  for (let r = 1; r < R; r++) {
    orderedByRound[r].forEach((p, k) => {
      if (linksResolved[r - 1]) {
        yByRound[r][k] = (yByRound[r - 1][2 * k] + yByRound[r - 1][2 * k + 1]) / 2;
      } else {
        const childYs = orderedByRound[r - 1]
          .map((c, i) => ({ w: winnerOf(c), y: yByRound[r - 1][i] }))
          .filter((o) => o.w && (o.w === p.home || o.w === p.away))
          .map((o) => o.y);
        yByRound[r][k] = childYs.length
          ? childYs.reduce((a, b) => a + b, 0) / childYs.length
          : (k + 0.5) * span / (orderedByRound[r].length || 1);
      }
    });
  }
  const allY = yByRound.reduce((a, col) => a.concat(col), []);
  const maxY = allY.length ? Math.max.apply(null, allY) : P / 2;
  return { yByRound, colX, stageHeight: maxY + H / 2 + 6, stageWidth: colX(R - 1) + CARD_W + 2 };
}

/* DESKTOP — a true bracket tree: absolutely-positioned match cards joined by SVG
   elbow connectors, converging left→right on the Final. Connectors are neutral and
   drawn from the bracket STRUCTURE, so the full tree is visible before a ball is
   kicked. Winners are bolded as results land; the champion is crowned in the Final
   card itself (a gold trophy on the winning side) — no extra column to scroll to. */
function BracketTree({ tree, slots, openMatch }) {
  const { mainStages, orderedByRound, linksResolved, winnerOf, geom, champion, finalMatch, tentativeByMatch } = tree;
  const { colX, yByRound, stageWidth, stageHeight } = slots;
  const { CARD_W, H } = geom;
  const R = mainStages.length;

  const paths = [];
  for (let r = 0; r < R - 1; r++) {
    if (!linksResolved[r]) continue;
    const kids = orderedByRound[r], parents = orderedByRound[r + 1];
    const childRight = colX(r) + CARD_W;
    const parentLeft = colX(r + 1);
    const midX = childRight + (parentLeft - childRight) / 2;
    for (let k = 0; k < parents.length; k++) {
      const parentCY = yByRound[r + 1][k];
      for (const ci of [2 * k, 2 * k + 1]) {
        if (!kids[ci]) continue;
        paths.push(`M ${childRight} ${yByRound[r][ci]} H ${midX} V ${parentCY} H ${parentLeft}`);
      }
    }
  }

  return (
    <div className="kob-scroll noscroll">
      <div className="kob-inner" style={{ width: stageWidth }}>
        <div className="kob-headers" style={{ width: stageWidth, height: 22 }}>
          {mainStages.map((s, r) => (
            <div key={s} className="kob-h eyebrow" style={{ left: colX(r), width: CARD_W }}>{s}</div>
          ))}
        </div>
        <div className="kob-stage" style={{ width: stageWidth, height: stageHeight }}>
          <svg className="kob-links" width={stageWidth} height={stageHeight} viewBox={`0 0 ${stageWidth} ${stageHeight}`}>
            {paths.map((d, i) => <path key={i} d={d} />)}
          </svg>
          {orderedByRound.map((round, r) => round.map((m, k) => {
            const championCode = champion && finalMatch && m.id === finalMatch.id ? champion : null;
            return (
              <div key={m.id} className="ko-card kob-card pointer"
                style={{ left: colX(r), top: yByRound[r][k], width: CARD_W, height: H, transform: "translateY(-50%)" }}
                onClick={() => openMatch && openMatch(m)}>
                <KoCardInner m={m} winner={winnerOf(m)} compact championCode={championCode} tentative={tentativeByMatch[m.id]} />
              </div>
            );
          }))}
        </div>
      </div>
    </div>
  );
}

/* MOBILE — a wide tree is unreadable on a phone, so stack the rounds top→bottom in
   bracket order, a chevron marking each round feeding the next. Winners bold as
   results land; the Final is the last stop. */
function BracketRoundsMobile({ tree, openMatch }) {
  const { mainStages, orderedByRound, winnerOf, champion, finalMatch, tentativeByMatch } = tree;
  return (
    <div className="col gap-14">
      {mainStages.map((s, r) => (
        <div key={s} className="col gap-9">
          <div className="row between">
            <span className="eyebrow">{s}</span>
            <span className="muted" style={{ fontSize: 11 }}>{orderedByRound[r].length} {orderedByRound[r].length === 1 ? "match" : "matches"}</span>
          </div>
          <div className="col gap-8">
            {orderedByRound[r].map((m) => (
              <KoCard key={m.id} m={m} winner={winnerOf(m)} onOpen={openMatch}
                championCode={champion && finalMatch && m.id === finalMatch.id ? champion : null}
                tentative={tentativeByMatch[m.id]} />
            ))}
          </div>
          {r < mainStages.length - 1 && (
            <div className="kob-mchev"><Icon name="chevronDown" size={18} /></div>
          )}
        </div>
      ))}
    </div>
  );
}

function KnockoutBracket({ store, go, openMatch, embedded }) {
  const A = window.ACQ;
  const ko = A.MATCHES.filter((m) => m.stage && m.stage !== "Group Stage");
  const narrow = useNarrow(820);
  // Group results feed the tentative Round-of-32 entrants, so they're part of the data.
  const gs = A.MATCHES.filter((m) => m.stage === "Group Stage");
  // Rebuild only when the knockout data OR the group standings actually change (ids /
  // status / scores / live minute, so a ticking match's clock still refreshes the cards).
  const sig = ko.map((m) => m.id + ":" + m.status + ":" + m.homeScore + ":" + m.awayScore + ":" + m.minute).join("|");
  const gsig = gs.map((m) => m.id + ":" + m.status + ":" + m.homeScore + ":" + m.awayScore).join("|");
  const tree = useMemo(() => {
    const groupTables = {};
    computeGroupTables(gs).forEach((t) => {
      groupTables[t.group] = { rows: t.rows, played: t.rows.some((x) => x.P > 0) };
    });
    return buildBracketTree(ko, groupTables);
  }, [sig, gsig]);
  const slots = useMemo(() => computeSlots(tree), [tree]);

  if (ko.length === 0) {
    if (embedded) return null; // host page guards on hasKO; nothing to show
    return (
      <div className="col gap-20">
        <PageHead eyebrow="Knockout" title="Knockout bracket"
          sub="The road to the trophy: every elimination match, round by round." />
        <div className="card card-pad col center gap-12" style={{ padding: 52, textAlign: "center" }}>
          <Icon name="bracket" size={32} style={{ color: "var(--ink-soft)" }} />
          <span style={{ fontWeight: 700, fontSize: 16 }}>The bracket isn't set yet</span>
          <span className="muted" style={{ fontSize: 13.5, maxWidth: 440 }}>
            Knockout fixtures appear here once the group stage is complete and the elimination rounds are drawn.
          </span>
          <button className="btn btn-ghost btn-sm" onClick={() => go("matches")}>Browse matches<Icon name="chevron" size={14} /></button>
        </div>
      </div>
    );
  }

  const { mainStages, thirdStage, byStage, winnerOf, finalMatch, champion, anyTentative } = tree;
  const finalDrawnUndecided = finalMatch && finalMatch.status === "finished" && !champion;

  return (
    <div className="col gap-20">
      {embedded
        ? <SubHead title="Knockout bracket" subWidth="none"
            sub="The road to the final: each match feeds the one beside it. Empty slots show the team currently on track to fill them, in grey, as group results come in." />
        : <PageHead eyebrow="Knockout" title="Knockout bracket"
            sub="The road to the final: every elimination match, round by round. Empty slots show the team currently on track to qualify, in grey; they firm up as results come in, and winners are marked as ties are decided."
            right={<span className="badge mono" style={{ flex: "none" }}>{ko.length} matches</span>} />}

      {champion && (
        <div className="card card-pad row between wrap gap-14" style={{ padding: "16px 20px",
          background: "color-mix(in srgb, var(--brand-yellow) 14%, var(--surface))",
          borderColor: "color-mix(in srgb, var(--gold) 50%, var(--line))" }}>
          <div className="row gap-14" style={{ minWidth: 0 }}>
            <Icon name="trophy" size={26} fill style={{ color: "var(--gold)", flex: "none" }} />
            <div className="col" style={{ lineHeight: 1.3 }}>
              <span className="eyebrow">Champions</span>
              <span style={{ fontWeight: 800, fontSize: 19 }}>{T(champion).name}</span>
            </div>
          </div>
          <TeamBadge code={champion} size="lg" />
        </div>
      )}

      <div className="card card-pad" style={{ padding: "18px 16px", overflow: "hidden" }}>
        {anyTentative && (
          <div className="row wrap gap-10" style={{ marginBottom: 14 }}>
            <span className="kob-legend"><span className="dot"></span>Greyed teams are tentative: the current group leaders &amp; runners-up on track to qualify</span>
          </div>
        )}
        {narrow
          ? <BracketRoundsMobile tree={tree} openMatch={openMatch} />
          : <BracketTree tree={tree} slots={slots} openMatch={openMatch} />}
        {finalDrawnUndecided && (
          <span className="muted" style={{ fontSize: 12, display: "block", marginTop: 14 }}>
            The final finished level and was decided on penalties: the stored scoreline can't name the winner.
          </span>
        )}
      </div>

      {thirdStage && (
        <div className="card card-pad col gap-10" style={{ padding: "16px 18px" }}>
          <span className="eyebrow">Third-place play-off</span>
          <span className="muted" style={{ fontSize: 12, marginTop: -2 }}>A consolation tie between the beaten semi-finalists, off the main bracket.</span>
          <div style={{ maxWidth: 360 }}>
            {byStage[thirdStage].map((m) => (
              <KoCard key={m.id} m={m} winner={winnerOf(m)} onOpen={openMatch} />
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, {
  computeGroupTables, GroupTable, GroupStandings,
  roundRank, bracketModel, KoCardInner, KoCard,
  buildBracketTree, computeSlots, BracketTree, BracketRoundsMobile, KnockoutBracket,
});
