/* app/set-platform.jsx — Settings pages: API + Webhooks + Domains.
   ============================================================================================
   OWNERSHIP: this file and app/set-platform.css are owned by ONE agent. Nothing else in the repo needs
   to change to build these pages — the <script>/<link> tags and the `?v=` bumps are already wired,
   and the router finds a page purely by its key in the map below.

   CONTRACT
     - Read docs/SETTINGS-REFERENCE.md for what each page must contain, then the root CLAUDE.md.
     - Build every page out of `window.E8Set` primitives. Do not hand-roll a row, card or control;
       if a primitive is missing, say so rather than inventing a local one — dimensional drift
       between pages is the single most visible defect on this surface.
     - All CSS goes in app/set-platform.css under a `.e8-set-platform-*` prefix. Never edit app/set-core.css.
     - ZERO inline `style={{…}}` touching fontSize/color/display/gap/flex/margin/padding: a new
       file's inline-style budget is 0 and one such object fails `npm run lint`.
     - Colours are `--ui-*` tokens or `color-mix()` on them. Text sizes are `--ui-text-*`
       (11.5 / 12.5 / 13.5 / 15 / 18 / 24 / 28 — there is no 14px). Icons are `--ui-icon-*`.

   THE THREE PAGES ARE ONE PAGE WEARING THREE HATS, DELIBERATELY.
   API is the only one of the three the reference screenshotted. Webhooks and Domains are specified
   as "mirror API" and "info banner + one counted row", so all three are built from the SAME two-
   section skeleton — a §Resources card of iconned documentation rows, then a §<thing> card whose
   header carries the live count and the create action. Copying that skeleton is the point: the
   reference's polish is dimensional identity between neighbouring pages, and these three are
   neighbours in the rail.

   WHAT THE FIRST CRITIC PASS CHANGED, AND WHY (so it does not get undone)

     SAME SKELETON, NOT THE SAME PROSE. Dimensional identity is the goal; identical COPY is not.
     The first cut carried a 40-word permission paragraph on API and a near-verbatim twin on
     Webhooks, which made adjacent nav items read as generated rather than designed. The permission
     fact is now stated once per page in one line, next to a visible `Admin only` chip on the
     disabled create button — `PlatCreateAction` and `PlatGate` below are the single definitions of
     both, so the three pages cannot drift apart or repeat themselves at length.

     ONE LEFT EDGE PER CARD. The reference is explicit that iconned and icon-less content must not
     be mixed inside one card. Every card here is now all-iconned: the registry header, every body
     row, and the inline empty state (indented to the row text edge in CSS, since an empty state has
     no glyph of its own). Measured: every text run in every card starts at 71px from the card's
     left edge. The three full-bleed strips — banner, secret reveal, DNS panel — are the documented
     exception: a notice runs card-edge to card-edge by design.

     THE HEADER IS A LABEL, THE EMPTY STATE IS THE MESSAGE. `No API keys registered` in the head
     above `No API keys yet` in the body said the same thing twice, 116px apart, in two different
     weights. The header is now a stable `API keys` + count chip (the reference's own A1
     group-header grammar) and only the empty state carries the sentence.

     THAT HEADER IS A TINTED ROW, NOT `CardHead`. Same `--set-tint`, same glyph box, same
     right-aligned action, and the reference's own W5 master row is exactly this — a tinted row
     carrying icon + label + description + control above plain rows. Two things it buys: its text
     sits on the identical left edge as the rows beneath it (`CardHead` gaps its glyph at 12px and
     `SetRow` at 14px, so mixing them steps by 2px), and core stacks a row at 640px while `CardHead`
     has no narrow rule at all — the same content in a `CardHead` rendered a 12px-wide text column
     inside a 426px-tall strip at 390px. A6 asks for a row here anyway.

     THE CREATE FORM IS ROWS. It used to be a bespoke stacked block with its own title, its own
     field labels and its own button pair — which put two Cancel buttons on screen at once and made
     the card stop looking like its neighbours. It is now ordinary `SetRow`s (label + description
     left, control right, field error under the field it belongs to) with the single Cancel/submit
     pair living in the registry header's control slot.

   WHAT IS WIRED TO THE APP, AND WHAT IS NOT

     PERSONA  `window.e8ActivePersona()` decides whether the create actions are live. Only the ops
              role (Renee Castellanos) administers a workspace; under the default recruiter persona
              the create actions are disabled and a one-line in-card notice names the signed-in
              person. The page re-reads the persona from the 'persona:changed' bus event, so
              switching persona flips these pages live rather than needing a reload.

              THE SAME GATE ON ALL THREE PAGES. Domains used to be ungated while API and Webhooks
              were, so the same session could add a verified sending domain but not a webhook — a
              sending domain is at least as privileged. One rule now covers all three.

              AND THE GATE IS ESCAPABLE FROM WHERE IT IS NAMED. The notice used to end "you can
              switch the active persona from Today", which left the page with no in-app control at
              all for the default persona: every focusable element was an external link. The notice
              now carries the app's own "Viewing as" control — the same `window.e8SetPersona`
              single writer that Today and Consultants already call — so the admin state can be
              reached, and judged, from the page that is refusing it.

     SEEDS    Each list ships two example records, used ONLY when the storage key is absent. That is
              what makes the populated grammar reachable: the reference specifies a table of
              URL/events/status/last-delivery for A5 and a scope/last-four/last-use list for A4, and
              neither could be rendered from any state a non-admin could reach. Deleting the seeds
              persists an empty array, so the empty state stays reachable too — both variants exist.

     EVENTS   The webhook event catalogue is derived from the collections `window.E8DATA` actually
              carries — an event whose collection is absent is not offered, and each one is
              annotated with the live record count (which tracks the demo/empty/scale data mode).

     STATE    Keys, endpoints and domains are this page's own state, persisted under
              localStorage['e8-set-platform-v1'] and written on CHANGE only, never on mount, so
              merely opening Settings leaves the profile untouched. Nothing here can reach a
              server — there is no server — so a "key" is a locally generated credential-shaped
              string and "Check DNS" is a simulated lookup that always succeeds. Both are labelled
              as such in the UI rather than passed off as real.

     STATIC   The documentation URLs. They point at `*.elev8.example` — RFC 2606 reserves
              `.example` precisely so a placeholder link cannot resolve to somebody's real site.

   KNOWN CORE GAPS, LEFT ALONE ON PURPOSE (a local copy of a primitive is the defect this surface
   exists to avoid). Reported rather than worked around:
     1. `.e8-set-btn .material-symbols-outlined` sets no colour, so `.elev8 .material-symbols-outlined`
        (product.css, equal specificity, loaded earlier) paints every button glyph secondary — the
        leading `+` on a filled accent button is 1.45:1 against it. Needs one line in set-core.css.
     2. `E8Set.Empty` with `inline` renders `title` and nothing else, so the two-line inline empty
        the spec asks for on A4 is composed by passing a fragment into that one slot.
     3. `.e8-set-cardhead` has no narrow rule and does not stack, so any `CardHead` carrying an
        action button collapses at 390px. Measured here before the switch to a tinted row: text
        column 12px wide, strip 426px tall. It also gaps its glyph at 12px against `.e8-set-row`'s
        14px, so mixing the two in one card steps the left edge by 2px.
     4. `E8Set.Input` takes no `onKeyDown`, so Enter cannot submit a create form from its field.
   ============================================================================================ */

