/* Entitlements.jsx — the ONE entitlement source for the app (slice 2, BUILD-0918-U)
   Repo: wpsitebeam-app / design/Entitlements.jsx
   Rulings: B21 (served to the app), B32 (locked, never hidden), B12 (owner-only).

   EXPORTS
     window.WPSBD.entitlements      — the last loaded payload, or null
     window.WPSBEntitlements.load() — fetch + publish + notify (coalesced)
     window.WPSBEntitlements.subscribe(cb) / unsubscribe(cb)
     window.useEntitlements()       — React hook; re-renders on change
     window.PlanLocked              — the locked-state panel every gated surface renders

   ⭐ WHY THIS FILE EXISTS RATHER THAN EACH SURFACE READING THE JWT: a token's `plan`
   claim is minted at login and goes stale the moment a customer upgrades — which is
   exactly when they are looking at the screen. Every field here comes from
   GET /account/entitlements, which reads the database on each call.

   ⛔ NEVER READ user_metadata, NEVER READ THE JWT, AND NEVER KEEP A SECOND COPY OF THE
   PLAN LADDER IN THIS REPO. A client-side copy of the ladder is free to drift from the
   server's gates, and when it does the app either renders a locked state over a feature
   the customer has paid for, or offers one the server will 403. The server's
   PLAN_FEATURES map is the only ladder; this file renders it.

   ⚠️ FAILURE POSTURE, STATED: a failed load leaves `entitlements` NULL, and every
   consumer treats null as UNKNOWN — not as "denied" and not as "allowed". A surface
   renders its normal loading state; it does NOT render a lock, because locking on an
   unreadable response would tell a paying customer they had been downgraded.

   Standards: React.createElement throughout (no JSX — the inline-Babel safety rule);
   CSS variables only; WCAG 2.1 AA (aria-live on state changes, labelled controls,
   visible focus, no colour-only state); logical CSS properties for RTL.
*/
(function () {
  'use strict';

  var h  = React.createElement;
  var tx = function (k, v, f) { return (window.WPSB && window.WPSB.t) ? window.WPSB.t(k, v, f) : (f != null ? f : k); };
  function apiBase() { return (window.WPSBD && window.WPSBD.apiBase) || 'https://api.wpsitebeam.io'; }
  function token()   { return (window.WPSBD && window.WPSBD.getToken && window.WPSBD.getToken()) || window.currentToken || ''; }

  window.WPSBD = window.WPSBD || {};
  var subs = new Set();
  var inFlight = null;
  var lastAt = 0;

  function notify() { subs.forEach(function (cb) { try { cb(window.WPSBD.entitlements); } catch (e) {} }); }

  /* Coalesced: several surfaces mounting at once produce ONE request, not one each. */
  function load(force) {
    if (inFlight) return inFlight;
    if (!force && window.WPSBD.entitlements && (Date.now() - lastAt) < 30000) {
      return Promise.resolve(window.WPSBD.entitlements);
    }
    var t = token();
    if (!t) return Promise.resolve(null);
    inFlight = fetch(apiBase() + '/account/entitlements', { headers: { 'Authorization': 'Bearer ' + t } })
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (data) {
        if (data) { window.WPSBD.entitlements = data; lastAt = Date.now(); notify(); }
        return data;
      })
      .catch(function (e) { console.warn('[entitlements]', e && e.message); return null; })
      .finally(function () { inFlight = null; });
    return inFlight;
  }

  window.WPSBEntitlements = {
    load: load,
    subscribe: function (cb) { subs.add(cb); },
    unsubscribe: function (cb) { subs.delete(cb); },
    /* allowed(key) → true | false | null. NULL IS "UNKNOWN", NOT "NO". */
    allowed: function (key) {
      var e = window.WPSBD.entitlements;
      if (!e || !e.features || !e.features[key]) return null;
      return !!e.features[key].allowed;
    },
    requiredPlan: function (key) {
      var e = window.WPSBD.entitlements;
      return (e && e.features && e.features[key]) ? e.features[key].required_plan_display || e.features[key].required_plan : null;
    },
  };

  function useEntitlements() {
    var s = React.useState(window.WPSBD.entitlements);
    var ent = s[0], setEnt = s[1];
    React.useEffect(function () {
      var cb = function (v) { setEnt(v); };
      window.WPSBEntitlements.subscribe(cb);
      load();
      /* Re-read on focus: a customer who upgrades in a Stripe tab and comes back should
         see the new plan without a reload. */
      var onFocus = function () { load(true); };
      window.addEventListener('focus', onFocus);
      return function () { window.WPSBEntitlements.unsubscribe(cb); window.removeEventListener('focus', onFocus); };
    }, []);
    return ent;
  }

  /* ── PlanLocked — B32: unavailable features show as LOCKED with the required plan
     and an upgrade button. NOTHING IS HIDDEN BY PLAN.
     ⭐ HIDING WAS THE OLD BEHAVIOUR AND IT IS WORSE FOR BOTH SIDES: a customer cannot
     ask for something they cannot see, and support cannot tell "the tab is missing"
     from "the tab is broken". A lock states the plan that unlocks it and offers the
     one control that does so. */
  function PlanLocked(props) {
    var feature = props.feature;
    var required = props.requiredPlan || (window.WPSBEntitlements.requiredPlan(feature)) || '';
    var title = props.title || tx('entitlement.locked_title', null, 'Available on a higher plan');
    /* K5 — this shape (prefer the in-page event, else navigate to ?billing=plans) was
       already correct here, so it was EXTRACTED into window.WPSB.BILLING.goUpgrade and
       every other CTA repointed at it. The core is EXTENDED, not forked (R-SHAREDCORE);
       this call site keeps the behaviour it already had. */
    function openPicker() { window.WPSB.billing().goUpgrade(); }
    return h('div', {
      className: 'card', role: 'region',
      'aria-label': tx('entitlement.locked_aria', null, 'This feature requires a plan upgrade'),
      style: { padding: 20, textAlign: 'center', borderInline: '1px solid var(--border)' },
    },
      h('div', { style: { fontWeight: 700, fontSize: '.95rem', color: 'var(--text)', marginBlockEnd: 6 } }, title),
      h('p', { style: { margin: 0, marginBlockEnd: 14, fontSize: '.82rem', color: 'var(--dim)' } },
        required
          ? tx('entitlement.locked_body', { plan: required }, 'This is included from the ' + required + ' plan.')
          : tx('entitlement.locked_body_generic', null, 'This is included on a higher plan.')),
      h('button', {
        type: 'button', className: 'btn btn-primary btn-sm', onClick: openPicker,
        'aria-label': tx('entitlement.upgrade_aria', null, 'See plans and upgrade'),
      }, tx('entitlement.upgrade', null, 'See plans'))
    );
  }

  /* PlanUpgradeNotice — renders a PLAN_UPGRADE_REQUIRED 403 body from its DATA FIELDS.
     ⛔ NEVER STRING-MATCH THE ENGLISH `error` SENTENCE. The server now sends `feature`,
     `required_plan` and `message_key` precisely so the app does not have to. */
  function PlanUpgradeNotice(props) {
    var body = props.body || {};
    if (body.code !== 'PLAN_UPGRADE_REQUIRED') return null;
    return h(PlanLocked, { feature: body.feature, requiredPlan: body.required_plan });
  }

  window.useEntitlements    = useEntitlements;
  window.PlanLocked         = PlanLocked;
  window.PlanUpgradeNotice  = PlanUpgradeNotice;
  console.log('[WPSB] Entitlements loaded');
})();
