/* app/set-brand.jsx — Settings pages: Workspace General (identity + branding).
   ============================================================================================
   OWNERSHIP: this file and app/set-brand.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-brand.css under a `.e8-set-brand-*` 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-*`.

   WHAT IS NEW HERE, AND WHY IT IS LOCAL
   The reference gives W1 one shape no other page has: the COLOUR SWATCH CHIP — "a bordered pill
   containing a small rounded colour square and the hex in mono-ish text". Three of them, and
   nothing else in the 20 pages needs one, so it lives in `.e8-set-brand-swatch` rather than in the
   shared layer. Everything else on this page is a core primitive: the public-slug field is
   `E8Set.Input` with its `prefix` prop (already the reference's "immutable prefix in secondary ink
   inside the field"), not a hand-rolled input.

   WHERE THE COLOURS COME FROM
   Seeded from the LIVE token layer, not from literals — `--ui-accent-base` is the workspace accent
   (cobalt by default, and it follows whatever accent the user picked in Appearance). Reading them
   through the CSSOM also means no hex ever appears in this file, which is both the lint rule and
   the reason dark mode works. `e8BrandHex` normalises whatever the token holds — a raw hex in light
   mode, a resolved `color-mix()` in dark — down to the `#rrggbb` an <input type="color"> accepts.
   ============================================================================================ */

const E8SBrand = window.E8Set;

/* The public job board's host. The dataset carries no product domain (grep for `https://` in app/
   returns only third-party links), so this one string is invented; everything else on the page is
   read from the app. It is the immutable half of the prefixed input. */
const E8_BRAND_HOST = 'elev8.jobs/';

/* Each brand colour is seeded from a real ELEV8 token. `fallback` is a CSS *named* colour in the
   same neighbourhood, used only if the token resolves to nothing — a literal hex here would fail
   check-hardcoded-colors, and an empty value would break the colour input. */
const E8_BRAND_COLORS = [
  { key: 'primary', label: 'Primary', token: '--ui-accent-base', fallback: 'slateblue',
    desc: 'Buttons, links and the header band on your public job board.' },
  { key: 'secondary', label: 'Secondary', token: '--ui-pm-glyph', fallback: 'midnightblue',
    desc: 'Headings and body copy on public pages and application forms.' },
  { key: 'accent', label: 'Accent', token: '--ui-daybreak', fallback: 'lightsteelblue',
    desc: 'Highlights, tags and the progress bar candidates see while applying.' },
];

const E8_BRAND_KEY = 'e8-set-general-v1';
const E8_BRAND_CTA_MAX = 2;

/* ---------- colour plumbing ------------------------------------------------------------------
   Normalise any CSS colour to `#rrggbb`. Three cheap string cases first, then the browser itself
   for anything computed — in dark mode `--ui-daybreak` is a `color-mix()` and only the CSSOM can
   say what it evaluates to. Returns '' rather than guessing when the value is not a colour. */
function e8BrandHex(raw) {
  const v = String(raw || '').trim();
  if (!v) return '';
  const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(v);
  if (short) return ('#' + short[1] + short[1] + short[2] + short[2] + short[3] + short[3]).toUpperCase();
  if (/^#[0-9a-f]{6}$/i.test(v)) return v.toUpperCase();
  if (/^#[0-9a-f]{8}$/i.test(v)) return v.slice(0, 7).toUpperCase();
  try {
    const host = document.body || document.documentElement;
    if (!host) return '';
    const probe = document.createElement('span');
    probe.className = 'e8-set-brand-probe';
    probe.setAttribute('aria-hidden', 'true');
    probe.style.color = v;
    if (!probe.style.color) return '';          /* CSSOM rejected it: not a colour */
    host.appendChild(probe);
    const computed = String(window.getComputedStyle(probe).color || '');
    host.removeChild(probe);
    /* Two serialisations to cope with. `rgb()/rgba()` is the classic one, channels 0-255.
       A computed `color-mix(in srgb, …)` — which is what the dark theme holds these tokens as —
       serialises in current Chrome as `color(srgb r g b)` with channels 0-1, and a parser that
       only knew rgb() would silently fall through to the named-colour fallback in dark mode. */
    const srgb = /^color\(srgb\s+([^)]+)\)/.exec(computed);
    const rgb = /rgba?\(([^)]+)\)/.exec(computed);
    const m = srgb || rgb;
    if (!m) return '';
    const scale = srgb ? 255 : 1;
    const parts = m[1].split(/[,\s/]+/).filter(Boolean).map(Number).slice(0, 3);
    if (parts.length < 3 || parts.some((n) => !isFinite(n))) return '';
    return '#' + parts
      .map((n) => Math.max(0, Math.min(255, Math.round(n * scale))).toString(16).padStart(2, '0').toUpperCase())
      .join('');
  } catch (e) { return ''; }
}