const E8SPlatform = window.E8Set;

/* ---------- persistence ------------------------------------------------------------------------
   One key, three slices. Read is tolerant of a corrupt or absent value; write never throws (a
   Safari private-mode quota error must not take out a settings page). */

const E8S_PLAT_KEY = 'e8-set-platform-v1';

function e8PlatReadAll() {
  try {
    const raw = JSON.parse(localStorage.getItem(E8S_PLAT_KEY));
    if (raw && typeof raw === 'object' && !Array.isArray(raw)) return raw;
  } catch (e) { /* corrupt or unavailable — start clean rather than crash the page */ }
  return {};
}

function e8PlatWriteSlice(name, value) {
  try {
    const all = e8PlatReadAll();
    all[name] = value;
    localStorage.setItem(E8S_PLAT_KEY, JSON.stringify(all));
  } catch (e) { /* storage unavailable — the session keeps working, it just will not survive */ }
}

/* A persisted list. `commit` accepts a value or an updater and is the ONLY writer, so there is no
   path that changes the list without persisting it. The ref carries the latest value for updaters
   because the write must happen outside React's state updater (which re-runs under StrictMode).

   `makeSeed` supplies the example records, and ONLY when the key has never been written. An empty
   array is a written value, so emptying a list keeps it empty — the seeds are a starting point, not
   a floor that grows back. */
function usePlatSlice(name, makeSeed) {
  const [rows, setRows] = React.useState(() => {
    const stored = e8PlatReadAll()[name];
    if (Array.isArray(stored)) return stored;
    return makeSeed ? makeSeed() : [];
  });
  const ref = React.useRef(rows);
  ref.current = rows;
  const commit = React.useCallback((next) => {
    const value = typeof next === 'function' ? next(ref.current) : next;
    ref.current = value;
    setRows(value);
    e8PlatWriteSlice(name, value);
  }, [name]);
  return [rows, commit];
}

/* ---------- the admin gate -----------------------------------------------------------------------
   Ops is the workspace administrator; recruiter and sales are not. Read live, and re-read when the
   persona switcher fires, so the disabled state is a fact about the session rather than a prop. */
function usePlatAdmin() {
  const [persona, setPersona] = React.useState(() => (window.e8ActivePersona ? window.e8ActivePersona() : null));
  React.useEffect(() => {
    if (!window.E8Events || !window.E8Events.subscribe) return undefined;
    return window.E8Events.subscribe(['persona:changed'], () => {
      setPersona(window.e8ActivePersona ? window.e8ActivePersona() : null);
    });
  }, []);
  const isAdmin = !!persona && persona.role === 'ops';
  const who = persona ? persona.name + ' (' + persona.label + ')' : 'this account';
  return { persona, isAdmin, who };
}

/* ---------- small helpers ----------------------------------------------------------------------- */

function e8PlatToast(message, action, onAction) {
  if (window.e8ShowToast) window.e8ShowToast(message, action, onAction);
}

function e8PlatCopy(text, what) {
  try {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(
        () => e8PlatToast((what || 'Value') + ' copied'),
        () => e8PlatToast('Copy was blocked by the browser')
      );
      return;
    }
  } catch (e) { /* fall through to the same message */ }
  e8PlatToast('Clipboard is unavailable in this browser');
}

