/* ════════════════════════════════════════════════════════════════════════════
   PrivacyConsent.jsx — the ACCOUNT PRIVACY & CONSENT panel   (lane BUILD-0916-R)
   ════════════════════════════════════════════════════════════════════════════

   ⛔ THIS IS A SETTINGS SURFACE. NOT A POPUP, NOT A MODAL, NOT A BANNER.
      A banner is for VISITORS on a CUSTOMER's site — a different capability,
      different subject, different consent type, and NOT built here. For an
      account holder, consent lives where every other account preference lives:
      in settings, reachable at any time, not interrupting anything.

   ⛔ WITHDRAWAL IS AS EASY AS GRANTING, AND THAT IS STRUCTURAL RATHER THAN A
      PROMISE. Under GDPR Art. 7(3) consent that cannot be withdrawn as easily
      as it was given is not valid consent. Here the SAME control, in the SAME
      place, at the SAME prominence, flips both ways in ONE click. There is no
      confirmation step on withdrawal that does not also exist on granting, no
      "are you sure", and no retention pitch in the way. A dark pattern on this
      surface would invalidate the consent it collects.

   ACCESS DECLARATION (role-completeness; master §3 #29)
     Personas:      customer, internal_partner (inherits the customer list).
                    A staff persona does NOT get this tab — it shows the
                    VIEWER's OWN consent state, and a staff member viewing it
                    inside an impersonated account would render their own
                    user-level rows against someone else's account. Staff read
                    consent through the SA-only Compliance & Consent monitor
                    (Exfiltrate boundary, master §3 #22), which already exists.
     Agency roles:  owner / dev / member may all view and may all change their
                    OWN user-level consent. The ACCOUNT-level consent
                    (telemetry_optin) is owner-only — enforced SERVER-side by
                    POST /account/consent, which 403s OWNER_ONLY. This surface
                    disables the control for non-owners as a courtesy; nav
                    visibility and a disabled button never widen API scope.
     Plan gate:     NONE, deliberately. Consent is not a feature to be sold, and
                    putting a privacy control behind a paid tier would be
                    indefensible on its face.
     Client portal: not visible.

   FOUNDATION (R-FOUNDATIONSET — the full declaration set is in this lane's report)
     · WCAG 2.1 AA — every control is a real <button> with an accessible name,
       state is conveyed by TEXT and aria-pressed rather than colour alone, the
       live region announces the result of a save, and focus is never trapped.
       ⚠️ CONFORMANCE IS NOT CLAIMED HERE: built to the standard, unaudited at
       the render layer by this lane (R-CONFORMPROOF — a declaration is not proof).
     · i18n — every string reads from window.WPSB.t(). No hardcoded copy.
     · RTL/LTR — LOGICAL CSS PROPERTIES ONLY (marginInline*, paddingInline*,
       textAlign start/end). ⭐ This is the FIRST app surface to do so: measured
       2026-09-16, logical properties appear in 0 of 24 design/*.jsx files that
       use physical ones. Retrofitting is a whole pass; doing it at authoring
       time costs nothing.
     · Failure posture — THREE distinct states (loading / error / empty), never
       a false empty. A failed read says it failed (master §3 #27).
     · Analytics — /client/events/track, the CUSTOMER-scoped ingest.
       ⛔ NOT /events/track, which is SA-only: four app surfaces call that one
       and get 401, and their events are silently dropped. Do not copy them.
   ════════════════════════════════════════════════════════════════════════════ */
