/* Explain.jsx — single dual-mode explainer primitive
   Desktop (hover-capable pointer): mouseenter shows, mouseleave hides.
   Mobile / touch: tap toggles popover; × button, Escape, click-outside, scroll all dismiss.
   Content lives in EXPLAIN registry below as plain text — no JSX inside strings.
*/

const EXPLAIN = {
  /* ──── Framing ──── */
  'forward-modelling': {
    title: 'Forward modelling',
    body: 'Given a portfolio and a hazard set, compute the expected loss distribution. The mature commercial discipline — every cat-modelling vendor does this. The output is a probability curve.',
    note: 'Clinical analogue: given the lesion, predict the symptoms.',
  },
  'inverse-design': {
    title: 'Inverse design',
    body: 'Given a desired outcome, find the boundary configuration that produces it. Standard in optical engineering, fluid dynamics, and drug discovery. Almost nobody offers it commercially in insurance.',
    note: 'Clinical analogue: given the clinical picture, find the lesion that would have produced it.',
  },
  'boundary': {
    title: 'Boundary configuration',
    body: 'In topology, ∂ denotes the boundary of a region. Here, ∂ is the boundary of risk retention — where the insured stops absorbing loss and a counterparty (treaty, reinsurer, capital market) starts. Designing the boundary is the core actuarial decision.',
  },
  'defensible-surface': {
    title: 'Defensible analysis surface',
    body: 'The output is not a recommendation — it is a structured object an underwriter and the appointed actuary can interrogate, perturb, and sign. The system shows its working. Auditable from any angle.',
    note: 'Why investors should care: the regulatory moat. RBNZ and the appointed actuary can both reject opaque optimisation; a defensible surface survives that scrutiny.',
  },

  /* ──── Incumbents ──── */
  'verisk': {
    title: 'Verisk (AIR Worldwide)',
    body: 'Largest commercial cat-modelling vendor; AIR Worldwide is its catastrophe-modelling subsidiary. Used industry-wide for hurricane, earthquake, terror loss simulations. Forward-modelling pipeline; not architected for inverse problems.',
  },
  'rms': {
    title: 'RMS (Moody\'s)',
    body: 'Risk Management Solutions, acquired by Moody\'s in 2021. Its RiskLink platform is the other dominant cat model. Excellent at probabilistic loss curves; same forward-modelling paradigm.',
  },
  'moodys': {
    title: 'Moody\'s Analytics',
    body: 'Credit-rating-led analytics group. Owns RMS post-2021. Strong on counterparty and credit-correlated risk; cat-modelling integrated under the RMS franchise.',
  },
  'milliman': {
    title: 'Milliman',
    body: 'Largest independent global actuarial consultancy. Builds bespoke loss models per client engagement. Forward-modelling craft sold as services, not productised inverse design.',
  },
  'willis-re': {
    title: 'Willis Re (now WTW)',
    body: 'Reinsurance broker and analytics arm of Willis Towers Watson. Places treaty programmes; runs portfolio analytics for clients. Forward-modelling-driven; the inverse problem is left to in-house actuarial judgment.',
  },

  /* ──── Equation symbols ──── */
  'boundary-star': {
    title: '∂* — the optimal boundary',
    body: 'The output of the optimisation. The asterisk denotes the argmin solution: the specific allocation across primary retention, treaties, cat reinsurance, parametric triggers and sidecar capital that minimises distance to the target outcome.',
  },
  'argmin': {
    title: 'argmin — the operator',
    body: 'Returns the configuration ∂ that minimises the expression in braces. Not a value, but a search: across all feasible boundary configurations, find the one that lands closest to B*, penalised by R(∂).',
  },
  'phi': {
    title: 'Φ — hazard frequency field',
    body: 'The probability density of hazard occurrence over the insured surface. Earthquake seismicity grids, cyclone tracks, supply-chain disruption rates. Sourced from cat models, regulators, and bespoke NZ work.',
  },
  'lambda': {
    title: 'λ — vulnerability function',
    body: 'How a given hazard intensity translates into loss for a given asset class. Damage functions per construction type, business-interruption curves per sector, mortality curves per age band.',
  },
  's-coverage': {
    title: 'S — coverage stack',
    body: 'The current shape of risk transfer: who pays first, who pays in the tail, what is excluded. The integrand the optimiser is allowed to redesign.',
  },
  'dA': {
    title: 'dA — surface element',
    body: 'Integration over the insured surface. Geographic (AVF zone, Hawkes Bay flood plain), exposure-class (dairy, regional bank loan books), or time-window. The differential keeps the integral dimensionally honest.',
  },
  'b-star': {
    title: 'B* — the target risk profile',
    body: 'The desired outcome the underwriter wants to land on: a target premium reduction, a target capital efficiency, a tail-risk tolerance, a regulatory-headroom buffer. The four sliders in this panel set B*.',
  },
  'r-of-boundary': {
    title: 'R(∂) — regularisation',
    body: 'The penalty term. Captures regulatory floors (RBNZ solvency, IFRS 17 risk adjustment, XRB climate disclosure), physical constraints (treaty capacity ceilings), and constitutional ethics (no orphaning of cover). Without R(∂) the optimiser would happily produce mathematically optimal but unshippable answers.',
    note: 'This is the load-bearing innovation. Without R(∂) the boundary is just an optimisation; with it, the boundary is governable.',
  },

  /* ──── Target dials ──── */
  'premium-reduction': {
    title: 'Premium reduction (target)',
    body: 'Percentage reduction the underwriter wants to achieve against the current cost-of-cover baseline. Dial 0% if the goal is structural improvement at constant cost; dial 25%+ if the goal is aggressive savings against the prior treaty year.',
  },
  'capital-efficiency': {
    title: 'Capital efficiency',
    body: 'Return on capital employed (ROCE) on the risk the insurer chooses to retain. Higher = same risk held against less capital, freeing balance sheet for growth. Bounded above by RBNZ solvency margin.',
  },
  'tail-risk-tolerance': {
    title: 'Tail-risk tolerance',
    body: '0 = retain almost no tail (transfer everything beyond the working layer); 1 = retain the entire portfolio tail. Real underwriters live in the 0.3 — 0.6 band. Above 0.6 the regulator notices.',
  },
  'regulatory-headroom': {
    title: 'Regulatory headroom',
    body: 'Distance from the floors set by RBNZ solvency margin and IFRS 17 risk adjustment. 0 = sitting on the floor (regulator concern); 1 = comfortably above. Most NZ insurers operate at 0.5 — 0.7.',
  },

  /* ──── Coverage stack components ──── */
  'primary-retention': {
    title: 'Primary retention',
    body: 'The slice of every loss the insurer pays first, before any treaty responds. Higher retention = lower premium ceded but more capital tied up. The optimiser trades retention against the rest of the stack.',
  },
  'quota-share': {
    title: 'Quota-share treaty',
    body: 'A reinsurer takes a fixed percentage of every premium and every loss. Capital relief comes proportionally. Typically priced via a ceding commission. Simplest treaty form.',
  },
  'xol': {
    title: 'Excess-of-loss layers',
    body: 'Reinsurance that responds only above an attachment point and up to a limit — e.g. "$50M xs $10M". Stack multiple layers to build a tower. Cheap per-dollar of cover relative to QS, but only at attachment.',
  },
  'cat-re': {
    title: 'Cat reinsurance',
    body: 'Specialist excess-of-loss programmes for natural catastrophe perils — earthquake, cyclone, flood. Priced off cat-model output curves. The most volatile line of the global reinsurance market.',
  },
  'parametric': {
    title: 'Parametric trigger',
    body: 'Pays out a fixed amount when a measurable index crosses a threshold — wind speed, ground-motion intensity, river height, days closed at port. No loss adjustment, fast cash, basis risk to manage. The growth frontier.',
    note: 'Why investors should care: parametric is where insurance and capital markets are converging. Iwi pools and infrastructure funds buy parametric naturally; vendor cat models do not design the trigger curves well.',
  },
  'alt-capital': {
    title: 'Alt-capital sidecar',
    body: 'A special-purpose vehicle backed by hedge-fund or pension capital that takes a slice of a treaty alongside traditional reinsurers. Brings non-correlated capacity in soft markets; vanishes in hard markets. Pricing tells you where the market is.',
  },

  /* ──── Regularisation constraints ──── */
  'rbnz-solvency': {
    title: 'RBNZ solvency margin',
    body: 'Reserve Bank of New Zealand sets a solvency-margin floor under the Insurance (Prudential Supervision) Act 2010. Capital at risk for unexpected losses must exceed the floor. The hardest of the five constraints — breach triggers regulatory action.',
  },
  'ifrs-17': {
    title: 'IFRS 17 risk-adjustment',
    body: 'Replaced IFRS 4 in 2023. Insurers now disclose the confidence level on their risk-adjustment for non-financial risk (e.g. "75th percentile"). Inverse design produces a defensible methodology for that confidence number.',
  },
  'xrb-climate': {
    title: 'XRB climate disclosure',
    body: 'External Reporting Board\'s NZ Climate Standards (NZ CS 1/2/3). Mandatory for ~200 climate-reporting entities, FY 2024 onwards. Forces explicit disclosure of climate-related risks, scenarios, and metrics.',
  },
  'treaty-capacity': {
    title: 'Treaty capacity ceiling',
    body: 'Each treaty placement has a hard limit on how much risk the reinsurance market is willing to take. The optimiser cannot demand more capacity than the global reinsurance balance sheet provides for that peril.',
  },
  'constitutional-ethics': {
    title: 'No orphaning of cover',
    body: 'A boundary configuration that abandons a class of insureds — small farmers, marae, low-income suburbs — to game the loss curve is rejected outright, regardless of how good the math is. This is a hard ethical floor, not a soft preference.',
    note: 'The Brahmacharya constraint, in plain English. Built in because Patrick will ask: "what stops your optimiser from producing predatory boundaries?" — this is the answer.',
  },

  /* ──── Audit trail ──── */
  'audit-trail': {
    title: 'Audit trail',
    body: 'Every iteration of the optimiser — its proposed configuration, its score against R(∂), the reason any candidate was rejected — is logged and cryptographically signed. The appointed actuary can replay the run, change a constraint, see what would have happened. Nothing is hidden.',
  },

  /* ──── Optimiser concepts (real engine) ──── */
  'non-uniqueness': {
    title: 'Non-uniqueness',
    body: 'Inverse problems generally do not have a single solution. Multiple distinct boundary configurations can satisfy the same target B* and the same regularisation R(∂). Each is mathematically valid; choice between them is judgment that lives outside the optimiser.',
    note: 'Why this matters for an investor: the moat is partly in surfacing the alternatives. A forward model produces one number; an inverse model produces a set, and the underwriter\'s craft is choosing within it.',
  },
  'projected-gradient': {
    title: 'Projected gradient descent',
    body: 'A first-order optimisation method. At each step: compute the gradient of the loss; take a step against it; project the result back onto the feasible region (here the 6-simplex: allocations sum to 1, all non-negative). The "projection" is the discipline that keeps the configuration legal at every iteration.',
    note: 'Cheap, transparent, debuggable. ~30ms per run client-side. Good enough for a demo; real deployments would use L-BFGS or interior-point methods on the same loss surface.',
  },
  'calibration-status': {
    title: 'Calibration status',
    body: 'Every coefficient in this document carries one of three labels. CALIBRATED means sourced from a published reference or live data feed. ASSERTED means a reasoned assumption with explicit justification. PROVISIONAL means a placeholder named as a gap, awaiting calibration.',
    note: 'Naming the gap is the discipline. A model whose coefficients are all "CALIBRATED" is either lucky or lying. The proportion in each column is the honest measure of where the work still needs to happen.',
  },

  /* ──── Tempos section ──── */
  'physical-tempo': {
    title: 'Physical-supply tempo',
    body: 'Tankers, pipelines, terminal stocks, refinery throughput. Days-to-weeks resolution. The tempo public discourse focuses on: pump prices, queues, shortage-watch.',
  },
  'community-tempo': {
    title: 'Community / lateral tempo',
    body: 'Marae clusters, iwi pooling, rural ride-shares, volunteer logistics. Hours-to-days resolution. Activates faster than government, slower than the financial-instrument tempo. Largely invisible in policy modelling.',
  },
  'financial-tempo': {
    title: 'Financial-instrument tempo',
    body: 'Forward derivatives, FX hedges, marine cargo cover, war-risk insurance, trade credit, business interruption. Re-prices in quarters, but already deployed before the shock. Absorbs the shock invisibly. The silent tempo.',
    note: 'This is the load-bearing observation: pump prices fell during a supply crisis because financial instruments paid the bill in dollars before the physical tempo could pay it in barrels.',
  },
  'govt-tempo': {
    title: 'Government policy tempo',
    body: 'Ministerial statements → Cabinet papers → Select Committee → readings → Royal Assent. Months-to-years resolution. Cannot move at shock speed; mistaking the gap for negligence misses the structural reality.',
  },
  'tempo-gap': {
    title: 'Tempo gap',
    body: 'Ratio of physical-supply events resolved per government-policy event at the same scrub position. A live measure of how far ahead the market has moved by the time policy can respond.',
  },

  /* ──── Pump absorption ──── */
  'hedge-layer': {
    title: 'Forward derivatives & FX hedges',
    body: 'Singapore Mogas 95 / Gasoil swaps and NZD/USD forwards locked months before the shock. The largest invisible absorber — typically 40–60% of the price-shock damping during a 2-month crude spike.',
  },
  'cargo-cover': {
    title: 'Marine cargo + war-risk',
    body: 'When the JWC (Joint War Committee) reclassifies a zone, the surcharge spikes — but cover does not fail. Premium movement, not coverage failure, is the pricing signal that reaches consumers.',
  },
  'trade-credit': {
    title: 'Trade credit insurance',
    body: 'Atradius, Coface, Euler Hermes. If a counterparty fails to pay during the shock, the insurer pays. Without it, primary fuel importers would seize up under counterparty risk before any physical shortage hit.',
  },
  'pump-rolling': {
    title: 'Rolling-weighted pump price',
    body: 'The price at the bowser is a weighted average across multiple supply tranches at different forward prices, not the spot price of the marginal barrel. By design, it lags and dampens the shock.',
  },

  /* ──── Pressures ──── */
  'xrb-pressure': {
    title: 'NZ Climate Standards (XRB)',
    body: 'NZ CS 1/2/3 apply to ~200 climate-reporting entities for FY 2024 onwards. Many disclosures are vendor-derived and not fully understood internally. A liability surface waiting to be tested.',
  },
  'avf-pressure': {
    title: 'Auckland Volcanic Field exposure',
    body: 'Insurers and reinsurers know the exposure exists. Defensible portfolio-level coverage modelling for AVF is rare. Vendor cat models exist but are general — not built around basaltic monogenetic vent statistics.',
  },
  'marsden-pressure': {
    title: 'Marsden Point closure',
    body: 'Three years of stranded fuel-import-terminal exposure. Reinsurers had to reprice. Some treaty wordings shifted materially. The Hormuz event is the second-order test of those reprices.',
  },

  /* ──── Cascade ──── */
  'vendor-model': {
    title: 'Vendor model view',
    body: 'What Verisk / RMS-style global cat models surface for an NZ portfolio: mostly perils-as-modelled-in-California-or-Florida, recalibrated. Sectoral correlation under NZ-specific cascades is largely missing from the output.',
  },
  'cascade-correlation': {
    title: 'NZ cascade correlation',
    body: 'A single MPI biosecurity event simultaneously hits dairy, regional bank loan books, and rural insurance lines. A Wellington fault rupture compromises infrastructure, transport, and Crown property in one move. Vendor models miss this; the gap is the IP.',
  },
  'hidden-cascade': {
    title: 'Hidden cascade',
    body: 'The dollar gap between vendor-model expected loss and NZ-cascade expected loss. The number reinsurers should be pricing into NZ treaties but typically are not.',
  },

  /* ──── Offerings ──── */
  'vulnerability-scan': {
    title: 'Vulnerability Scan-as-a-Service',
    body: 'A 90-minute analytical service, white-labelled for brokerages. Surfaces coverage gaps, redundancies, mispriced exposures, tempo-mismatch risks. The broker keeps the client relationship; we provide the analytical engine.',
    note: 'Warmest market entry. Three free pilots in exchange for case studies.',
  },
  'underwriter-copilot': {
    title: 'Underwriter co-pilot',
    body: 'Five-persona Council reviews every commercial-lines decision above $5M sum-insured before it is bound. Surfaces tail-risk and accumulation flags that a single reviewer might miss. Threshold-gated; underwriter retains override.',
  },
  'cascade-library': {
    title: 'NZ cascade-scenario library',
    body: 'Canonical, regularly updated, defensible. Public-good summary scenarios; underlying modelling sold to reinsurers and consultancies. Pathway to regulatory recognition with XRB and RBNZ. Slow to procure, but durable.',
  },
  'iwi-parametric': {
    title: 'Iwi parametric trigger design',
    body: 'Inverse-designed parametric triggers for iwi pools — rainfall, seismic, supply-chain, biosecurity — calibrated against historical claims and forward scenarios. Long sales cycle, capital-intensive, mission-critical.',
  },
};