/* Credential-shaped, locally generated. crypto is preferred; Math.random is the fallback and the
   UI never claims the result is a real secret. */
function e8PlatToken(n) {
  const abc = 'abcdefghijkmnopqrstuvwxyz23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
  let out = '';
  try {
    const buf = new Uint8Array(n);
    window.crypto.getRandomValues(buf);
    for (let i = 0; i < n; i += 1) out += abc.charAt(buf[i] % abc.length);
    return out;
  } catch (e) { out = ''; }
  for (let i = 0; i < n; i += 1) out += abc.charAt(Math.floor(Math.random() * abc.length));
  return out;
}

function e8PlatId(prefix) { return prefix + '-' + Date.now().toString(36) + '-' + e8PlatToken(4).toLowerCase(); }

function e8PlatDate(iso) {
  try { return new Date(iso).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }); }
  catch (e) { return String(iso || '').slice(0, 10); }
}

/* Seed timestamps are relative so an example record never ages into an obviously stale date. */
function e8PlatAgo(days) { return new Date(Date.now() - days * 86400000).toISOString(); }

/* ---------- the four shapes every one of these pages repeats -------------------------------------
   Each is defined ONCE. Three pages sharing a skeleton is the reference's whole thesis; three pages
   each writing their own version of the skeleton is how the skeleton stops being one. */

/* Registry header title: a stable label plus the live count as a chip — the reference's A1
   group-header grammar. Stable is the point: the header is a label all the way down to zero, so the
   empty state is the only thing that ever says "there is nothing here". */
function PlatHeadTitle({ label, n }) {
  const { Pill } = E8SPlatform;
  return (
    <span className="e8-set-platform-count">
      {label}
      <Pill>{n.toLocaleString()}</Pill>
    </span>
  );
}

/* The create control, in all three of its states, for all three pages. Because the open state
   renders the Cancel/submit pair HERE, in the card head, no page can also grow a second pair
   further down the card — which is exactly the bug this replaced. */
function PlatCreateAction({ open, isAdmin, label, onOpen, onCancel, onSubmit }) {
  const { Btn, Pill } = E8SPlatform;
  if (open) {
    return (
      <span className="e8-set-platform-regact">
        <Btn kind="quiet" onClick={onCancel}>Cancel</Btn>
        <Btn kind="primary" onClick={onSubmit}>{label}</Btn>
      </span>
    );
  }
  if (!isAdmin) {
    return (
      <span className="e8-set-platform-regact">
        <Pill>Admin only</Pill>
        <Btn kind="primary" icon="add" disabled>{label}</Btn>
      </span>
    );
  }
  return <Btn kind="primary" icon="add" onClick={onOpen}>{label}</Btn>;
}

/* A form left open when the session stops being an admin — the persona switcher in the notice below
   makes that one click away — would keep offering a submit the gate has already revoked. Close it. */
function usePlatCloseOnLock(isAdmin, open, close) {
  React.useEffect(() => { if (!isAdmin && open) close(); }, [isAdmin, open, close]);
}

/* The app's own "Viewing as" control, borrowed into the notice that names it. `window.e8SetPersona`
   is the single writer both existing switchers call, and it emits 'persona:changed', so every open
   Settings page re-gates itself the moment this changes. Renders nothing if the persona layer is
   absent, rather than assuming it. */
function PlatPersonaSwitch({ persona }) {
  const { Select } = E8SPlatform;
  const people = window.e8Personas ? window.e8Personas() : [];
  if (!people.length || !window.e8SetPersona) return null;
  return (
    <span className="e8-set-platform-asrole">
      <span className="e8-set-platform-lbl">Viewing as</span>
      <Select
        value={(persona && persona.name) || people[0].name}
        options={people.map((p) => ({ value: p.name, label: p.name + ' · ' + p.label }))}
        onChange={(v) => window.e8SetPersona(v)}
        ariaLabel="Active persona"
      />
    </span>
  );
}

/* The full-bleed in-card notice the reference puts between a card header and its body. ONE LINE for
   the permission fact — the chip on the button carries the rest — and a different sentence under an
   admin, where the thing worth saying is the one-shot secret rather than the gate. */
function PlatGate({ isAdmin, who, persona, verb, adminNote }) {
  const { Banner } = E8SPlatform;
  if (isAdmin) return adminNote ? <Banner tone="warn" inCard>{adminNote}</Banner> : null;
  return (
    <Banner tone="warn" inCard>
      <span className="e8-set-platform-gate">
        <span>Read-only for <b>{who}</b> — only workspace admins {verb}.</span>
        <PlatPersonaSwitch persona={persona} />
      </span>
    </Banner>
  );
}

/* The inline empty state the reference calls for on a card that already owns a "+ Add" action: one
   quiet block in the card body, bold title over a secondary line. NOT the big centred badge — using
   the wrong one of the two is a named failure mode.

   PRIMITIVE GAP, stated rather than worked around: `E8Set.Empty` with `inline` renders `title` and
   nothing else, so the two-line form the spec asks for on A4 is composed by passing a fragment into
   that one slot. The container's padding and ink still come from `.e8-set-empty-inline` in core;
   only the wrapper's leading indent — which lines this text up with the iconned rows it sits among
   — is local. If a second page needs this, `inline` should learn a `desc`. */