function e8BrandSeed(spec) {
  let raw = '';
  try { raw = window.getComputedStyle(document.documentElement).getPropertyValue(spec.token); } catch (e) { raw = ''; }
  return e8BrandHex(raw) || e8BrandHex(spec.fallback);
}

function e8BrandDefaultColors() {
  const out = {};
  E8_BRAND_COLORS.forEach((c) => { out[c.key] = e8BrandSeed(c); });
  return out;
}

/* ---------- workspace facts ------------------------------------------------------------------
   The dataset names the tenant in exactly one place: the persona's `org` string,
   'Stand8 delivery - Memphis pod'. Drop the pod after ' - ', then take the leading word — the
   agency, not the department. ELEV8 is the ATS; Stand8 is the workspace using it. */
function e8BrandOrgName() {
  const org = String(((window.E8DATA || {}).user || {}).org || '').trim();
  const head = (org.split(' - ')[0] || '').trim();
  return head.split(/\s+/)[0] || 'ELEV8';
}

/* Loose while typing: lowercase, collapse anything illegal to a hyphen, cap the length. Trimming
   the ends here would make a mid-word hyphen impossible to type, so `e8BrandSlugTrim` does that
   for the published URL only. */
function e8BrandSlugType(s) {
  return String(s || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-{2,}/g, '-').slice(0, 32);
}
function e8BrandSlugTrim(s) { return e8BrandSlugType(s).replace(/^-+|-+$/g, ''); }

/* Real counts for the open-applications row. `source` is 'Careers site' on the rows that came in
   through the public board, which is exactly what this toggle governs. */
function e8BrandReach() {
  const D = window.E8DATA || {};
  const apps = Array.isArray(D.applications) ? D.applications : [];
  return {
    jobs: Array.isArray(D.jobs) ? D.jobs.length : 0,
    apps: apps.length,
    site: apps.filter((a) => /careers site/i.test(String((a && a.source) || ''))).length,
  };
}

function e8BrandWho() {
  const p = window.e8ActivePersona ? window.e8ActivePersona() : null;
  return (p && p.name) || ((window.E8DATA || {}).user || {}).name || 'You';
}

/* The shell's toast takes (message, action, onAction) and appends the platform undo chord itself —
   app/main.jsx renders `action + ' · ' + UNDO_CHORD`, so passing 'Undo' here is what produces the
   "Undo · ⌘Z" affordance the rest of the surface shows. Never spell the chord out at a call site:
   it is ⌘Z on a Mac and Ctrl+Z everywhere else, and only main.jsx knows which. */
function e8BrandToast(msg, action, onAction) {
  if (window.e8ShowToast) window.e8ShowToast(msg, action, onAction);
}

/* ---------- persistence -----------------------------------------------------------------------
   One localStorage record, written whole on every change. Uploaded images are deliberately NOT in
   it: a data URL for a logo runs to hundreds of kilobytes and would sit one upload away from
   blowing the origin's quota, which fails the OTHER keys' writes, not just this one. */
function e8BrandRead() {
  try {
    const raw = window.localStorage.getItem(E8_BRAND_KEY);
    const parsed = raw ? JSON.parse(raw) : null;
    return parsed && typeof parsed === 'object' ? parsed : null;
  } catch (e) { return null; }
}
function e8BrandWrite(state) {
  try { window.localStorage.setItem(E8_BRAND_KEY, JSON.stringify(state)); } catch (e) {}
}