function Explain({ term, children }) {
  const anchorRef = React.useRef(null);
  const popoverRef = React.useRef(null);
  const hideTimerRef = React.useRef(null);
  const [open, setOpen] = React.useState(false);
  const [pos, setPos] = React.useState({ top: 0, left: 0, placement: 'top', width: 320, dark: false });
  const isTouchRef = React.useRef(false);

  React.useEffect(() => {
    const mq = window.matchMedia('(hover: hover) and (pointer: fine)');
    isTouchRef.current = !mq.matches;
    return () => {
      if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
    };
  }, []);

  const entry = EXPLAIN[term];
  if (!entry) {
    if (typeof console !== 'undefined') console.warn('[Explain] missing term:', term);
    return <span>{children}</span>;
  }

  const computePos = () => {
    const el = anchorRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    const W = Math.min(320, vw - 24);
    const H = 220; // conservative estimate
    const spaceAbove = r.top;
    const spaceBelow = vh - r.bottom;
    // Prefer the side with more clearance; require at least 80px before falling back
    const placement = (spaceAbove >= 80 && spaceAbove >= spaceBelow)
                   || (spaceBelow < 80 && spaceAbove >= spaceBelow)
                    ? 'top' : 'bottom';
    let left = r.left + r.width / 2 - W / 2;
    left = Math.max(12, Math.min(left, vw - W - 12));
    // Compute top edge clamped so the popover never overflows the viewport.
    // For 'top' placement the popover grows upward from anchor → top = r.top - 10 - height,
    // but we anchor via `bottom` CSS so the popover top is naturally clamped by maxHeight.
    // For 'bottom' we cap top to vh - 12 - estimated height.
    const top = placement === 'top'
      ? Math.max(12, r.top - 10)
      : Math.max(12, Math.min(vh - H - 12, r.bottom + 10));
    // Detect dark-section context by walking up the DOM
    let dark = false;
    let p = el;
    while (p) {
      if (p.id === 'inverse' || (p.classList && p.classList.contains('dark-section'))) { dark = true; break; }
      p = p.parentElement;
    }
    setPos({ top, left, placement, width: W, dark });
  };

  const cancelHide = () => {
    if (hideTimerRef.current) {
      clearTimeout(hideTimerRef.current);
      hideTimerRef.current = null;
    }
  };
  const scheduleHide = (delay) => {
    cancelHide();
    hideTimerRef.current = setTimeout(() => {
      hideTimerRef.current = null;
      setOpen(false);
    }, delay == null ? 220 : delay);
  };

  const show = () => {
    cancelHide();
    if (!open) computePos();
    setOpen(true);
  };
  const hideNow = () => {
    cancelHide();
    setOpen(false);
  };
  const toggle = (e) => {
    if (e) { e.stopPropagation(); }
    if (open) hideNow();
    else show();
  };

  // Recompute position when transitioning from closed -> open
  React.useEffect(() => {
    if (open) computePos();
  }, [open]);

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') hideNow(); };
    const onDown = (e) => {
      const a = anchorRef.current;
      const pop = popoverRef.current;
      if (a && a.contains(e.target)) return;
      if (pop && pop.contains(e.target)) return;
      hideNow();
    };
    const onScroll = () => hideNow();
    document.addEventListener('keydown', onKey);
    document.addEventListener('mousedown', onDown);
    document.addEventListener('touchstart', onDown);
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onScroll);
    return () => {
      document.removeEventListener('keydown', onKey);
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('touchstart', onDown);
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', onScroll);
    };
  }, [open]);

  // Pointer-fine devices: hover shows + click toggles. Hide is grace-delayed
  // so the cursor can travel from anchor to popover without the popover
  // being torn down mid-traverse.
  const desktopProps = {
    onMouseEnter: show,
    onMouseLeave: () => scheduleHide(220),
    onClick: toggle,
    onFocus: show,
    onBlur: () => scheduleHide(220),
  };
  const touchProps = {
    onClick: toggle,
  };
  const handlers = isTouchRef.current ? touchProps : desktopProps;

  // Popover catches mouseenter/leave so the user can read it without the
  // anchor's leave timer killing it.
  const popoverHover = isTouchRef.current ? {} : {
    onMouseEnter: cancelHide,
    onMouseLeave: () => scheduleHide(220),
  };

  const popover = open ? (
    <div ref={popoverRef}
         className={'explain-popover explain-place-' + pos.placement + (pos.dark ? ' in-dark' : '')}
         role="tooltip"
         style={{
           position: 'fixed',
           top: pos.placement === 'top' ? 'auto' : pos.top,
           bottom: pos.placement === 'top' ? Math.max(12, window.innerHeight - pos.top) : 'auto',
           left: pos.left,
           width: pos.width,
           maxHeight: 'calc(100vh - 24px)',
           overflowY: 'auto',
         }}
         {...popoverHover}
         onClick={(e) => e.stopPropagation()}>
      <button className="explain-close" aria-label="Close explainer" onClick={hideNow}>×</button>
      <div className="explain-title">{entry.title}</div>
      <div className="explain-body">{entry.body}</div>
      {entry.note ? <div className="explain-note">{entry.note}</div> : null}
    </div>
  ) : null;

  return (
    <React.Fragment>
      <span ref={anchorRef}
            className="explain-anchor"
            tabIndex={0}
            role="button"
            aria-label={'Explain: ' + entry.title}
            aria-expanded={open}
            onKeyDown={(e) => {
              if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e); }
            }}
            {...handlers}>
        {children}
      </span>
      {popover && ReactDOM.createPortal ? ReactDOM.createPortal(popover, document.body) : popover}
    </React.Fragment>
  );
}

window.Explain = Explain;
window.EXPLAIN = EXPLAIN;