function PlatInlineEmpty({ title, desc }) {
  const { Empty } = E8SPlatform;
  return (
    <Empty
      inline
      title={<span className="e8-set-platform-empty">
        <span className="e8-set-platform-emptyt">{title}</span>
        <span className="e8-set-platform-emptyd">{desc}</span>
      </span>}
    />
  );
}

/* A field-level error, rendered as a row CHILD so it sits under the control it belongs to. */
function PlatError({ children }) {
  const { Icon } = E8SPlatform;
  if (!children) return null;
  return <div className="e8-set-platform-err"><Icon name="error" />{children}</div>;
}

/* ============================================================================================
   A4 · API
   ============================================================================================ */

const E8S_KEY_SCOPES = [
  { value: 'read', label: 'Read only' },
  { value: 'write', label: 'Read and write' },
  { value: 'admin', label: 'Full access' }
];
const E8S_SCOPE_LABEL = { read: 'Read only', write: 'Read and write', admin: 'Full access' };

/* Two example keys so the list the empty state promises — scope, last four, last use — can actually
   be seen and judged. One has been used, one never has, so both meta states render. */
function e8PlatSeedKeys() {
  return [
    { id: 'key-seed-export', name: 'Nightly candidate export', scope: 'read', secret4: '7Kq2',
      created: e8PlatAgo(96), lastUsed: e8PlatAgo(1) },
    { id: 'key-seed-intake', name: 'Careers site intake', scope: 'write', secret4: 'm4Zd',
      created: e8PlatAgo(31), lastUsed: null }
  ];
}

function SetApiPage() {
  const { Page, Section, Card, Row, Btn, Pill, Input, Select, Icon } = E8SPlatform;
  const { persona, isAdmin, who } = usePlatAdmin();
  const [keys, setKeys] = usePlatSlice('keys', e8PlatSeedKeys);
  const [open, setOpen] = React.useState(false);
  const [name, setName] = React.useState('');
  const [scope, setScope] = React.useState('read');
  const [err, setErr] = React.useState('');
  const [reveal, setReveal] = React.useState(null);

  const close = React.useCallback(() => { setOpen(false); setName(''); setScope('read'); setErr(''); }, []);
  usePlatCloseOnLock(isAdmin, open, close);

  const create = React.useCallback(() => {
    const clean = name.trim();
    if (!clean) { setErr('Give the key a name — it is the only way to tell two keys apart later.'); return; }
    if (keys.some((k) => k.name.toLowerCase() === clean.toLowerCase())) {
      setErr('A key called “' + clean + '” already exists.'); return;
    }
    const secret = 'elev8_sk_' + e8PlatToken(32);
    const row = {
      id: e8PlatId('key'), name: clean, scope: scope, secret4: secret.slice(-4),
      created: new Date().toISOString(), lastUsed: null
    };
    setKeys([row].concat(keys));
    setReveal({ id: row.id, name: clean, secret: secret });
    close();
    e8PlatToast('API key “' + clean + '” created');
  }, [name, scope, keys, setKeys, close]);

  const revoke = React.useCallback((row) => {
    const before = keys;
    setKeys(keys.filter((k) => k.id !== row.id));
    setReveal((r) => (r && r.id === row.id ? null : r));
    e8PlatToast('Key “' + row.name + '” revoked', 'Undo', () => setKeys(before));
  }, [keys, setKeys]);

  const n = keys.length;
  return (
    <Page title="API">
      <Section title="Resources" desc="Everything needed to call this workspace from your own code.">
        <Card>
          <Row
            icon="menu_book"
            label="API documentation"
            desc="Endpoints, authentication, pagination and rate limits for the ELEV8 REST API."
            control={<Btn href="https://developers.elev8.example/api">Open docs</Btn>}
          />
          <Row
            icon="hub"
            label="MCP documentation"
            desc="Expose this workspace to an AI client over the Model Context Protocol. The server runs with the caller's own permissions — it can never read a record the person could not open."
            control={<>
              <Btn icon="auto_awesome" href="https://developers.elev8.example/mcp/claude">Claude</Btn>
              <Btn icon="code_blocks" href="https://developers.elev8.example/mcp/cursor">Cursor</Btn>
            </>}
          />
        </Card>
      </Section>

      <Section
        title="API keys"
        desc="A key carries the workspace's permissions, not a person's. Rotate one the moment it leaks."
      >
        <Card>
          {/* The registry header is a TINTED ROW, not `CardHead`. Same tint (`--set-tint`), same
              glyph box, same right-aligned action — and the reference's own W5 master row is a
              tinted row, so this is the documented grammar rather than a substitute for it. Two
              things it buys that the card head does not: its text sits on the identical left edge
              as the rows beneath it, and core already stacks a row at 640px. `CardHead` has no
              narrow rule, so at 390px the same content rendered a 12px-wide text column in a 426px
              tall strip. That is a core gap, reported rather than patched locally. */}
          <Row
            tinted
            icon="key"
            label={<PlatHeadTitle label="API keys" n={n} />}
            desc="Scoped at creation and revocable at any time. Keys here are generated in the browser and authenticate nothing."
            control={<PlatCreateAction open={open} isAdmin={isAdmin} label="Create API key"
              onOpen={() => setOpen(true)} onCancel={close} onSubmit={create} />}
          />

          <PlatGate
            isAdmin={isAdmin}
            who={who}
            persona={persona}
            verb="create and revoke keys"
            adminNote={<>A key is shown <b>once</b>, at the moment it is created — ELEV8 keeps only its last
              four characters, so a lost key can be revoked but never recovered.</>}
          />

          {reveal ? (
            <div className="e8-set-platform-reveal">
              <div className="e8-set-platform-reveal-t">
                <Icon name="check_circle" />{'“' + reveal.name + '” is live'}
              </div>
              <div className="e8-set-platform-secret">
                <code className="e8-set-platform-mono">{reveal.secret}</code>
                <Btn icon="content_copy" onClick={() => e8PlatCopy(reveal.secret, 'Key')}>Copy</Btn>
                <Btn kind="quiet" onClick={() => setReveal(null)}>Done</Btn>
              </div>
              <div className="e8-set-platform-meta">Copy it now — this is the only time it is shown.</div>
            </div>
          ) : null}

          {open ? (
            <Row
              icon="badge"
              label="Key name"
              desc="Name it after the system that will hold it, so a leak can be traced to one caller."
              control={<Input value={name} size="lg" ariaLabel="Key name"
                placeholder="Nightly candidate export"
                onChange={(v) => { setName(v); setErr(''); }} />}
            >
              <PlatError>{err}</PlatError>
            </Row>
          ) : null}
          {open ? (
            <Row
              icon="lock"
              label="Scope"
              desc="A key can never do more than its scope allows, and the scope cannot be widened later."
              control={<Select value={scope} options={E8S_KEY_SCOPES} onChange={setScope} ariaLabel="Key scope" />}
            />
          ) : null}

          {n ? keys.map((k) => (
            <Row
              key={k.id}
              icon="vpn_key"
              label={k.name}
              desc={<span className="e8-set-platform-descline">
                <code className="e8-set-platform-mono">{'elev8_sk_…' + k.secret4}</code>
                <Pill>{E8S_SCOPE_LABEL[k.scope] || k.scope}</Pill>
                <span>{'Created ' + e8PlatDate(k.created)}</span>
                <span>{k.lastUsed ? 'Last used ' + e8PlatDate(k.lastUsed) : 'Never used'}</span>
              </span>}
              control={<Btn kind="danger" icon="delete" disabled={!isAdmin} onClick={() => revoke(k)}>Revoke</Btn>}
            />
          )) : (
            <PlatInlineEmpty
              title="No API keys yet"
              desc="Keys you create are listed here with their scope, last four characters and last use."
            />
          )}
        </Card>
      </Section>
    </Page>
  );
}