function e8BrandInit() {
  const saved = e8BrandRead() || {};
  const name = typeof saved.name === 'string' ? saved.name : e8BrandOrgName();
  const seeded = e8BrandDefaultColors();
  const colors = {};
  E8_BRAND_COLORS.forEach((c) => {
    const held = saved.colors && typeof saved.colors[c.key] === 'string' ? e8BrandHex(saved.colors[c.key]) : '';
    colors[c.key] = held || seeded[c.key];
  });
  return {
    name,
    slug: typeof saved.slug === 'string' ? saved.slug : e8BrandSlugType(name),
    openApps: typeof saved.openApps === 'boolean' ? saved.openApps : true,
    colors,
    ctas: Array.isArray(saved.ctas) ? saved.ctas.slice(0, E8_BRAND_CTA_MAX) : [],
    by: typeof saved.by === 'string' ? saved.by : '',
  };
}

/* ---------- page-local shapes ------------------------------------------------------------------
   The swatch chip: bordered pill + rounded colour square + the hex in mono. The interactive
   element is a real <input type="color"> laid over the whole chip, so it is in the tab order and
   opens the platform picker on Enter; the chip takes the focus ring via :focus-within. */
function E8BrandSwatch({ label, value, onChange }) {
  const hex = String(value || '').toUpperCase();
  if (!hex) {
    return <span className="e8-set-brand-swatch is-unset"><span className="e8-set-brand-swatch-hex">Unavailable</span></span>;
  }
  return (
    <span className="e8-set-brand-swatch">
      <span className="e8-set-brand-swatch-sq" style={{ background: hex }} aria-hidden="true" />
      <span className="e8-set-brand-swatch-hex">{hex}</span>
      <input type="color" className="e8-set-brand-swatch-in" value={hex}
        aria-label={label + ' brand colour, currently ' + hex}
        onChange={(e) => onChange && onChange(String(e.target.value || '').toUpperCase())} />
    </span>
  );
}

/* Logo / favicon: the reference's "image placeholder + pencil". The placeholder carries the
   workspace monogram over a 10% wash of the primary brand colour, so the tile stays readable in
   both themes while still answering to the swatch above it. The pencil is a <label> around a real
   file input — hidden visually, never from the keyboard. */
function E8BrandMedia({ kind, src, mono, word, tone, onPick, onClear, label, accept }) {
  const Icon = E8SBrand.Icon;
  const Btn = E8SBrand.Btn;
  return (
    <span className="e8-set-brand-media">
      <span className={'e8-set-brand-thumb is-' + kind + (src ? ' has-img' : '')}
        style={{ '--e8-set-brand-c': tone || '', '--e8-set-brand-img': src ? 'url("' + src + '")' : 'none' }}>
        {src ? null : <span className="e8-set-brand-mono">{mono}</span>}
        {src || kind !== 'logo' ? null : <span className="e8-set-brand-word">{word}</span>}
      </span>
      <label className="e8-set-brand-pick">
        <Icon name="edit" />
        <input type="file" accept={accept} aria-label={label}
          onChange={(e) => {
            const file = e.target.files && e.target.files[0];
            e.target.value = '';
            if (file && onPick) onPick(file);
          }} />
      </label>
      {src ? <Btn kind="quiet" onClick={onClear}>Remove</Btn> : null}
    </span>
  );
}

