// Represents: GET /changedomain · ChangeDomainController — see docs/ISYSTEM_REFERENCE.md §Batch 3 "Change Domain"
/* Settings ▾ → Change Domain. NEW SCREEN — the prototype had no page for this route at all
   (Phase E long-tail gap). Built from the reference only.

   Real screen actions:
     · ChangeDomainController::index          (ChangeDomainController.php:15-29) — GET /changedomain,
       route unnamed (it sits inside Route::name('admin.') but never calls ->name()), routes/admin.php:79-81.
       Loads DB::table('skins_domains')->where('skin_id', $user->skin_id)->get() (:26) — raw query builder,
       no model — and renders resources/views/admin/changedomain/index.blade.php.
     · ChangeDomainController::updateMainDomain (same file:31-62) — POST /dochangedomain,
       named admin.dochangedomain, routes/admin.php:83. Reads $request->input('domain_id') raw (:43).

   Shape of the real page: one <h3> and one table of every domain row belonging to the signed-in skin
   admin's own skin, with a radio in the last column, and a single "Save Changes" button under it.
   No filters, no sorting (no ORDER BY — rows come back in table/PK order), no pagination (->get()
   returns everything), no KPIs/totals, no export, no bulk actions, no create/edit/delete of domain
   rows. All of those absences are honoured below; the Hrs* kit is used only for page chrome and the
   table itself. Every string on the real page is hardcoded English — no backend.* keys are involved,
   so nothing here needs a "label inferred" marker.

   Permission reality (surfaced in the gate hint, not just here):
     Sidebar entry (sidebar.blade.php:569-575) is rendered under @if(isSkinAdmin() && $enable_change_domain),
     where $enable_change_domain = isSkinAdmin() && SkinsController::checkSkinSett(skin_id,'enable_change_domain')
     (sidebar.blade.php:20). Both controller methods then guard with
         if (!isSkinAdmin() && !$enable_change_domain)     (:21 and :38)
     Because $enable_change_domain already implies isSkinAdmin(), that condition collapses to
     !isSkinAdmin(): the skin flag protects the SIDEBAR LINK ONLY and the effective gate on both the
     GET and the POST is plain isSkinAdmin() (users.user_level == 2). A super admin (user_level 0) is
     DENIED — there is no isadmin() branch anywhere in the controller. A denied GET returns the literal
     string '404' with HTTP 200 (:22); a denied POST returns proper JSON 403 'Unauthorized'.

   Write path + side effects (ChangeDomainController.php:50-59, SkinsController::flushSkinCache:354-410) —
   reproduced verbatim in the "What saving does" list and the confirm dialog:
     1. SkinsController::flushSkinCache($skin_id) runs FIRST, then
     2. UPDATE skins_domains SET main = 0 WHERE skin_id = ?, then
     3. UPDATE skins_domains SET main = 1 WHERE id = ?
     — two separate statements with NO transaction, so the skin momentarily has no main domain, and
     because the flush precedes the writes a concurrent frontend request can re-cache the OLD main
     domain into skin_main_domain_<id> for up to 60 minutes.

   Known real-platform defects, handled per the repo's known-bug policy (CLAUDE.md):
   - NO VALIDATION on domain_id (no FormRequest, no required/integer rule). A missing or foreign id
     simply misses the ownership lookup `skins_domains WHERE id = ? AND skin_id = <auth skin_id>` (:44)
     and answers JSON 404 "Domain not found". Evident intent implemented here: Save Changes is disabled
     while nothing is selected, and the hint names the real 404 instead of firing a pointless request.
   - NO-OP SAVE is not blocked upstream: re-submitting the domain that is already main runs the whole
     flushSkinCache + two UPDATEs, i.e. it wipes a skin's entire cache set for no change. Evident intent
     implemented here: Save Changes is disabled when the pick equals the current main.
   - POST-SUCCESS REDIRECT IS DEAD CODE: index.blade.php:150-152 has the redirect commented out and it
     references `$domains->where('id','!=',0)->first()->url`, which is not the chosen row anyway. On the
     real platform the admin just sees a 3-second success toast and stays on /changedomain. Evident
     intent implemented here: after a successful switch the new main URL is offered as an explicit link
     the operator can follow, and the divergence is stated on screen.
   - MULTIPLE / ZERO main rows are possible: neither this controller nor the Skins editor's
     updateDomainsSkin (SkinsController:945-986) validates how many rows carry main=1, and
     getSkinDomain() is a plain `WHERE main = 1`. The anomaly banner below only renders when the data
     is in that state; the seeded mock has exactly one main, so it stays hidden by default.

   Deliberately NOT added (the real screen has none of these): DNS lookups, ownership/TXT verification,
   propagation or health checks, SSL/certificate status, "test this domain", add/remove/rename of a
   domain row, a skin picker (the query is hard-scoped to Auth::user()->skin_id), an audit/history list
   (no log is written — the controller records nothing), scheduling, and any rollback action.

   CROSS-SCREEN: `skins_domains` rows themselves are created and edited on the Skins editor's Domains
   tab (GET /skins/{id}/domains → SkinsController::showSkinDomains:1285-1302, save case "domains":3525-3533
   → updateDomainsSkin:945-986, isadmin() only), surfaced in the prototype by the Skins unified editor.
   This page deliberately does not duplicate that editing surface — it links to it in words and keeps the
   same vocabulary (Domain / URL / Main, table `skins_domains`, row key `uniqid`).

   <!-- SUGGESTION: give /changedomain a route name and wrap the two UPDATEs in a DB transaction, then move flushSkinCache() to AFTER the commit. Today the flush runs first, so a single frontend request landing between the flush and the second UPDATE re-caches the old main domain into skin_main_domain_<id> for the full 60-minute TTL — the operator sees "success" while the site keeps redirecting to the old address. -->
   <!-- SUGGESTION: fix the gate. `if(!isSkinAdmin() && !$enable_change_domain)` collapses to `!isSkinAdmin()` because $enable_change_domain already requires isSkinAdmin(); the enable_change_domain skin setting therefore hides the menu entry but protects nothing. Use `if(!isSkinAdmin() || !checkSkinSett($skin_id,'enable_change_domain'))`, and return a real abort(404) instead of `return '404';` with HTTP 200. -->
   <!-- SUGGESTION: validate domain_id (required|integer) and reject a no-op switch before flushSkinCache() runs — re-selecting the current main currently clears skin_main_domain_*, the CORS origin allowlist, providers_*, count_games_*, subcategories_* and jackpot_winners_* for the whole skin in exchange for nothing. -->
   <!-- SUGGESTION: enforce exactly one main row per skin (partial unique index or a service guard). Neither ChangeDomainController nor SkinsController::updateDomainsSkin checks the count, and getSkinDomain() is a bare `WHERE main = 1`, so a skin can silently end up with zero mains (no canonical URL) or several (first-match wins, non-deterministically). --> */