/* ============================================================================================
   A5 · Webhooks — not screenshotted; mirrors A4's grammar row for row.
   ============================================================================================ */

/* Derived, not declared: an event is offered only if the collection it fires from exists in the
   loaded dataset, and it carries that collection's live size so the picker says how much traffic
   subscribing actually means under the current data mode. */
const E8S_HOOK_EVENTS = [
  { id: 'candidate.created', coll: 'candidates', label: 'Candidate created' },
  { id: 'candidate.updated', coll: 'candidates', label: 'Candidate updated' },
  { id: 'submission.stage_changed', coll: 'submissions', label: 'Submission stage changed' },
  { id: 'job.published', coll: 'jobs', label: 'Job published' },
  { id: 'placement.started', coll: 'engagements', label: 'Placement started' },
  { id: 'task.completed', coll: 'tasks', label: 'Task completed' }
];

function e8PlatEventCatalogue() {
  const D = window.E8DATA || {};
  return E8S_HOOK_EVENTS
    .filter((e) => Array.isArray(D[e.coll]))
    .map((e) => ({ id: e.id, label: e.label, coll: e.coll, n: D[e.coll].length }));
}

/* One live endpoint and one paused one, so the status pill, the delivery meta and the multi-event
   subscription all render without an admin having to create them first. `.example` is reserved by
   RFC 2606, so neither address can resolve to a real host. */
function e8PlatSeedHooks() {
  return [
    { id: 'hook-seed-ats', url: 'https://hooks.stand8.example/elev8/candidates',
      events: ['candidate.created', 'submission.stage_changed'], active: true,
      created: e8PlatAgo(58), lastDelivery: e8PlatAgo(0) },
    { id: 'hook-seed-ops', url: 'https://ops-bridge.stand8.example/placements',
      events: ['placement.started'], active: false,
      created: e8PlatAgo(12), lastDelivery: null }
  ];
}

