/* PlanPicker.jsx — the purchase surface (slice 2, BUILD-0918-U)
   Repo: wpsitebeam-app / design/PlanPicker.jsx
   Rulings: B7 (purchase path), B12 (owner-only), B33 (promo interval), B32 (locked
   never hidden), B24 (no promotional-email consent in slice 2).

   EXPORTS
     window.PlanPicker            — the picker itself, rendered in the Billing page
     window.wpsbOpenPlanPicker()  — opens it (the ONE deep link, ?billing=plans)
     window.BillingReturnHandler  — reads ?billing=success|cancelled|portal|plans

   ⛔ EVERY PRICE ON THIS SURFACE COMES FROM GET /billing/plans, WHICH READS STRIPE.
   No price literal appears in this file. The repo's own pricing comments have drifted
   from Stripe twice on record, and a picker rendering a number this repo believes —
   rather than the number Stripe will charge — is a Radical-Transparency defect the
   customer discovers at the card form.

   ⛔ THE PICKER IS RENDERED ONLY WHEN can_manage_billing. A non-owner sees a sentence
   explaining who can change the plan and NO control (B12). An internal partner sees
   nothing at all — those accounts are not billed through Stripe, so a purchase button
   on one can only produce an error.

   ⚠️ SERVER ERRORS ARE RENDERED FROM THEIR `code`, NEVER BY MATCHING ENGLISH PROSE.
   PROMO_INVALID, PROMO_INTERVAL_MISMATCH, USE_CHANGE_PLAN, PURCHASE_PAUSED and
   NOT_BILLABLE each get their own i18n message.

   Standards: React.createElement throughout (no JSX — the inline-Babel safety rule);
   CSS variables only; WCAG 2.1 AA — the interval toggle is a real radiogroup, the
   promo error is an aria-live region, every control is labelled, focus is visible, and
   no state is communicated by colour alone (the current plan carries a text badge);
   logical CSS properties throughout for RTL.
*/
(function () {
  'use strict';

  var useState = React.useState, useEffect = React.useEffect, useCallback = React.useCallback;
  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 || ''; }

  async function api(path, opts) {
    var r = await fetch(apiBase() + path, Object.assign({
      headers: { 'Authorization': 'Bearer ' + token(), 'Content-Type': 'application/json' },
    }, opts || {}));
    var data = null;
    try { data = await r.json(); } catch (e) {}
    if (!r.ok) { var e2 = new Error((data && data.error) || ('HTTP ' + r.status)); e2.code = data && data.code; e2.body = data; throw e2; }
    return data;
  }

  /* Error copy keyed on the server's CODE. An unknown code falls back to the server's
     own sentence rather than to a generic "something went wrong" — the server's text is
     more specific than anything this file could invent. */
  function errText(e) {
    switch (e && e.code) {
      case 'PROMO_INVALID':            return tx('billing.promo_invalid', null, 'That code is not valid.');
      case 'PROMO_INTERVAL_MISMATCH':  return e.body && e.body.code_interval === 'year'
        ? tx('billing.promo_annual_only', null, 'That code applies to annual plans only.')
        : tx('billing.promo_monthly_only', null, 'That code applies to monthly plans only.');
      case 'PROMO_LOOKUP_UNAVAILABLE': return tx('billing.promo_lookup_unavailable', null, 'We could not check that code right now. Please try again.');
      case 'USE_CHANGE_PLAN':          return tx('billing.use_change_plan', null, 'You already have a subscription — use Change plan so the difference is prorated.');
      case 'PURCHASE_PAUSED':          return tx('billing.purchase_paused', null, 'New purchases are paused right now. Your existing subscription is unaffected.');
      case 'NOT_BILLABLE':             return tx('billing.not_billable', null, 'This account is not billed through Stripe.');
      case 'AUDIT_WRITE_FAILED':       return tx('billing.audit_write_failed', null, 'We could not record that action, so it was not performed. Please try again.');
      default:                         return (e && e.message) || tx('common.error', null, 'Something went wrong.');
    }
  }

  function money(cents, currency) {
    if (cents == null) return '';
    try {
      return new Intl.NumberFormat(undefined, { style: 'currency', currency: (currency || 'usd').toUpperCase(), maximumFractionDigits: 0 }).format(cents / 100);
    } catch (e) { return '$' + Math.round(cents / 100); }
  }

  function PlanPicker(props) {
    var onClose = props && props.onClose;
    var s1 = useState(null);  var data = s1[0], setData = s1[1];
    var s2 = useState(true);  var loading = s2[0], setLoading = s2[1];
    var s3 = useState(null);  var err = s3[0], setErr = s3[1];
    var s4 = useState('month'); var interval = s4[0], setInterval_ = s4[1];
    var s5 = useState(false); var byok = s5[0], setByok = s5[1];
    var s6 = useState('');    var promo = s6[0], setPromo = s6[1];
    var s7 = useState(null);  var busy = s7[0], setBusy = s7[1];
    var s8 = useState(null);  var preview = s8[0], setPreview = s8[1];

    var reload = useCallback(function () {
      setLoading(true); setErr(null);
      api('/billing/plans')
        .then(function (d) { setData(d); if (d && d.current_price_id) { /* leave the toggle where the customer put it */ } })
        .catch(function (e) { setErr(errText(e)); })
        .finally(function () { setLoading(false); });
    }, []);
    useEffect(function () { reload(); }, [reload]);

    if (loading) return h('div', { className: 'card', style: { padding: 20, color: 'var(--dim)', fontSize: '.85rem' } },
      tx('common.loading', null, 'Loading…'));

    if (err && !data) return h('div', { className: 'card', role: 'alert', style: { padding: 20, color: 'var(--red)', fontSize: '.85rem' } }, err);

    if (data && data.is_internal_partner) {
      /* B31 — internal partners see no purchase controls, ever. */
      return h('div', { className: 'box info', style: { padding: 16, fontSize: '.84rem' } },
        tx('billing.partner_no_purchase', null, 'This is an Internal Partner account — there is no Stripe billing to manage.'));
    }

    var plans = (data && data.plans) || [];
    var shown = plans.filter(function (p) { return p.interval === interval && !!p.byok === byok; });
    var hasByokChoice = plans.some(function (p) { return p.byok; });

    async function startCheckout(p) {
      setBusy(p.price_id); setErr(null);
      try {
        var out = await api('/billing/checkout', { method: 'POST', body: JSON.stringify({ price_id: p.price_id, promotion_code: promo.trim() || undefined }) });
        if (out && out.url) { window.location.href = out.url; return; }
        setErr(tx('billing.no_checkout_url', null, 'Checkout did not return a link. Please try again.'));
      } catch (e) { setErr(errText(e)); } finally { setBusy(null); }
    }

    async function loadPreview(p) {
      setBusy('preview:' + p.price_id); setErr(null); setPreview(null);
      try {
        var out = await api('/billing/upcoming-invoice?price_id=' + encodeURIComponent(p.price_id));
        setPreview({ plan: p, data: out });
      } catch (e) { setErr(errText(e)); } finally { setBusy(null); }
    }

    async function confirmChange() {
      if (!preview) return;
      setBusy('change'); setErr(null);
      try {
        await api('/billing/change-plan', { method: 'POST', body: JSON.stringify({ price_id: preview.plan.price_id }) });
        setPreview(null);
        if (window.WPSBEntitlements) window.WPSBEntitlements.load(true);
        window.wpsbToast && window.wpsbToast(tx('billing.plan_changed', null, 'Plan changed. The prorated amount appears on your next invoice.'), 'ok');
        reload();
      } catch (e) { setErr(errText(e)); } finally { setBusy(null); }
    }

    var IntervalToggle = h('div', { role: 'radiogroup', 'aria-label': tx('billing.interval_aria', null, 'Billing interval'),
      style: { display: 'flex', gap: 8, marginBlockEnd: 12 } },
      ['month', 'year'].map(function (iv) {
        var on = interval === iv;
        return h('button', {
          key: iv, type: 'button', role: 'radio', 'aria-checked': on,
          onClick: function () { setInterval_(iv); setPreview(null); },
          className: on ? 'btn btn-primary btn-sm' : 'btn btn-ghost btn-sm',
        }, iv === 'month' ? tx('billing.monthly', null, 'Monthly') : tx('billing.annual', null, 'Annual'));
      })
    );

    var ByokToggle = hasByokChoice ? h('div', { role: 'radiogroup', 'aria-label': tx('billing.byok_aria', null, 'AI key option'),
      style: { display: 'flex', gap: 8, marginBlockEnd: 16 } },
      [false, true].map(function (b) {
        var on = byok === b;
        return h('button', {
          key: String(b), type: 'button', role: 'radio', 'aria-checked': on,
          onClick: function () { setByok(b); setPreview(null); },
          className: on ? 'btn btn-primary btn-sm' : 'btn btn-ghost btn-sm',
        }, b ? tx('billing.byok', null, 'Bring your own AI key') : tx('billing.ai_included', null, 'AI included'));
      })
    ) : null;

    return h('div', { className: 'card', style: { padding: 20 } },
      h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBlockEnd: 12 } },
        h('h3', { style: { margin: 0, fontSize: '1rem', color: 'var(--text)' } }, tx('billing.choose_plan', null, 'Choose a plan')),
        onClose ? h('button', { type: 'button', className: 'btn btn-ghost btn-sm', onClick: onClose,
          'aria-label': tx('common.close', null, 'Close') }, tx('common.close', null, 'Close')) : null),

      data && data.purchase_enabled === false
        ? h('div', { className: 'box warn', role: 'status', style: { marginBlockEnd: 12, fontSize: '.8rem' } },
            tx('billing.purchase_paused', null, 'New purchases are paused right now. Your existing subscription is unaffected.'))
        : null,

      IntervalToggle, ByokToggle,

      err ? h('div', { role: 'alert', 'aria-live': 'assertive',
        style: { marginBlockEnd: 12, padding: '8px 10px', borderRadius: 6, fontSize: '.8rem',
                 background: 'var(--red-dim)', color: 'var(--red)', border: '1px solid var(--red)' } }, err) : null,

      shown.length === 0
        ? h('div', { style: { color: 'var(--dim)', fontSize: '.82rem' } },
            tx('billing.no_plans', null, 'No plans are available for that combination right now.'))
        : h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: 12 } },
            shown.map(function (p) {
              var isCurrent = data.current_price_id === p.price_id;
              var L = p.limits_summary || {};
              return h('div', { key: p.price_id, className: 'card',
                style: { padding: 14, borderInlineStart: isCurrent ? '3px solid var(--beam)' : '3px solid transparent' } },
                h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBlockEnd: 4 } },
                  h('span', { style: { fontWeight: 700, fontSize: '.95rem', color: 'var(--text)' } }, p.plan_display),
                  /* Text badge, not a colour cue — WCAG 2.1 AA 1.4.1 */
                  isCurrent ? h('span', { className: 'tag ok', style: { fontSize: '.62rem' } }, tx('billing.current', null, 'CURRENT')) : null),
                h('div', { style: { fontFamily: 'var(--font-mono)', fontSize: '1.15rem', color: 'var(--text)', marginBlockEnd: 8 } },
                  money(p.unit_amount, p.currency),
                  h('span', { style: { fontSize: '.72rem', color: 'var(--dim)' } },
                    p.interval === 'year' ? tx('billing.per_year', null, '/yr') : tx('billing.per_month', null, '/mo'))),
                h('ul', { style: { margin: 0, marginBlockEnd: 12, paddingInlineStart: 16, fontSize: '.74rem', color: 'var(--dim)' } },
                  [ L.sites_max != null ? tx('billing.f_sites', { n: L.sites_max }, L.sites_max + ' sites') : null,
                    L.scans_per_month != null ? tx('billing.f_scans', { n: L.scans_per_month }, 'up to ' + L.scans_per_month + ' scans a month') : null,
                    L.ai_invocations_per_month != null ? tx('billing.f_ai', { n: L.ai_invocations_per_month }, L.ai_invocations_per_month + ' AI actions a month') : null,
                    L.migrations_per_month != null ? tx('billing.f_mig', { n: L.migrations_per_month }, L.migrations_per_month + ' migrations a month') : null,
                  ].filter(Boolean).map(function (t, i) { return h('li', { key: i }, t); })),
                isCurrent
                  ? h('button', { type: 'button', className: 'btn btn-ghost btn-sm', disabled: true }, tx('billing.current_plan', null, 'Your plan'))
                  : h('button', {
                      type: 'button', className: 'btn btn-primary btn-sm',
                      disabled: !!busy || data.purchase_enabled === false,
                      'aria-label': tx('billing.select_aria', { plan: p.plan_display }, 'Select the ' + p.plan_display + ' plan'),
                      onClick: function () { return data.has_subscription ? loadPreview(p) : startCheckout(p); },
                    }, busy === p.price_id || busy === ('preview:' + p.price_id)
                        ? tx('common.working', null, 'Working…')
                        : (data.has_subscription ? tx('billing.change_to', null, 'Change to this') : tx('billing.subscribe', null, 'Subscribe')))
              );
            })),

      !data.has_subscription ? h('div', { style: { marginBlockStart: 16 } },
        h('label', { htmlFor: 'wpsb-promo', style: { display: 'block', fontSize: '.72rem', color: 'var(--dim)', marginBlockEnd: 4 } },
          tx('billing.promo_label', null, 'Promotion code (optional)')),
        h('input', { id: 'wpsb-promo', value: promo, onChange: function (e) { setPromo(e.target.value); },
          style: { inlineSize: '100%', maxInlineSize: 260, padding: '6px 8px', background: 'var(--surface-2)',
                   color: 'var(--text)', border: '1px solid var(--border)', borderRadius: 6, fontSize: '.82rem' } }),
        h('p', { style: { margin: '6px 0 0', fontSize: '.7rem', color: 'var(--dim)' } },
          tx('billing.promo_help', null, 'Codes apply to either monthly or annual plans, not both.'))
      ) : null,

      preview ? h('div', { className: 'card', role: 'region', 'aria-live': 'polite',
        style: { marginBlockStart: 16, padding: 14, background: 'var(--surface-2)' } },
        h('div', { style: { fontWeight: 700, fontSize: '.86rem', color: 'var(--text)', marginBlockEnd: 6 } },
          tx('billing.preview_title', { plan: preview.plan.plan_display }, 'Change to ' + preview.plan.plan_display)),
        preview.data && preview.data.has_upcoming
          ? h('div', { style: { fontSize: '.8rem', color: 'var(--dim)', marginBlockEnd: 10 } },
              tx('billing.preview_amount', { amount: money(preview.data.amount_due, preview.data.currency) },
                 'Your next invoice would be ' + money(preview.data.amount_due, preview.data.currency) + ', including proration.'))
          : h('div', { style: { fontSize: '.8rem', color: 'var(--dim)', marginBlockEnd: 10 } },
              tx('billing.preview_unavailable', null, 'We could not preview the prorated amount. You can still continue; Stripe will show the exact figure.')),
        h('div', { style: { display: 'flex', gap: 8 } },
          h('button', { type: 'button', className: 'btn btn-primary btn-sm', disabled: busy === 'change', onClick: confirmChange },
            busy === 'change' ? tx('common.working', null, 'Working…') : tx('billing.confirm_change', null, 'Confirm change')),
          h('button', { type: 'button', className: 'btn btn-ghost btn-sm', onClick: function () { setPreview(null); } },
            tx('common.cancel', null, 'Cancel')))
      ) : null
    );
  }

  /* ── P3 · BillingReturnHandler — reads the query string the server builds. ──
     ?billing=success&session_id=… → confirm via GET /billing/checkout/session/:id
     ?billing=cancelled | portal   → a short status line
     ?billing=plans                → open the picker (THE ONE deep link the plugin uses, B35)
     ⭐ THE URL IS CLEANED AFTER READING, with replaceState: a session id left in the
     address bar travels into history, bookmarks and referrer headers. */
  function BillingReturnHandler(props) {
    var s1 = useState(null); var msg = s1[0], setMsg = s1[1];
    useEffect(function () {
      var q;
      try { q = new URLSearchParams(window.location.search); } catch (e) { return; }
      var mode = q.get('billing');
      if (!mode) return;
      var sid = q.get('session_id');
      try {
        q.delete('billing'); q.delete('session_id'); q.delete('upsell');
        var rest = q.toString();
        window.history.replaceState({}, '', window.location.pathname + (rest ? '?' + rest : ''));
      } catch (e) {}

      if (mode === 'plans') { if (props && props.onOpenPicker) props.onOpenPicker(); return; }
      if (mode === 'cancelled') { setMsg({ tone: 'info', text: tx('billing.checkout_cancelled', null, 'Checkout was cancelled. Nothing was charged.') }); return; }
      /* ── K3 — A PORTAL RETURN REFRESHES BILLING STATE, NOT JUST ENTITLEMENTS ──
         This reloaded entitlements only. But a customer in the Stripe portal most often
         changes their CARD, which does not touch entitlements at all — so the copy said
         "Any changes you made in Stripe are now in effect" while the Payment-methods card
         beside it still showed the card they had just replaced. ⭐ The sentence was the
         defect: it ASSERTED freshness the page had not fetched. Both are refreshed now. */
      if (mode === 'portal')    { if (window.WPSBEntitlements) window.WPSBEntitlements.load(true); if (window.WPSB && window.WPSB.BILLING) window.WPSB.BILLING.requestRefresh(); setMsg({ tone: 'info', text: tx('billing.portal_returned', null, 'Welcome back. Any changes you made in Stripe are now in effect.') }); return; }
      if (mode !== 'success') return;

      if (!sid) { setMsg({ tone: 'info', text: tx('billing.success_no_session', null, 'Payment complete. Your plan will appear here shortly.') }); return; }
      setMsg({ tone: 'info', text: tx('billing.confirming', null, 'Confirming your payment…') });
      api('/billing/checkout/session/' + encodeURIComponent(sid))
        .then(function (out) {
          if (window.WPSBEntitlements) window.WPSBEntitlements.load(true);
          setMsg(out && out.fulfilled
            ? { tone: 'ok', text: tx('billing.success_fulfilled', { plan: out.plan || '' }, 'Payment confirmed. Your plan is now ' + (out.plan || 'active') + '.') }
            : { tone: 'info', text: tx('billing.success_pending', null, 'Payment received. We are still setting up your plan — this page will be accurate in a moment.') });
        })
        .catch(function (e) {
          /* ⛔ NEVER RENDER A FAILED CONFIRMATION AS A FAILED PAYMENT. The money moved;
             what did not complete is our provisioning, and the webhook is the second
             path that will finish it. */
          setMsg({ tone: 'info', text: e && e.code === 'FULFIL_RETRY'
            ? tx('billing.fulfil_retry', null, 'Your payment went through. We are still finishing setup — refresh in a moment; nothing further is needed from you.')
            : tx('billing.success_pending', null, 'Payment received. We are still setting up your plan — this page will be accurate in a moment.') });
        });
    }, []);
    if (!msg) return null;
    return h('div', {
      role: 'status', 'aria-live': 'polite',
      className: 'box ' + (msg.tone === 'ok' ? 'ok' : 'info'),
      style: { marginBlockEnd: 16, fontSize: '.84rem' },
    }, msg.text);
  }

  window.PlanPicker           = PlanPicker;
  window.BillingReturnHandler = BillingReturnHandler;
  window.wpsbOpenPlanPicker   = function () { window.dispatchEvent(new CustomEvent('wpsb:open-plan-picker')); };
  console.log('[WPSB] PlanPicker loaded');
})();