const { useState: hcdUseState, useMemo: hcdUseMemo } = React;

/* Deterministic PRNG (FNV-1a + mulberry32) — same convention as the sibling Host pages, so the
   seeded rows render identically on every load. */
const hcdHash = (s) => { let h = 2166136261 >>> 0; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; };
const hcdRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
/* skins_domains.uniqid is md5(uniqid()) written server-side by updateDomainsSkin (SC:971) — 32 hex
   chars. This screen never reads it (it posts the numeric row id as domain_id); it is carried on the
   mock rows only so the record shape matches the real table. */
const hcdUniqid = (seed) => { const rnd = hcdRng(hcdHash(seed)); let out = ""; for (let i = 0; i < 32; i++) out += Math.floor(rnd() * 16).toString(16); return out; };

/* The signed-in user is a Skin admin (user_level 2); the whole screen is scoped to their own
   Auth::user()->skin_id. Skin 69 / "Jokerenvivo" is the same record the Skins editor and the Payments
   brand switcher use, so the two surfaces never disagree about which brand is being talked about. */
const HCD_SKIN = { id: 69, name: "Jokerenvivo", code: "jokerenvivo" };

/* `skins_domains` rows for that skin. Columns are exactly the real ones: id, domain (indexed), url,
   main (boolean NULLABLE — 1 = main, 0 or NULL = secondary), skin_id (indexed), uniqid. Row 404 keeps
   main = null on purpose: the migration allows it and the UPDATE ... SET main = 0 path is what
   normalises those rows to 0. */
/* ---------- the row source ------------------------------------------------
   Was five invented jokerenvivo.* domains. Now skin_domains, live.

   isystem stores the flag as `main` (1 / 0 / NULL — the NULLs are why this
   screen has an anomaly banner at all). Ours is `is_primary boolean not null`,
   so the tri-state cannot happen; the banner stays because the screen mirrors a
   real platform behaviour, and it simply never fires here.

   `url` has no column: isystem derives the launch URL from the domain. Derived
   the same way rather than invented. */