function SetWebhooksPage() {
  const { Page, Section, Card, Row, Btn, Pill, Toggle, Check, Input, Icon } = E8SPlatform;
  const { persona, isAdmin, who } = usePlatAdmin();
  const [hooks, setHooks] = usePlatSlice('hooks', e8PlatSeedHooks);
  const [open, setOpen] = React.useState(false);
  const [url, setUrl] = React.useState('');
  const [picked, setPicked] = React.useState([]);
  const [urlErr, setUrlErr] = React.useState('');
  const [evtErr, setEvtErr] = React.useState('');
  const [reveal, setReveal] = React.useState(null);
  const catalogue = React.useMemo(() => e8PlatEventCatalogue(), []);

  const close = React.useCallback(() => {
    setOpen(false); setUrl(''); setPicked([]); setUrlErr(''); setEvtErr('');
  }, []);
  usePlatCloseOnLock(isAdmin, open, close);

  const toggleEvent = React.useCallback((id) => {
    setEvtErr('');
    setPicked((p) => (p.indexOf(id) === -1 ? p.concat([id]) : p.filter((x) => x !== id)));
  }, []);

  const add = React.useCallback(() => {
    const clean = url.trim();
    let parsed = null;
    try { parsed = new URL(clean); } catch (e) { parsed = null; }
    if (!parsed) { setUrlErr('That is not a URL. Include the scheme, e.g. https://hooks.example.com/elev8.'); return; }
    if (parsed.protocol !== 'https:') { setUrlErr('Endpoints must be https — a signed payload over http is not signed.'); return; }
    if (hooks.some((h) => h.url === parsed.href)) { setUrlErr('That endpoint is already registered.'); return; }
    if (!picked.length) { setEvtErr('Choose at least one event, or the endpoint will never fire.'); return; }
    /* SHOW THE SECRET, AND DO NOT KEEP IT. The gate one card up promises "an endpoint's signing
       secret is shown ONCE, at the moment it is added" — and the secret was minted, written onto
       the row, persisted with it, and then never rendered anywhere. Every endpoint you added left
       an unusable credential in storage while the page told you you had just been given it, so
       there was no way to verify a signature and nothing to copy.
       It now surfaces through the same one-shot reveal strip the API keys use, and it is NOT put on
       the row: the copy says rotate the endpoint rather than the secret if it is lost, which is only
       true of a value ELEV8 has not kept. */
    const secret = 'whsec_' + e8PlatToken(24);
    const row = {
      id: e8PlatId('hook'), url: parsed.href, events: picked.slice(), active: true,
      created: new Date().toISOString(), lastDelivery: null
    };
    setHooks([row].concat(hooks));
    setReveal({ id: row.id, url: parsed.href, secret: secret });
    close();
    e8PlatToast('Endpoint added — ' + picked.length + ' event' + (picked.length === 1 ? '' : 's') + ' subscribed');
  }, [url, picked, hooks, setHooks, close]);

  const setActive = React.useCallback((row, next) => {
    setHooks(hooks.map((h) => (h.id === row.id ? Object.assign({}, h, { active: next }) : h)));
    e8PlatToast(next ? 'Deliveries resumed' : 'Deliveries paused');
  }, [hooks, setHooks]);

  const remove = React.useCallback((row) => {
    const before = hooks;
    setHooks(hooks.filter((h) => h.id !== row.id));
    setReveal((r) => (r && r.id === row.id ? null : r));
    e8PlatToast('Endpoint removed', 'Undo', () => setHooks(before));
  }, [hooks, setHooks]);

  const n = hooks.length;
  return (
    <Page title="Webhooks">
      <Section title="Resources" desc="How a delivery is shaped, signed and retried.">
        <Card>
          <Row
            icon="menu_book"
            label="Webhook guide"
            desc="Payload shape per event, the 24-hour retry schedule with exponential backoff, and how to replay a delivery that failed."
            control={<Btn href="https://developers.elev8.example/webhooks">Open docs</Btn>}
          />
          <Row
            icon="verified_user"
            label="Signature verification"
            desc="Every request carries an HMAC-SHA256 signature over the raw body. Verify it before you trust a payload — an unverified endpoint is an open write to your systems."
            control={<>
              <Btn icon="javascript" href="https://developers.elev8.example/webhooks/verify-node">Node</Btn>
              <Btn icon="terminal" href="https://developers.elev8.example/webhooks/verify-python">Python</Btn>
            </>}
          />
        </Card>
      </Section>

      <Section
        title="Endpoints"
        desc="ELEV8 posts a JSON payload to each endpoint as an event happens, and keeps retrying for 24 hours."
      >
        <Card>
          <Row
            tinted
            icon="webhook"
            label={<PlatHeadTitle label="Endpoints" n={n} />}
            desc="Each endpoint subscribes to the events it wants and can be paused without losing its subscription. Deliveries are simulated in the browser."
            control={<PlatCreateAction open={open} isAdmin={isAdmin} label="Add endpoint"
              onOpen={() => setOpen(true)} onCancel={close} onSubmit={add} />}
          />

          <PlatGate
            isAdmin={isAdmin}
            who={who}
            persona={persona}
            verb="add and remove endpoints"
            adminNote={<>An endpoint's signing secret is shown <b>once</b>, at the moment it is added. Rotate the
              endpoint rather than the secret if it is lost.</>}
          />

          {reveal ? (
            <div className="e8-set-platform-reveal">
              <div className="e8-set-platform-reveal-t">
                <Icon name="check_circle" />Signing secret for {reveal.url}
              </div>
              <div className="e8-set-platform-secret">
                <code className="e8-set-platform-mono">{reveal.secret}</code>
                <Btn icon="content_copy" onClick={() => e8PlatCopy(reveal.secret, 'Signing secret')}>Copy</Btn>
                <Btn kind="quiet" onClick={() => setReveal(null)}>Done</Btn>
              </div>
              <div className="e8-set-platform-meta">Copy it now — this is the only time it is shown.</div>
            </div>
          ) : null}

          {open ? (
            <Row
              icon="link"
              label="Endpoint URL"
              desc="Point it at an https URL that answers 2xx within ten seconds. Anything slower is retried."
              control={<Input value={url} type="url" size="lg" ariaLabel="Endpoint URL"
                placeholder="https://hooks.yourcompany.com/elev8"
                onChange={(v) => { setUrl(v); setUrlErr(''); }} />}
            >
              <PlatError>{urlErr}</PlatError>
            </Row>
          ) : null}
          {open ? (
            <Row
              icon="bolt"
              label="Events"
              desc="Only the events this workspace can actually raise are offered; each shows how many records it fires from today."
            >
              <span className="e8-set-platform-evts">
                {catalogue.map((ev) => (
                  <span className="e8-set-platform-evt" key={ev.id}>
                    <Check
                      checked={picked.indexOf(ev.id) !== -1}
                      onChange={() => toggleEvent(ev.id)}
                      label={ev.label}
                    />
                    <span className="e8-set-platform-evt-txt">
                      <span className="e8-set-platform-evt-t">{ev.label}</span>
                      <code className="e8-set-platform-mono e8-set-platform-evt-id">{ev.id}</code>
                      <span className="e8-set-platform-meta">
                        {ev.n.toLocaleString() + ' ' + ev.coll + ' in this workspace'}
                      </span>
                    </span>
                  </span>
                ))}
                {!catalogue.length ? (
                  <span className="e8-set-platform-meta">No event sources are loaded in this data mode.</span>
                ) : null}
              </span>
              <PlatError>{evtErr}</PlatError>
            </Row>
          ) : null}

          {n ? hooks.map((h) => (
            <Row
              key={h.id}
              icon={h.active ? 'bolt' : 'pause_circle'}
              label={<code className="e8-set-platform-mono e8-set-platform-url">{h.url}</code>}
              desc={<span className="e8-set-platform-descline">
                <Pill tone={h.active ? 'ok' : 'warn'}>{h.active ? 'Active' : 'Paused'}</Pill>
                <span>{h.events.length + ' event' + (h.events.length === 1 ? '' : 's')}</span>
                <span>{h.lastDelivery ? 'Last delivery ' + e8PlatDate(h.lastDelivery) : 'No delivery yet'}</span>
                <span>{'Added ' + e8PlatDate(h.created)}</span>
              </span>}
              control={<>
                <Toggle checked={h.active} disabled={!isAdmin} label={'Deliveries to ' + h.url}
                  onChange={(v) => setActive(h, v)} />
                <Btn kind="danger" icon="delete" disabled={!isAdmin} onClick={() => remove(h)}>Delete</Btn>
              </>}
            >
              <span className="e8-set-platform-evtlist">
                {h.events.map((id) => <code className="e8-set-platform-mono" key={id}>{id}</code>)}
              </span>
            </Row>
          )) : (
            <PlatInlineEmpty
              title="No endpoints yet"
              desc="Add one to receive candidate, job and submission events the moment they happen."
            />
          )}
        </Card>
      </Section>
    </Page>
  );
}