/* ---------- W1 · General ------------------------------------------------------------------- */
function SetGeneralPage() {
  const { Page, Section, Card, Row, Input, Toggle, Btn } = E8SBrand;
  const [st, setSt] = React.useState(e8BrandInit);
  const [logo, setLogo] = React.useState('');
  const [favicon, setFavicon] = React.useState('');
  /* Only the NAME row shows a "last changed by" line, so this flag tracks that field alone — a
     colour reset setting it would put an unrelated attribution under the workspace name. */
  const [renamed, setRenamed] = React.useState(false);
  const reach = React.useMemo(e8BrandReach, []);

  const patch = React.useCallback((fields) => {
    setSt((prev) => {
      const next = Object.assign({}, prev, fields, { by: e8BrandWho() });
      e8BrandWrite(next);
      return next;
    });
  }, []);

  const pickImage = React.useCallback((file, apply) => {
    try {
      const reader = new FileReader();
      reader.onload = () => apply(String(reader.result || ''));
      reader.onerror = () => e8BrandToast('That image could not be read.');
      reader.readAsDataURL(file);
    } catch (e) { e8BrandToast('That image could not be read.'); }
  }, []);

  /* The native picker fires onChange continuously while the user drags, so a no-op guard keeps
     that from becoming a localStorage write per frame. */
  const setColor = (key, hex) => {
    if (!hex || st.colors[key] === hex) return;
    patch({ colors: Object.assign({}, st.colors, { [key]: hex }) });
  };
  /* Reset is the most destructive control on the page — it overwrites all three brand colours at
     once — so it is the one that most needs the Undo the connect/disconnect toasts already carry.
     `before` is the colours object from this render; `patch` always writes a NEW object, so the
     captured one stays intact and the undo hands it straight back.
     A reset that changes nothing gets no Undo: an action button that would be a no-op is worse
     than none, because it teaches people the affordance is decorative. */
  const resetColors = () => {
    const before = st.colors;
    const after = e8BrandDefaultColors();
    if (!E8_BRAND_COLORS.some((c) => before[c.key] !== after[c.key])) {
      /* Same no-op guard as setColor above, for the same reason: no state churn and no
         localStorage write for a reset that resets nothing. */
      e8BrandToast('Brand colours already match the ELEV8 palette');
      return;
    }
    patch({ colors: after });
    e8BrandToast('Brand colours reset to the ELEV8 palette', 'Undo', () => patch({ colors: before }));
  };

  const addCta = () => {
    if (st.ctas.length >= E8_BRAND_CTA_MAX) return;
    patch({ ctas: st.ctas.concat([{ id: 'cta-' + Math.random().toString(36).slice(2, 8), label: '', href: '' }]) });
  };
  const editCta = (id, fields) => patch({
    ctas: st.ctas.map((c) => (c.id === id ? Object.assign({}, c, fields) : c)),
  });
  /* Same reasoning as the palette reset, and the same grammar the other pages use for a destructive
     row action: restore the whole prior list rather than re-inserting the one entry, so the button
     comes back at its original index with whatever label and link had been typed into it. */
  const dropCta = (id) => {
    const before = st.ctas;
    patch({ ctas: before.filter((c) => c.id !== id) });
    e8BrandToast('Call-to-action button removed', 'Undo', () => patch({ ctas: before }));
  };

  const published = e8BrandSlugTrim(st.slug);
  /* The shell's own mark is a single-glyph tile (app/shell.jsx renders `8` beside the ELEV8
     wordmark); the workspace placeholder follows that shape with the workspace's initial. */
  const mono = (st.name.trim().charAt(0) || '8').toUpperCase();

  return (
    <Page title="General">
      <Section title="General settings">
        <Card>
          <Row label="Logo"
            desc="Shown on your public job board, on application forms and in the emails candidates receive."
            control={(
              <E8BrandMedia kind="logo" src={logo} mono={mono} word={st.name.trim() || 'Workspace'}
                tone={st.colors.primary} label="Upload a workspace logo"
                accept="image/png,image/jpeg,image/svg+xml"
                onPick={(file) => pickImage(file, setLogo)} onClear={() => setLogo('')} />
            )} />

          <Row label="Favicon"
            desc="A square PNG or SVG, at least 64 × 64 px. It appears in the browser tab of your job board and on bookmarks."
            control={(
              <E8BrandMedia kind="favicon" src={favicon} mono={mono} tone={st.colors.primary}
                label="Upload a favicon" accept="image/png,image/svg+xml,image/x-icon"
                onPick={(file) => pickImage(file, setFavicon)} onClear={() => setFavicon('')} />
            )} />

          <Row label="Name"
            desc="How this workspace is named to candidates, clients and everyone on your team."
            control={(
              <Input size="md" value={st.name} placeholder="Workspace name" ariaLabel="Workspace name"
                onChange={(v) => { setRenamed(true); patch({ name: v.slice(0, 48) }); }} />
            )}>
            {renamed && st.by ? (
              <span className="e8-set-brand-hint">Last changed by {st.by} in this session.</span>
            ) : null}
          </Row>

          <Row label="Public URL"
            desc="Where your job board is published. Lowercase letters, numbers and hyphens only."
            control={(
              <Input size="lg" prefix={E8_BRAND_HOST} value={st.slug} placeholder="workspace"
                ariaLabel="Public URL slug" onChange={(v) => patch({ slug: e8BrandSlugType(v) })} />
            )}>
            {published ? (
              <span className="e8-set-brand-hint">
                Live at <span className="e8-set-brand-url">{E8_BRAND_HOST}<b>{published}</b></span>
                {' · '}{reach.jobs} published {reach.jobs === 1 ? 'job' : 'jobs'}
              </span>
            ) : (
              <span className="e8-set-brand-hint is-warn">
                Without a slug your job board has no address and nobody can reach it.
              </span>
            )}
          </Row>

          <Row label="Open applications"
            desc={st.openApps
              ? 'Anyone can apply to your ' + reach.jobs + ' published jobs without an invite. '
                + reach.site + ' of the ' + reach.apps + ' applications in this workspace arrived through the careers site.'
              : 'Only people you send a direct link to can apply. The board stays visible, but every apply button is hidden.'}
            control={(
              <Toggle checked={st.openApps} label="Open applications"
                onChange={(v) => {
                  patch({ openApps: v });
                  e8BrandToast(v ? 'Applications are open to anyone with the link'
                    : 'Applications are now invite-only');
                }} />
            )} />
        </Card>
      </Section>

      <Section title="Branding of public pages"
        desc="These three colours are applied to your job board, application forms and candidate emails."
        action={<Btn kind="quiet" icon="restart_alt" onClick={resetColors}>Reset to ELEV8 palette</Btn>}>
        <Card>
          {E8_BRAND_COLORS.map((c) => (
            <Row key={c.key} label={c.label} desc={c.desc}
              control={(
                <E8BrandSwatch label={c.label} value={st.colors[c.key]}
                  onChange={(hex) => setColor(c.key, hex)} />
              )} />
          ))}

          <Row label="Call to action button"
            desc={'One or two buttons pinned to the top of your job board — “Join our talent network”, '
              + '“Talk to a recruiter”. ' + st.ctas.length + ' of ' + E8_BRAND_CTA_MAX + ' in use.'}
            control={(
              <Btn icon="add" onClick={addCta} disabled={st.ctas.length >= E8_BRAND_CTA_MAX}
                title={st.ctas.length >= E8_BRAND_CTA_MAX
                  ? 'Two buttons is the maximum — remove one first' : undefined}>
                Add CTA button
              </Btn>
            )}>
            {st.ctas.length ? (
              <ul className="e8-set-brand-ctas">
                {st.ctas.map((c, i) => (
                  <li className="e8-set-brand-cta" key={c.id}>
                    <Input size="md" value={c.label} placeholder="Button label"
                      ariaLabel={'Call to action ' + (i + 1) + ' label'}
                      onChange={(v) => editCta(c.id, { label: v.slice(0, 40) })} />
                    <Input size="lg" value={c.href} placeholder="https://"
                      ariaLabel={'Call to action ' + (i + 1) + ' link'}
                      onChange={(v) => editCta(c.id, { href: v })} />
                    <Btn kind="danger" icon="close" onClick={() => dropCta(c.id)}>Remove</Btn>
                  </li>
                ))}
              </ul>
            ) : null}
          </Row>
        </Card>
      </Section>
    </Page>
  );
}

window.E8SetPages = window.E8SetPages || {};
Object.assign(window.E8SetPages, { general: SetGeneralPage });