const hcdRow = (d, i) => ({
  id: d.id,
  domain: d.domain,
  url: `https://${d.domain}`,
  main: d.is_primary ? 1 : 0,
  skin_id: d.skin_id,
  uniqid: hcdUniqid(`skins_domains|${d.skin_id}|${d.id}|${d.domain}`),
});

/* Everything SkinsController::flushSkinCache($skin_id) drops (SC:354-410), in the order the reference
   lists it. Rendered verbatim so the operator can see the blast radius before confirming. */
const HCD_FLUSH = [
  <><code>skin_main_domain_{HCD_SKIN.id}</code> — the 60-minute cache of this skin's canonical domain</>,
  <>the <b>CORS origin allowlist</b> (<code>CorsOriginService::forget()</code>) — it is derived from <code>skins_domains</code>, so it must drop or the new domain is refused as a cross-origin caller</>,
  <>the domain-keyed skin cache, plus this skin's settings, custom-settings, data and code caches</>,
  <><code>count_games_*</code>, <code>providers_*</code>, <code>subcategories_*</code>, <code>jackpot_winners_*</code></>,
  <>the betmaker-subdomain variants and the <code>gameapi</code> / <code>gameapi-staging</code> / <code>gameapi-dev</code> subdomain keys</>,
];

/* ------------------------------------------------------------------ *
 * Small presentational helpers — the Settings.jsx sectioned-panel
 * vocabulary (soft card, compact uppercase heading, label/value rows).
 * ------------------------------------------------------------------ */
const HcdPanel = ({ tone, icon, title, note, children }) => (
  <div className={`hcd-panel${tone ? ` hcd-panel--${tone}` : ""}`}>
    {title && (
      <div className="hcd-panel__h">
        {icon && <Icon name={icon} size={12} />}
        <span>{title}</span>
        {note && <em>· {note}</em>}
      </div>
    )}
    <div className="hcd-panel__b">{children}</div>
  </div>
);

const HcdKv = ({ k, children }) => (
  <div className="hcd-kv"><span className="hcd-kv__k">{k}</span><span className="hcd-kv__v">{children}</span></div>
);

/* ------------------------------------------------------------------ *
 * Confirm dialog. The real one is a SweetAlert2 popup loaded from a CDN
 * (index.blade.php:115-171) whose title is "Are you sure?", whose text is
 * "Changing the domain will redirect the site to the selected domain!"
 * and whose confirm button reads "Yes, change it!". Those three strings
 * are kept exactly; what is added is the unambiguous named summary —
 * the skin, the domain being left and the domain being adopted — plus
 * the write-path caveats, because this flips a live brand's public
 * address and a bare "Are you sure?" does not say which brand or which
 * two domains are involved. Full-screen on mobile (brief §11).
 * ------------------------------------------------------------------ */