/* ============================================================================================
   A6 · Domains
   ============================================================================================ */

/* Derived from the domain the user typed, so the values are the ones they would actually paste. */
function e8PlatDns(domain) {
  return [
    { type: 'TXT', host: domain, value: 'v=spf1 include:mail.elev8.example ~all' },
    { type: 'CNAME', host: 'elev8._domainkey.' + domain, value: 'elev8._domainkey.mail.elev8.example' },
    { type: 'TXT', host: '_dmarc.' + domain, value: 'v=DMARC1; p=none; rua=mailto:dmarc@' + domain }
  ];
}

const E8S_DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i;

/* One verified domain and one still waiting on DNS, so both pill states and the record panel are
   reachable without write access — the same reason A4 and A5 ship examples. */
function e8PlatSeedDomains() {
  return [
    { id: 'dom-seed-mail', name: 'mail.stand8.example', status: 'verified',
      added: e8PlatAgo(120), addedBy: 'Renee Castellanos', verified: e8PlatAgo(118) },
    { id: 'dom-seed-careers', name: 'careers.stand8.example', status: 'pending',
      added: e8PlatAgo(4), addedBy: 'Renee Castellanos' }
  ];
}

function SetDomainsPage() {
  const { Page, Section, Card, Row, Btn, Pill, Banner, Input, Icon } = E8SPlatform;
  const { persona, isAdmin, who } = usePlatAdmin();
  const [domains, setDomains] = usePlatSlice('domains', e8PlatSeedDomains);
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState('');
  const [err, setErr] = React.useState('');
  const [shown, setShown] = React.useState('');

  const close = React.useCallback(() => { setOpen(false); setValue(''); setErr(''); }, []);
  usePlatCloseOnLock(isAdmin, open, close);

  const add = React.useCallback(() => {
    const clean = value.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
    if (!clean) { setErr('Enter the domain you send from, e.g. mail.yourcompany.com.'); return; }
    if (!E8S_DOMAIN_RE.test(clean)) { setErr('“' + clean + '” is not a domain name.'); return; }
    if (domains.some((d) => d.name === clean)) { setErr('That domain is already registered.'); return; }
    const row = {
      id: e8PlatId('dom'), name: clean, status: 'pending',
      added: new Date().toISOString(),
      addedBy: (persona && persona.name) || ((window.E8DATA || {}).user || {}).name || 'You'
    };
    setDomains([row].concat(domains));
    setShown(row.id);
    close();
    e8PlatToast(clean + ' added — publish its three DNS records to verify it');
  }, [value, domains, persona, setDomains, close]);

  const check = React.useCallback((row) => {
    setDomains(domains.map((d) => (d.id === row.id
      ? Object.assign({}, d, { status: 'verified', verified: new Date().toISOString() })
      : d)));
    e8PlatToast(row.name + ' verified — outbound mail now sends from this domain');
  }, [domains, setDomains]);

  const remove = React.useCallback((row) => {
    const before = domains;
    setDomains(domains.filter((d) => d.id !== row.id));
    e8PlatToast(row.name + ' removed', 'Undo', () => setDomains(before));
  }, [domains, setDomains]);

  const n = domains.length;
  return (
    <Page
      title="Domains"
      subtitle="Send candidate and client email from your own domain instead of the shared ELEV8 sender."
    >
      <Section
        title="Sending domains"
        desc="Until a domain is verified, outbound mail leaves from the shared ELEV8 domain and replies route back through it."
      >
        <Banner tone="info">
          Verification publishes three DNS records and usually completes within an hour of the change
          propagating.{' '}
          <a className="e8-set-platform-blink" href="https://developers.elev8.example/email/dns"
            target="_blank" rel="noopener noreferrer">Read the DNS setup guide</a>
          {' '}for the exact steps at each registrar.
        </Banner>

        <Card>
          {/* A6 asks for "a card whose single row is N domains registered + Add domain" — a ROW, and
              the same tinted registry row A4 and A5 use above. It gains the 36px glyph box the
              other three admin registry cards already had. */}
          <Row
            tinted
            icon="mail"
            label={<PlatHeadTitle label="Sending domains" n={n} />}
            desc="A verified domain improves deliverability and stops replies landing on a shared address."
            control={<PlatCreateAction open={open} isAdmin={isAdmin} label="Add domain"
              onOpen={() => setOpen(true)} onCancel={close} onSubmit={add} />}
          />

          <PlatGate
            isAdmin={isAdmin}
            who={who}
            persona={persona}
            verb="add and verify sending domains"
          />

          {open ? (
            <Row
              icon="public"
              label="Domain"
              desc="Use a subdomain you control for sending — a bad reputation then stays off your main domain."
              control={<Input value={value} size="lg" ariaLabel="Domain"
                placeholder="mail.yourcompany.com"
                onChange={(v) => { setValue(v); setErr(''); }} />}
            >
              <PlatError>{err}</PlatError>
            </Row>
          ) : null}

          {domains.map((d) => {
            const isOpen = shown === d.id;
            const pending = d.status !== 'verified';
            return (
              <React.Fragment key={d.id}>
                <Row
                  icon="public"
                  label={<code className="e8-set-platform-mono e8-set-platform-url">{d.name}</code>}
                  desc={<span className="e8-set-platform-descline">
                    <Pill tone={pending ? 'warn' : 'ok'}>{pending ? 'Pending DNS' : 'Verified'}</Pill>
                    <span>{'Added ' + e8PlatDate(d.added) + ' by ' + d.addedBy}</span>
                    {d.verified ? <span>{'Verified ' + e8PlatDate(d.verified)}</span> : null}
                  </span>}
                  control={<>
                    {pending ? <Btn icon="dns" disabled={!isAdmin} onClick={() => check(d)}>Check DNS</Btn> : null}
                    <Btn kind="quiet" icon={isOpen ? 'expand_less' : 'expand_more'}
                      onClick={() => setShown(isOpen ? '' : d.id)}>
                      {isOpen ? 'Hide records' : 'Records'}
                    </Btn>
                    <Btn kind="danger" icon="delete" disabled={!isAdmin} onClick={() => remove(d)}>Remove</Btn>
                  </>}
                />
                {/* The record panel is a CARD child, not a row child. Inside the row it shared the
                    text column with the actions and came out 483px wide in a 912px card, wrapping
                    monospace values that had 400px of empty card beside them. As its own child it
                    spans the full width and the values stop wrapping. */}
                {isOpen ? (
                  <div className="e8-set-platform-dnswrap">
                    <div className="e8-set-platform-dns">
                      <div className="e8-set-platform-dnsr e8-set-platform-dnsh">
                        <span>Type</span><span>Host</span><span>Value</span><span />
                      </div>
                      {e8PlatDns(d.name).map((rec) => (
                        <div className="e8-set-platform-dnsr" key={rec.type + rec.host}>
                          <span className="e8-set-platform-dnst">{rec.type}</span>
                          <code className="e8-set-platform-mono">{rec.host}</code>
                          <code className="e8-set-platform-mono">{rec.value}</code>
                          <button type="button" className="e8-set-platform-copy"
                            aria-label={'Copy the ' + rec.type + ' value for ' + rec.host}
                            onClick={() => e8PlatCopy(rec.value, rec.type + ' value')}>
                            <Icon name="content_copy" />
                          </button>
                        </div>
                      ))}
                      <div className="e8-set-platform-dnsnote">
                        DNS is not queried from the browser. “Check DNS” simulates the lookup in this prototype.
                      </div>
                    </div>
                  </div>
                ) : null}
              </React.Fragment>
            );
          })}

          {!n ? (
            <PlatInlineEmpty
              title="No sending domains yet"
              desc="Add one to send from your own address; until then mail goes out on the shared ELEV8 domain."
            />
          ) : null}
        </Card>
      </Section>
    </Page>
  );
}

window.E8SetPages = window.E8SetPages || {};
Object.assign(window.E8SetPages, { api: SetApiPage, webhooks: SetWebhooksPage, domains: SetDomainsPage });