(function () {
  'use strict';

  var t = function (k, v, f) {
    return (window.WPSB && window.WPSB.t) ? window.WPSB.t(k, v, f) : (f || k);
  };

  function apiBase() {
    return (window.WPSB && window.WPSB.apiBase) ? window.WPSB.apiBase() : 'https://api.wpsitebeam.io';
  }
  function authToken() {
    return (window.WPSB && window.WPSB.getToken && window.WPSB.getToken())
        || (function () { try { return localStorage.getItem('wpsb-auth-token'); } catch (e) { return null; } })();
  }

  /* Customer-scoped analytics ingest. account_id + user_id come from the JWT
     server-side — never from the body. Fire-and-forget: a failure can never
     block or break a privacy control. */
  function emitClientEvent(eventType, props) {
    try {
      var token = authToken(); if (!token) return;
      fetch(apiBase() + '/client/events/track', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
        body: JSON.stringify({ event_type: eventType, props: props || {} }),
      }).catch(function () {});
    } catch (e) { /* swallow — analytics never blocks the surface */ }
  }

  /* The three user-controllable consents, in the order the customer meets them.
     Kept in sync with consent.js TOGGLE_TYPES — the SERVER decides what is
     accepted; this array only decides what is rendered. A type the server
     rejects renders and then fails loudly, which is the right direction: a
     surface that silently hides a consent type would hide a withdrawal path. */
  var TOGGLES = [
    { type: 'telemetry_optin',        scope: 'account' },
    { type: 'anonymous_data_sharing', scope: 'user' },
    { type: 'marketing_optin',        scope: 'user' },
  ];

  function h(tag, props, children) { return React.createElement(tag, props, children); }

  function Card(children, extra) {
    return h('div', {
      className: 'wiz-card',
      style: Object.assign({
        background: 'var(--surface)', border: '1px solid var(--border)',
        borderRadius: 'var(--r-xl)', padding: 'var(--sp-6)', marginBlockEnd: 'var(--sp-5)',
      }, extra || {}),
    }, children);
  }

  function PrivacyConsent() {
    var st = React.useState({ phase: 'loading', current: [], history: [], flags: {}, flagsResolved: null, error: null });
    var s = st[0], setS = st[1];
    var busyState = React.useState(null);
    var busy = busyState[0], setBusy = busyState[1];
    var noticeState = React.useState(null);
    var notice = noticeState[0], setNotice = noticeState[1];

    var isOwner = !!(window.WPSBD && window.WPSBD.agencyRole
      ? window.WPSBD.agencyRole === 'owner'
      : true); /* unknown ⇒ render enabled; the server is the gate and 403s OWNER_ONLY. */

    var load = React.useCallback(function () {
      setS(function (p) { return Object.assign({}, p, { phase: 'loading', error: null }); });
      var token = authToken();
      if (!token) { setS({ phase: 'error', current: [], history: [], flags: {}, flagsResolved: null, error: 'no_token' }); return; }
      var hdr = { Authorization: 'Bearer ' + token };

      Promise.all([
        fetch(apiBase() + '/account/consent', { headers: hdr }),
        fetch(apiBase() + '/account/privacy/flags', { headers: hdr }),
      ]).then(function (rs) {
        /* ⛔ The CONSENT read failing is an ERROR state, never an empty one.
           Rendering "nothing is set" over a failed read is the false-empty
           defect (catch #40) on a compliance surface, which is the worst place
           for it: a customer would read it as "my choices were lost". */
        if (!rs[0].ok) throw new Error('consent_read_failed');
        return Promise.all([
          rs[0].json(),
          rs[1].ok ? rs[1].json() : { flags: {}, resolved: 'read_failed' },
        ]);
      }).then(function (j) {
        setS({
          phase: 'ready',
          current: (j[0] && j[0].current) || [],
          history: (j[0] && j[0].history) || [],
          flags: (j[1] && j[1].flags) || {},
          flagsResolved: (j[1] && j[1].resolved) || null,
          error: null,
        });
      }).catch(function () {
        setS({ phase: 'error', current: [], history: [], flags: {}, flagsResolved: null, error: 'load_failed' });
      });
    }, []);

    React.useEffect(function () {
      load();
      emitClientEvent('surface.viewed', { surface: 'privacy_consent' });
    }, [load]);

    function stateOf(type) {
      for (var i = 0; i < s.current.length; i++) {
        if (s.current[i].consent_type === type) {
          var a = s.current[i].action;
          if (a === 'withdrawn') return 'withdrawn';
          if (a === 'granted' || a === 'acknowledged') return 'granted';
          return 'absent';
        }
      }
      return 'absent';
    }

    function setConsent(type, action) {
      setBusy(type); setNotice(null);
      fetch(apiBase() + '/account/consent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + authToken() },
        body: JSON.stringify({ consent_type: type, action: action }),
      }).then(function (r) {
        if (!r.ok) throw new Error('write_failed');
        emitClientEvent('consent.toggled', { consent_type: type, action: action });
        /* Re-read rather than patching local state: the LEDGER is the truth and
           an optimistic update would show a choice that may not have persisted. */
        load();
      }).catch(function () {
        setNotice(t('page.privacy.save_failed'));
      }).then(function () { setBusy(null); });
    }

    /* ── ONE control, both directions, identical weight ──────────────────────
       The grant button and the withdraw button are THE SAME ELEMENT with the
       same class, the same size and the same position. That is what makes
       "as easy to withdraw as to grant" a property of the markup rather than a
       claim in a comment. */
    function Toggle(cfg) {
      var cur = stateOf(cfg.type);
      var on = cur === 'granted';
      var next = on ? 'withdrawn' : 'granted';
      var disabled = (cfg.scope === 'account' && !isOwner) || busy === cfg.type;
      var label = t('page.privacy.toggle.' + cfg.type + '.label');

      return h('div', {
        key: cfg.type,
        style: {
          display: 'flex', gap: 'var(--sp-4)', alignItems: 'flex-start',
          paddingBlock: 'var(--sp-4)', borderBlockEnd: '1px solid var(--border)',
          flexWrap: 'wrap',
        },
      }, [
        h('div', { key: 'txt', style: { flex: '1 1 260px', minInlineSize: 0 } }, [
          h('div', { key: 'l', style: { fontWeight: 600, color: 'var(--text)' } }, label),
          h('div', { key: 'd', style: { color: 'var(--dim)', fontSize: '.9rem', marginBlockStart: 4 } },
            t('page.privacy.toggle.' + cfg.type + '.desc')),
          cfg.scope === 'account'
            ? h('div', { key: 'o', style: { color: 'var(--dim)', fontSize: '.82rem', marginBlockStart: 4 } },
                t('page.privacy.owner_only'))
            : null,
          cur === 'absent'
            ? h('div', { key: 'n', style: { color: 'var(--dim)', fontSize: '.82rem', marginBlockStart: 4 } },
                t('page.privacy.state.absent_note'))
            : null,
        ]),
        h('div', { key: 'ctl', style: { display: 'flex', alignItems: 'center', gap: 'var(--sp-3)' } }, [
          /* State as TEXT, not colour alone — WCAG 1.4.1. */
          h('span', {
            key: 'st',
            style: { fontSize: '.85rem', color: 'var(--dim)', minInlineSize: '4.5rem', textAlign: 'end' },
          }, t('page.privacy.state.' + cur)),
          h('button', {
            key: 'b',
            className: 'wiz-btn',
            type: 'button',
            disabled: disabled,
            'aria-pressed': on ? 'true' : 'false',
            'aria-label': (on ? t('page.privacy.action.withdraw') : t('page.privacy.action.grant')) + ': ' + label,
            onClick: function () { setConsent(cfg.type, next); },
            style: { minInlineSize: '7.5rem', minBlockSize: 44, opacity: disabled ? .55 : 1 },
          }, busy === cfg.type
              ? t('page.privacy.action.working')
              : (on ? t('page.privacy.action.withdraw') : t('page.privacy.action.grant'))),
        ]),
      ]);
    }

    function Header() {
      return h('div', { style: { marginBlockEnd: 'var(--sp-5)' } }, [
        h('div', { key: 'e', className: 'wiz-eyebrow' }, t('page.privacy.eyebrow')),
        h('h1', { key: 'h', className: 'wiz-title', style: { margin: 0, fontSize: '1.6rem' } }, t('page.privacy.title')),
        h('p', { key: 's', style: { color: 'var(--dim)', maxInlineSize: '60ch', marginBlockStart: 8 } },
          t('page.privacy.sub')),
      ]);
    }

    if (s.phase === 'loading') {
      return h('div', { style: { padding: 'var(--sp-6)', maxInlineSize: 'var(--content-max)', marginInline: 'auto' } },
        [h(Header, { key: 'hd' }), Card(h('div', { style: { color: 'var(--dim)' } }, t('page.privacy.loading')))]);
    }

    /* ⛔ ERROR is its own state and says so. It is NOT rendered as "you have no
       consents recorded" — that would be a false zero on a compliance surface. */
    if (s.phase === 'error') {
      return h('div', { style: { padding: 'var(--sp-6)', maxInlineSize: 'var(--content-max)', marginInline: 'auto' } },
        [h(Header, { key: 'hd' }), Card([
          h('div', { key: 'a', role: 'alert', style: { fontWeight: 600, color: 'var(--text)' } }, t('page.privacy.error.title')),
          h('p', { key: 'b', style: { color: 'var(--dim)', maxInlineSize: '60ch' } }, t('page.privacy.error.body')),
          h('button', { key: 'r', type: 'button', className: 'wiz-btn wiz-btn-primary',
            onClick: load, style: { minBlockSize: 44 } }, t('page.privacy.error.retry')),
        ])]);
    }

    var gpcOn = !!(navigator && navigator.globalPrivacyControl);
    var flagKeys = Object.keys(s.flags || {});
    var anyFlagOn = flagKeys.some(function (k) { return s.flags[k] === true; });

    return h('div', { style: { padding: 'var(--sp-6)', maxInlineSize: 'var(--content-max)', marginInline: 'auto' } }, [
      h(Header, { key: 'hd' }),

      /* Live region: the RESULT of a save is announced, not just rendered. */
      h('div', { key: 'live', role: 'status', 'aria-live': 'polite',
        style: { color: 'var(--red)', marginBlockEnd: notice ? 'var(--sp-3)' : 0 } }, notice || ''),

      Card([
        h('h2', { key: 'h', style: { margin: 0, fontSize: '1.05rem' } }, t('page.privacy.section.choices')),
        h('p', { key: 'e', style: { color: 'var(--dim)', fontSize: '.85rem', maxInlineSize: '60ch' } },
          t('page.privacy.equal_ease')),
        h('div', { key: 'list' }, TOGGLES.map(Toggle)),
      ]),

      Card([
        h('h2', { key: 'h', style: { margin: 0, fontSize: '1.05rem' } }, t('page.privacy.gpc.title')),
        h('p', { key: 'p', style: { color: 'var(--dim)', marginBlockStart: 8, maxInlineSize: '60ch' } },
          gpcOn ? t('page.privacy.gpc.on') : t('page.privacy.gpc.off')),
        h('p', { key: 'a', style: { color: 'var(--dim)', fontSize: '.85rem', maxInlineSize: '60ch' } },
          t('page.privacy.gpc.always')),
      ]),

      Card([
        h('h2', { key: 'h', style: { margin: 0, fontSize: '1.05rem', marginBlockEnd: 'var(--sp-3)' } },
          t('page.privacy.section.record')),
        s.current.length === 0
          ? h('div', { key: 'empty', style: { color: 'var(--dim)' } }, t('page.privacy.record.empty'))
          : h('table', { key: 'tbl', style: { inlineSize: '100%', borderCollapse: 'collapse', fontSize: '.9rem' } }, [
              h('thead', { key: 'th' }, h('tr', null, [
                h('th', { key: 1, scope: 'col', style: { textAlign: 'start', padding: 'var(--sp-2)', color: 'var(--dim)' } }, t('page.privacy.record.col_type')),
                h('th', { key: 2, scope: 'col', style: { textAlign: 'start', padding: 'var(--sp-2)', color: 'var(--dim)' } }, t('page.privacy.record.col_state')),
                h('th', { key: 3, scope: 'col', style: { textAlign: 'start', padding: 'var(--sp-2)', color: 'var(--dim)' } }, t('page.privacy.record.col_when')),
              ])),
              h('tbody', { key: 'tb' }, s.current.map(function (r, i) {
                var lbl = r.action === 'withdrawn' ? 'withdrawn' : (r.action === 'granted' || r.action === 'acknowledged' ? 'granted' : 'absent');
                return h('tr', { key: i, style: { borderBlockStart: '1px solid var(--border)' } }, [
                  h('td', { key: 1, style: { padding: 'var(--sp-2)' } }, r.consent_type),
                  h('td', { key: 2, style: { padding: 'var(--sp-2)' } }, t('page.privacy.state.' + lbl)),
                  h('td', { key: 3, style: { padding: 'var(--sp-2)', color: 'var(--dim)' } },
                    r.created_at ? new Date(r.created_at).toLocaleString() : '—'),
                ]);
              })),
            ]),
      ]),

      /* Public privacy surfaces — every one DEFAULT OFF until counsel confirms
         the obligation attaches. An unresolved read renders as UNRESOLVED, never
         as "none are on": those are different facts and only one of them is
         something we know. */
      Card([
        h('h2', { key: 'h', style: { margin: 0, fontSize: '1.05rem' } }, t('page.privacy.section.surfaces')),
        h('p', { key: 'p', style: { color: 'var(--dim)', marginBlockStart: 8, maxInlineSize: '60ch' } },
          (s.flagsResolved && s.flagsResolved !== 'read')
            ? t('page.privacy.surfaces.unresolved')
            : (anyFlagOn ? '' : t('page.privacy.surfaces.none'))),
        anyFlagOn
          ? h('ul', { key: 'u', style: { marginBlockStart: 8, paddingInlineStart: '1.25rem' } },
              flagKeys.filter(function (k) { return s.flags[k] === true; })
                      .map(function (k) { return h('li', { key: k }, k); }))
          : null,
      ]),
    ]);
  }

  window.PrivacyConsent = PrivacyConsent;
})();