const HcdConfirm = ({ from, to, others, onCancel, onConfirm }) => (
  <div className="bp-modal-scrim hcd-scrim" onClick={onCancel}>
    <div className="bp-modal hcd-modal" onClick={e => e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="hcd-confirm-title">
      <div className="hcd-modal__head">
        <div className="hcd-modal__title" id="hcd-confirm-title">
          <span className="hcd-modal__warnic"><Icon name="alert" size={14} /></span>
          Are you sure?
        </div>
        <button className="hcd-x" title="Close" onClick={onCancel}><Icon name="x" size={14} /></button>
      </div>

      <div className="hcd-modal__body">
        <p className="hcd-lead">Changing the domain will redirect the site to the selected domain!</p>

        <div className="hcd-confirm">
          <div className="hcd-confirm__skin">
            <span className="hcd-confirm__lab">Brand</span>
            <b>{HCD_SKIN.name}</b>
            <span className="hcd-confirm__id">skin_id {HCD_SKIN.id} · <code>{HCD_SKIN.code}</code></span>
          </div>
          <div className="hcd-diff">
            <div className="hcd-diff__side hcd-diff__side--from">
              <span className="hcd-diff__lab">Main domain now</span>
              <b className="hcd-diff__dom">{from ? from.domain : "— none set —"}</b>
              <span className="hcd-diff__url">{from ? from.url : "getSkinDomain() resolves to nothing"}</span>
            </div>
            <div className="hcd-diff__arrow" aria-hidden="true"><Icon name="chevron_right" size={16} /></div>
            <div className="hcd-diff__side hcd-diff__side--to">
              <span className="hcd-diff__lab">Main domain after saving</span>
              <b className="hcd-diff__dom">{to.domain}</b>
              <span className="hcd-diff__url">{to.url}</span>
            </div>
          </div>
        </div>

        <ul className="hcd-fx">
          <li>Every visitor who lands on <b>{from ? from.domain : "any other domain of this skin"}</b>{others > 0 ? <> — and on the {others} other registered {others === 1 ? "domain" : "domains"} of this brand</> : null} is redirected to <b>{to.domain}</b> over https, keeping the path and any subdomain prefix.</li>
          <li><code>getSkinDomain()</code> starts returning <b>{to.domain}</b> everywhere it is used as the canonical domain/URL for this skin.</li>
          <li>The write is <b>not wrapped in a transaction</b>: <code>main</code> is cleared for the whole skin first and set on the new row second, so for a moment this brand has no main domain at all.</li>
          <li>The cache flush runs <b>before</b> those two writes, so a request arriving in that window can re-cache the <b>old</b> domain for up to 60 minutes.</li>
          <li><b>No DNS record is touched.</b> If {to.domain} is not already pointed at this platform, saving takes the brand offline for visitors.</li>
        </ul>
      </div>

      <div className="hcd-modal__foot">
        <button className="btn btn--secondary" onClick={onCancel}>Cancel</button>
        <button className="btn btn--danger hcd-go" onClick={onConfirm}>
          <Icon name="check" size={13} /> Yes, change it!
        </button>
      </div>
    </div>
  </div>
);

/* ================================================================== *
 * Page
 * ================================================================== */
const SetChangeDomain = () => {
  const feed = useHrsFetch(() => window.sb.list("skinDomains", { limit: 200 }), []);
  const rows = hcdUseMemo(() => (feed.data || []).map(hcdRow), [feed.data]);
  const seeded = rows;
  /* getSkinDomain() is a bare `WHERE main = 1` — first match wins, which is why the anomaly banner
     below exists at all. */
  const mains = rows.filter(r => !!r.main);
  const current = mains.length > 0 ? mains[0] : null;
  const [picked, setPicked] = hcdUseState(() => { const m = seeded.filter(r => !!r.main)[0]; return m ? m.id : null; });
  const [confirming, setConfirming] = hcdUseState(false);
  const [done, setDone] = hcdUseState(null);

  const target = rows.filter(r => r.id === picked)[0] || null;
  const dirty = !!target && (!current || target.id !== current.id);

  /* Mirrors the real write path exactly: SET main = 0 for the whole skin (which also normalises the
     NULL rows), then SET main = 1 on the chosen row. */
  /* Switching the main domain is a write, and src/supabase.js is read-only by
     design (stage 7 of docs/WORK_PLAN.md). It used to flip the flag in a local
     array and toast "Main domain changed" — which reads as success, survives
     nothing, and is the exact failure mode this whole exercise removed.
     It now reports what it WOULD write. */
  const apply = () => {
    const from = current, to = target;
    setConfirming(false);
    hrsToast("Not saved — no write path yet",
      `Would set skin_domains.is_primary = true on ${to.domain} and false on every other domain of ${HCD_SKIN.name}. Reads are live; writes land in stage 7.`);
  };

  /* `#` is $loop->iteration in the blade — a row counter, NOT skins_domains.id. */
  const numbered = rows.map((r, i) => ({ ...r, _n: i + 1 }));
  const cellCls = (r) => picked === r.id ? "hcd-c-picked" : (r.main ? "hcd-c-main" : "");

  const columns = [
    { key: "_n", label: <>#<Tip size={11}>Row counter (<code>$loop-&gt;iteration</code>), not <code>skins_domains.id</code> — the real table shows the position, and the radio posts the row id as <code>domain_id</code>.</Tip></>,
      align: "center", width: 64, cellClass: cellCls, render: r => <span className="hcd-n">{r._n}</span> },
    { key: "domain", label: "Domain", cellClass: cellCls, render: r => (
      <div className="hcd-dom">
        <b>{r.domain}</b>
        {r.main ? <span className="hcd-pill hcd-pill--main">Main</span> : null}
        {picked === r.id && !r.main ? <span className="hcd-pill hcd-pill--pending">Pending</span> : null}
        {r.main === null ? <span className="hcd-pill hcd-pill--null" title="skins_domains.main is NULL on this row — the column is boolean nullable; NULL and 0 both mean secondary."><code>main NULL</code></span> : null}
      </div>
    ) },
    { key: "url", label: "URL", cellClass: cellCls, render: r => <span className="hcd-url">{r.url}</span> },
    /* The real column is a bare radio, name="domain_id", value=skins_domains.id, pre-checked from
       `main` (index.blade.php:22-44). Kept as one radio group; the row is clickable as a larger hit
       target for the same control. */
    { key: "main", label: "Main", align: "center", width: 96, cellClass: cellCls, render: r => (
      <label className="hcd-radio" onClick={e => e.stopPropagation()}>
        <input type="radio" name="domain_id" value={r.id} checked={picked === r.id}
          onChange={() => setPicked(r.id)} aria-label={`Make ${r.domain} the main domain`} />
        <span className="hcd-radio__dot" aria-hidden="true" />
      </label>
    ) },
  ];

  const otherCount = Math.max(0, rows.length - 1);

  return (
    <HrsShell
      title="Change Domain" /* hardcoded English in the real sidebar and view — no backend.* key exists */
      subtitle={`Choose which registered domain ${HCD_SKIN.name} serves from`}
      gate={<>Real-platform access: <b>Skin admin only</b> — <code>isSkinAdmin()</code> (<code>users.user_level</code> = 2). No <code>checkUserBoPerm</code> is involved anywhere, and a <b>super admin (user_level 0) is denied</b> — the controller has no <code>isadmin()</code> branch. </>}
      gateNote={<>The skin flag <code>enable_change_domain</code> gates the <b>sidebar link only</b>. Both controller methods guard with <code>if(!isSkinAdmin() &amp;&amp; !$enable_change_domain)</code>, and <code>$enable_change_domain</code> is itself defined as <code>isSkinAdmin() &amp;&amp; checkSkinSett(skin_id,'enable_change_domain')</code> — so the condition <b>reduces to <code>!isSkinAdmin()</code></b>. Any skin admin can open <code>/changedomain</code> and post <code>/dochangedomain</code> directly with the flag switched off; the flag hides the menu entry and protects nothing. A denied GET also answers the literal string <code>404</code> with HTTP <b>200</b>, not a real 404 (denied POST does return JSON 403 <code>Unauthorized</code>).</>}
      explainer={{ title: "What this screen does, in plain English", bullets: [
        <>One radio per domain already registered against <b>your own skin</b> (<code>skins_domains WHERE skin_id = {HCD_SKIN.id}</code>). Saving flips which row carries <code>main = 1</code>. Nothing else on the row changes.</>,
        <>What <b>main</b> means at runtime: the <code>frontendweb</code> <code>SkinSetting</code> middleware resolves the skin's main domain (cached 60 minutes as <code>skin_main_domain_{HCD_SKIN.id}</code>) and <b>redirects any visitor who lands on one of this skin's other domains to it</b>, over https, preserving the URI and subdomain prefix. <code>SkinsController::getSkinDomain()</code> (<code>WHERE main = 1</code>) is the canonical domain/URL resolver used across the platform.</>,
        <><b>No DNS is touched.</b> Registering a hostname, pointing it at the platform, certificates and propagation all happen outside this system — this screen only re-flags a row among domains that already exist in <code>skins_domains</code>. If the domain you pick is not already pointed here, the brand goes dark for visitors.</>,
        <>Domain rows themselves — adding, renaming, changing the URL, removing — are <b>not</b> edited here. They live on the <b>Skins editor → Domains tab</b> (<code>GET /skins/{"{id}"}/domains</code> → <code>SkinsController::updateDomainsSkin</code>), which is <code>isadmin()</code>-only. This screen is the one place a skin admin can change which of them is main.</>,
        <>Nothing is logged: the controller writes no audit row, so there is no history of past switches to show here.</>,
      ] }}>

      {/* Destructive-adjacent framing, matching the sibling counter screens' risk banner. */}
      <div className="hcd-risk">
        <span className="hcd-risk__ic"><Icon name="globe" size={13} /></span>
        <div>
          <b>This changes the public address of a live brand.</b> Saving redirects every visitor of {HCD_SKIN.name} to the domain you select and drops the whole skin's cache, including the CORS origin allowlist. It takes effect immediately and there is no undo button — switching back means coming here again.
        </div>
      </div>

      <HrsSection title="Current main domain" sub={`Resolved the way the platform resolves it — SkinsController::getSkinDomain(), skins_domains WHERE main = 1 AND skin_id = ${HCD_SKIN.id}`}>
        <HcdPanel>
          <div className="hcd-now">
            <div className="hcd-now__main">
              <span className="hcd-now__lab">Main domain</span>
              <b className="hcd-now__dom">{current ? current.domain : "— none set —"}</b>
              <span className="hcd-now__url">{current ? current.url : "getSkinDomain() resolves to nothing for this skin"}</span>
            </div>
            <div className="hcd-now__meta">
              <HcdKv k="Brand">{HCD_SKIN.name} <span className="hcd-mut">· skin_id {HCD_SKIN.id} · <code>{HCD_SKIN.code}</code></span></HcdKv>
              <HcdKv k="Registered domains">{rows.length} <span className="hcd-mut">· {otherCount} of them redirect to the main domain</span></HcdKv>
              <HcdKv k="Cache key">{current ? <code>skin_main_domain_{HCD_SKIN.id}</code> : <span className="hcd-mut">nothing cached</span>} <span className="hcd-mut">· 60 min TTL</span></HcdKv>
            </div>
          </div>
        </HcdPanel>

        {/* Only renders when the data is in a state neither this controller nor updateDomainsSkin
            prevents. The seeded rows have exactly one main, so it stays hidden by default. */}
        {mains.length !== 1 && (
          <HcdPanel tone="warn" icon="alert" title={mains.length === 0 ? "This skin has no main domain" : `This skin has ${mains.length} main domains`}>
            {mains.length === 0
              ? <>No row carries <code>main = 1</code>, so <code>getSkinDomain()</code> returns nothing and the frontend has no canonical address to redirect to. Nothing prevents this state: the Skins editor's Domains tab saves <code>main</code> straight from checkbox presence with no validation.</>
              : <>More than one row carries <code>main = 1</code>. <code>getSkinDomain()</code> is a plain <code>WHERE main = 1</code>, so whichever row the database returns first wins — and it may not be the same one every time. Saving from this screen normalises it back to exactly one.</>}
          </HcdPanel>
        )}

        {done && (
          <HcdPanel tone="ok" icon="check" title="Main domain changed">
            <>
              {HCD_SKIN.name} now serves from <b>{done.to}</b>{done.from ? <> instead of <b>{done.from}</b></> : null}. Visitors on the other registered domains are redirected here from now on.
              <div className="hcd-done">
                <a className="hcd-done__link" href={done.url} target="_blank" rel="noopener noreferrer">
                  <Icon name="external" size={12} /> {done.url}
                </a>
              </div>
              {/* Known-bug policy: the real screen's post-success redirect is commented out
                  (index.blade.php:150-152) and points at the wrong row anyway, so the admin sees only a
                  3-second toast. The evident intent — take me to the new address — is offered as an
                  explicit link instead of an automatic jump, and stated here rather than hidden. */}
              <div className="hcd-note">
                On the real platform nothing navigates after this point: the redirect to the new domain is commented out in the page's script and the admin stays on <code>/changedomain</code> after a 3-second toast. The link above is this prototype supplying that missing step explicitly.
              </div>
            </>
          </HcdPanel>
        )}
      </HrsSection>

      <HrsSection
        title="Registered domains"
        sub="Every skins_domains row for this skin, in table order — the real screen has no filters, no sorting and no pagination">
        <HrsAsync state={feed} skeletonRows={5} skeletonCols={4}
                  empty="This skin has no rows in skin_domains yet.">
          {() => (<>
        <HrsTable
          columns={columns} rows={numbered} rowKey="id"
          onRowClick={(r) => setPicked(r.id)}
          empty={<>This skin has no rows in <code>skins_domains</code>. Domains are added on the Skins editor's Domains tab, not here.</>}
          renderCard={r => (
            <>
              <div className="hrs-card__top">
                <b>{r.domain}</b>
                <label className="hcd-radio" onClick={e => e.stopPropagation()}>
                  <input type="radio" name="domain_id_m" value={r.id} checked={picked === r.id}
                    onChange={() => setPicked(r.id)} aria-label={`Make ${r.domain} the main domain`} />
                  <span className="hcd-radio__dot" aria-hidden="true" />
                </label>
              </div>
              <div className="hcd-card__url">{r.url}</div>
              <div className="hcd-card__pills">
                <span className="hcd-n">#{r._n}</span>
                {r.main ? <span className="hcd-pill hcd-pill--main">Main</span> : null}
                {picked === r.id && !r.main ? <span className="hcd-pill hcd-pill--pending">Pending</span> : null}
                {r.main === null ? <span className="hcd-pill hcd-pill--null"><code>main NULL</code></span> : null}
              </div>
            </>
          )} />
          </>)}
        </HrsAsync>
      </HrsSection>

      <HrsSection title="Apply the change" sub="POST /dochangedomain · ChangeDomainController::updateMainDomain">
        <HcdPanel>
          <div className="hcd-pending">
            {dirty ? (
              <div className="hcd-diff hcd-diff--inline">
                <div className="hcd-diff__side hcd-diff__side--from">
                  <span className="hcd-diff__lab">From</span>
                  <b className="hcd-diff__dom">{current ? current.domain : "— none set —"}</b>
                  <span className="hcd-diff__url">{current ? current.url : "no canonical URL today"}</span>
                </div>
                <div className="hcd-diff__arrow" aria-hidden="true"><Icon name="chevron_right" size={16} /></div>
                <div className="hcd-diff__side hcd-diff__side--to">
                  <span className="hcd-diff__lab">To</span>
                  <b className="hcd-diff__dom">{target.domain}</b>
                  <span className="hcd-diff__url">{target.url}</span>
                </div>
              </div>
            ) : (
              <div className="hcd-nochange">
                <Icon name="info" size={13} />
                <span>{picked == null
                  ? <>Nothing selected. Pick a domain above to enable the save.</>
                  : <><b>{target ? target.domain : ""}</b> is already this skin's main domain — there is nothing to change.</>}</span>
              </div>
            )}
          </div>

          <div className="hcd-fxblock">
            <div className="hcd-fxblock__h"><Icon name="zap" size={12} /> What saving does, in order</div>
            <ol className="hcd-steps">
              <li><code>SkinsController::flushSkinCache({HCD_SKIN.id})</code> — runs <b>first</b>, dropping:
                <ul className="hcd-fx hcd-fx--sub">{HCD_FLUSH.map((f, i) => <li key={i}>{f}</li>)}</ul>
              </li>
              <li><code>UPDATE skins_domains SET main = 0 WHERE skin_id = {HCD_SKIN.id}</code></li>
              <li><code>UPDATE skins_domains SET main = 1 WHERE id = {target ? target.id : "…"}</code></li>
            </ol>
            <div className="hcd-note">
              Those two statements are <b>not</b> in a transaction, so the brand momentarily has no main domain; and because the flush precedes them, a frontend request arriving in that window can re-cache the <b>old</b> domain into <code>skin_main_domain_{HCD_SKIN.id}</code> for up to 60 minutes. Nothing is written to any audit log.
            </div>
          </div>

          <div className="hcd-actbar">
            <button className="hrs-btn hrs-btn--filters hcd-save" disabled={!dirty} onClick={() => setConfirming(true)}>
              <Icon name="check" size={14} /> Save Changes
            </button>
            {/* Known-bug policy divergence, stated on screen rather than only in a comment. */}
            <span className="hcd-hint">
              {picked == null
                ? <>The real endpoint applies no validation to <code>domain_id</code> — no <code>required</code>, no <code>integer</code>, no FormRequest. A missing or foreign id just misses the ownership lookup and answers JSON 404 <code>Domain not found</code>. The button is disabled here instead of sending that request.</>
                : !dirty
                  ? <>Re-submitting the current main is allowed upstream and runs the full cache flush plus both UPDATEs for no change. The button is disabled here instead.</>
                  : <>Ownership is the only server-side guard: <code>skins_domains WHERE id = {target.id} AND skin_id = {HCD_SKIN.id}</code>. Anything else answers JSON 404 <code>Domain not found</code>.</>}
            </span>
          </div>
        </HcdPanel>
      </HrsSection>

      {confirming && target && (
        <HcdConfirm from={current} to={target} others={otherCount}
          onCancel={() => setConfirming(false)} onConfirm={apply} />
      )}
    </HrsShell>
  );
};

window.SetChangeDomain = SetChangeDomain;
