// ===== Main app: stage machine + layout =====

const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ---- Canvas derivations: keyed on the actual answers, not just count ----
function computeProposed(riskAnswer, liqItems, usdCeil) {
  let a = { us_eq: 42, intl_eq: 18, em_eq: 8, ig_bond: 22, cash: 10, reserve: 0 };

  if (riskAnswer === 'more-cons') {
    a = { us_eq: 32, intl_eq: 16, em_eq: 5, ig_bond: 33, cash: 14, reserve: 0 };
  } else if (riskAnswer === 'same') {
    a = { us_eq: 38, intl_eq: 18, em_eq: 8, ig_bond: 26, cash: 10, reserve: 0 };
  } else if (riskAnswer === 'more-aggr') {
    a = { us_eq: 46, intl_eq: 20, em_eq: 11, ig_bond: 15, cash: 8, reserve: 0 };
  }

  const reserveSize = liqItems.reduce((acc, i) => {
    if (!i.on) return acc;
    if (i.id === 'mba') return acc + 6;
    if (i.id === 'property') return acc + 2.5;
    if (i.id === 'tax') return acc + 1;
    return acc;
  }, 0);
  if (reserveSize > 0) {
    a.reserve = reserveSize;
    const fromUs   = Math.min(a.us_eq,   reserveSize * 0.55);
    const fromCash = Math.min(a.cash,    reserveSize * 0.30);
    const fromBond = Math.min(a.ig_bond, reserveSize - fromUs - fromCash);
    a.us_eq   -= fromUs;
    a.cash    -= fromCash;
    a.ig_bond -= fromBond;
  }

  if (usdCeil === '20')      { a.us_eq -= 4; a.intl_eq += 3;   a.em_eq += 1; }
  else if (usdCeil === '30') { a.us_eq -= 2; a.intl_eq += 1.5; a.em_eq += 0.5; }
  else if (usdCeil === '40') { a.us_eq += 1; a.intl_eq -= 0.5; a.em_eq -= 0.5; }

  for (const k of Object.keys(a)) a[k] = Math.max(0, a[k]);
  const order = ['us_eq', 'intl_eq', 'em_eq', 'ig_bond', 'reserve', 'cash'];
  const total = order.reduce((s, k) => s + a[k], 0) || 1;
  return order
    .filter(k => a[k] > 0.05)
    .map(k => ({ id: k, pct: Math.round((a[k] * 100 / total) * 10) / 10 }));
}

function computeKpis(riskAnswer, liqItems, usdCeil) {
  let yld = 2.6, dd = 22, ret = 6.2, inc = 94;
  if (riskAnswer === 'more-cons')      { yld += 0.7; dd -= 9;  ret -= 1.0; inc += 26; }
  else if (riskAnswer === 'same')      { yld += 0.4; dd -= 4;  ret -= 0.4; inc += 15; }
  else if (riskAnswer === 'more-aggr') { yld -= 0.3; dd += 4;  ret += 0.7; inc -= 10; }
  if (liqItems.some(i => i.on)) {
    const onCount = liqItems.filter(i => i.on).length;
    yld += 0.15 * onCount; dd -= 1.5 * onCount; inc += 4 * onCount;
  }
  if (usdCeil === '20')      { dd -= 2; inc += 2; ret -= 0.1; }
  else if (usdCeil === '30') { dd -= 1; inc += 1; }
  else if (usdCeil === '40') { dd += 1; ret += 0.1; }
  return {
    yield:    yld.toFixed(1) + '%',
    drawdown: '-' + Math.max(8, Math.round(dd)) + '%',
    return:   ret.toFixed(1) + '%',
    income:   'S$' + Math.round(inc) + 'k/yr',
  };
}

// ---- Adaptive stage flow ----
// `next` is computed from the current state so conditional steps slot in
// only when they're triggered. This is how the demo bakes in the
// "guide depends on other guides" idea.
// Walk the active plan from the current step and return the next id
// the user should land on. Skips auto / skipped / untriggered conditional
// steps. `export` maps to 'review' (the stage id for the final screen).
function computeNextStage(current, planSteps, state) {
  if (current === 'discovery' && state.discoveryReturnTo) return state.discoveryReturnTo;
  const anchor = current === 'intro' ? 'open' : current === 'review' ? 'export' : current;
  const idx = planSteps.findIndex(p => p.id === anchor);
  for (let i = idx + 1; i < planSteps.length; i++) {
    const p = planSteps[i];
    if (p.kind === 'auto' || p.kind === 'skipped') continue;
    if (p.kind === 'conditional' && !(p.triggeredBy && p.triggeredBy(state))) continue;
    if (p.id === 'export') return 'review';
    return p.id;
  }
  return 'review';
}

function App() {
  // ----- core stage state -----
  const [stage, setStage] = useState('intro');
  const [briefText, setBriefText] = useState('');
  const [submittedBrief, setSubmittedBrief] = useState('');
  const [whyOpen, setWhyOpen] = useState(false);

  // ----- decisions -----
  const [riskAnswer, setRiskAnswer] = useState(null);
  const [stressAnswer, setStressAnswer] = useState(null);
  const [liqItems, setLiqItems] = useState([
    { id: 'mba',      name: 'Maya\'s MBA tuition',     window: '2026 → 2028', purpose: 'education',  amt: 'US$220k', on: true,  detected: true },
    { id: 'property', name: '2027 property completion',window: 'Q3 2027',     purpose: 'real estate',amt: 'S$80k',   on: false, detected: true },
    { id: 'tax',      name: 'Annual SG income tax',    window: 'Apr/yr',      purpose: 'tax',        amt: 'S$30k',   on: false, detected: false },
  ]);
  const [propertyAnswer, setPropertyAnswer] = useState(null);
  const [usdCeil, setUsdCeil] = useState(null);
  const [esgAnswer, setEsgAnswer] = useState(null);

  // ----- discovery as a side-quest -----
  const [discoveryVisited, setDiscoveryVisited] = useState(false);
  const [discoveryReturnTo, setDiscoveryReturnTo] = useState(null);

  // ----- which plan template is active. Picked when the brief is submitted. -----
  const [planId, setPlanId] = useState('tan-review');
  const activePlan = PLAN_TEMPLATES[planId] || PLAN_TEMPLATES['tan-review'];

  // ----- toast + impact tracking -----
  const [toast, setToast] = useState(null);
  // Map of stepId -> {ts, summary}. Cleared after a few seconds so the
  // "just modified by your prompt" badge feels live.
  const [modified, setModified] = useState({});
  function flagModified(entries) {
    // Persist modified flags so the user can see what their prompt touched
    // for as long as they're in the session. They clear on full restart.
    setModified(prev => {
      const next = { ...prev };
      for (const [id, summary] of Object.entries(entries)) {
        next[id] = { ts: Date.now(), summary };
      }
      return next;
    });
  }

  const propertyOn = liqItems.find(i => i.id === 'property')?.on || false;
  const flowState = { riskAnswer, liqItems, discoveryReturnTo };

  // ----- canvas derivations -----
  const completedDecisionCount =
    (riskAnswer ? 1 : 0) + (liqItems.some(i => i.on) ? 1 : 0) + (usdCeil ? 1 : 0);
  const proposed = useMemo(() => computeProposed(riskAnswer, liqItems, usdCeil), [riskAnswer, liqItems, usdCeil]);
  const kpis     = useMemo(() => computeKpis(riskAnswer, liqItems, usdCeil),     [riskAnswer, liqItems, usdCeil]);
  const prevRef = useRef({ kpis: null, sig: '' });
  const sig = `${riskAnswer}|${liqItems.map(i => (i.on ? i.id : '')).join(',')}|${usdCeil}`;
  const prevKpis = prevRef.current.sig !== sig ? prevRef.current.kpis : null;
  useEffect(() => { prevRef.current = { kpis, sig }; }, [sig]);

  // ----- per-step done state (used by plan tree) -----
  // At intro we explicitly suppress every ✓ — `mba.on` is true by default
  // (it's a detected reserve, not a user decision), and we don't want the
  // pre-brief plan tree showing a green checkmark on liquidity.
  const stepDone = stage === 'intro' ? {} : {
    open:      true,
    discovery: discoveryVisited,
    risk:      !!riskAnswer,
    stress:    !!stressAnswer,
    liquidity: liqItems.some(i => i.on),
    property:  !!propertyAnswer,
    currency:  !!usdCeil,
    esg:       !!esgAnswer,
    concentr:  false,
    modeling:  stage === 'review',
    narrative: stage === 'review',
    export:    false,
  };

  // map current stage → which plan-tree id is "active"
  const activePlanId = stage === 'intro' ? 'open' : stage === 'review' ? 'export' : stage;

  // ----- planView: one row per PLAN entry, with computed visual state -----
  // Each entry gets:
  //   state:     'active' | 'done' | 'unlocked' | 'locked' | 'auto' | 'skipped' | 'future'
  //   reachable: whether clicking it should navigate
  //   badge:     short tag shown next to the title
  const planView = useMemo(() => {
    return activePlan.map(p => {
      // resolve conditional visibility
      const triggered = p.kind === 'conditional' ? !!p.triggeredBy({ riskAnswer, liqItems }) : true;
      const isActive = p.id === activePlanId;
      const isDone = stepDone[p.id];

      let state = 'future';
      let reachable = false;
      let badge = null, badgeKind = '';

      if (isActive) {
        state = 'active'; reachable = true;
      } else if (p.kind === 'skipped') {
        state = 'skipped'; reachable = false; badge = 'n/a'; badgeKind = 'skipped';
      } else if (p.kind === 'auto') {
        state = 'auto'; reachable = false; badge = 'auto'; badgeKind = 'auto';
      } else if (p.kind === 'conditional' && !triggered) {
        state = 'locked'; reachable = false; badge = 'locked'; badgeKind = '';
      } else if (isDone) {
        state = 'done'; reachable = true; badge = '✓'; badgeKind = 'done';
      } else if (p.kind === 'conditional' && triggered) {
        state = 'unlocked'; reachable = stage !== 'intro'; badge = 'unlocked'; badgeKind = 'unlocked';
      } else if (p.kind === 'optional') {
        state = 'future'; reachable = stage !== 'intro'; badge = 'optional'; badgeKind = 'optional';
      } else {
        state = 'future';
        // future required step is reachable if intro has been left
        reachable = stage !== 'intro';
      }

      // 'open' is always anchored — never navigable
      if (p.id === 'open') reachable = false;

      return { ...p, state, reachable, badge, badgeKind, modified: !!modified[p.id], modifiedSummary: modified[p.id]?.summary };
    });
  }, [activePlan, activePlanId, stage, riskAnswer, liqItems, stressAnswer, propertyAnswer, usdCeil, discoveryVisited, modified]);

  // ----- handlers -----
  // Resolve a plan id from the brief text. Exact starter match wins;
  // otherwise we keyword-sniff; otherwise default to the Tan plan.
  function resolvePlanId(text) {
    const exact = STARTERS.find(s => s.prompt === text);
    if (exact) return exact.id;
    const lc = text.toLowerCase();
    if (lc.includes('onboard') || lc.includes('new mandate') || lc.includes('balanced proposal') || lc.includes('build a balanced')) return 'khoo-new';
    if (lc.includes('property sale') || lc.includes('top-up') || lc.includes('top up') || lc.includes('deploy') || lc.includes('lump-sum') || lc.includes('rebalance')) return 'goh-topup';
    if (lc.includes('estate') || lc.includes('pension lump') || lc.includes('dual-resident') || lc.includes('dual resident') || lc.includes('hk$')) return 'pereira-complex';
    return 'tan-review';
  }

  function submitBrief(text) {
    const t = text || briefText;
    if (!t.trim()) return;
    setSubmittedBrief(t);
    setPlanId(resolvePlanId(t));
    // After the brief is submitted, land on the pre-fill review so the
    // advisor sees what was already known before any decisions are asked.
    setDiscoveryReturnTo('risk');
    setStage('discovery');
  }

  function handleStarter(starter) {
    setBriefText(starter.prompt);
    setPlanId(starter.id);
  }

  function leaveDiscovery() {
    setDiscoveryVisited(true);
    const back = discoveryReturnTo || 'risk';
    setStage(back);
    setDiscoveryReturnTo(null);
    setWhyOpen(false);
  }

  function answerRisk(v)     { setRiskAnswer(v); if (v !== 'more-aggr') setStressAnswer(null); }
  function answerStress(v)   { setStressAnswer(v); }
  function answerProperty(v) { setPropertyAnswer(v); }
  function answerCurrency(v) { setUsdCeil(v); }
  function answerEsg(v)      { setEsgAnswer(v); }
  function toggleLiq(id) {
    setLiqItems(prev => prev.map(it => it.id === id ? { ...it, on: !it.on } : it));
    if (id === 'property') setPropertyAnswer(null); // re-ask if toggled
  }

  function goNext() {
    const next = computeNextStage(stage, activePlan, { riskAnswer, liqItems, discoveryReturnTo });
    setStage(next);
    setWhyOpen(false);
  }

  // Plan tree click — jump to any reachable step. Discovery from elsewhere
  // remembers where to send the user back to.
  function jumpTo(stepId) {
    if (stepId === 'export') { setStage('review'); setWhyOpen(false); return; }
    if (stepId === 'discovery') {
      // remember where we were so the side-quest can resume
      setDiscoveryReturnTo(stage === 'discovery' ? (discoveryReturnTo || 'risk') : stage);
      setStage('discovery');
      setWhyOpen(false);
      return;
    }
    setStage(stepId);
    setWhyOpen(false);
  }

  function restart() {
    setStage('intro');
    setBriefText('');
    setSubmittedBrief('');
    setRiskAnswer(null); setStressAnswer(null);
    setUsdCeil(null); setPropertyAnswer(null);
    setEsgAnswer(null);
    setLiqItems(prev => prev.map(it => ({ ...it, on: it.id === 'mba' })));
    setDiscoveryVisited(false); setDiscoveryReturnTo(null);
    setWhyOpen(false);
    // Everything that survives navigation needs an explicit reset here
    // so Restart returns the page to a true initial state.
    setPlanId('tan-review');
    setModified({});
    setCmdValue('');
    setToast(null);
  }

  const [cmdValue, setCmdValue] = useState('');

  // ----- NL command bar handler -----
  function handleCommand(cmd) {
    const t = cmd.toLowerCase();
    if (!t.trim()) return;
    if (t.includes('conservative') || t.includes('safer')) {
      setRiskAnswer('more-cons'); setUsdCeil('20'); setStressAnswer(null);
      flagModified({
        risk: 'Risk → More conservative than 2023',
        currency: 'USD ceiling tightened to 20%',
        stress: 'Step retired — no longer needed',
        modeling: 'Allocation re-modeled',
      });
      showToast({
        title: 'Re-routed to a more conservative plan',
        body: 'Risk → "More conservative than 2023". USD ceiling tightened to 20%. Re-running the model — canvas updated.',
        meta: '2 decisions overridden · stress-test step retired',
      });
      setStage('review');
    } else if (t.includes('aggressive') || t.includes('growth')) {
      setRiskAnswer('more-aggr'); setUsdCeil('40');
      flagModified({
        risk: 'Risk → More aggressive than 2023',
        currency: 'USD ceiling raised to 40%',
        stress: 'Step unlocked — confirm the worst case',
        modeling: 'Allocation re-modeled',
      });
      showToast({
        title: 'Re-routed to a more aggressive plan',
        body: 'Risk → "More aggressive than 2023". USD ceiling raised to 40%. I added a stress-test confirmation step — please review it.',
        meta: '2 decisions overridden · stress-test unlocked',
      });
      setStage('stress');
    } else if (t.includes('mba') || t.includes('tuition') || t.includes('property reserve') || t.includes('add property')) {
      setLiqItems(prev => prev.map(it => ({ ...it, on: it.id === 'mba' || it.id === 'property' })));
      setPropertyAnswer(null);
      flagModified({
        liquidity: 'Property reserve added (S$80k)',
        property: 'Step unlocked — confirm timing',
        modeling: 'Reserve sleeve resized',
      });
      showToast({
        title: 'Liquidity reserves updated',
        body: 'Added the 2027 property top-up alongside the MBA tuition. I unlocked a "property timing" question — please confirm.',
        meta: '1 reserve added · property step unlocked',
      });
      setStage('property');
    } else if (t.includes('retirement') || t.includes('age 65') || t.includes('age 64')) {
      flagModified({
        discovery: 'Retirement age updated to 65',
        risk: 'Risk envelope re-anchored to longer horizon',
        liquidity: 'Tuition window now 4 years out, not 2',
        modeling: 'Glide-path re-projected',
      });
      showToast({
        title: 'Retirement age updated',
        body: 'Pushed retirement to age 65. The longer horizon loosens the drawdown constraint and pushes the tuition window further out — 4 fields touched.',
        meta: '4 fields modified · see plan tree',
      });
    } else if (t.includes('esg') || t.includes('sustainable') || t.includes('green')) {
      flagModified({
        discovery: 'ESG preference flipped on',
        esg: 'Step un-skipped — screen now applies',
        modeling: 'Universe filtered to ESG-eligible',
      });
      showToast({
        title: 'ESG screen activated',
        body: 'Sarah opted in to a light ESG screen. The previously-skipped ESG step is back in the plan, and the modeling universe is filtered.',
        meta: '3 fields modified · 1 step un-skipped',
      });
    } else if (t.includes('discovery') || t.includes('pre-fill') || t.includes('prefill') || t.includes('what you know')) {
      setDiscoveryReturnTo(stage);
      setStage('discovery');
      showToast({
        title: 'Opening pre-fill review',
        body: 'Showing what I already pulled from CRM, custodian feed, and the doc vault. Use Resume when you\'re done.',
        meta: 'side-quest · main flow paused',
      });
    } else if (t.includes('skip') && t.includes('done')) {
      setRiskAnswer('same'); setUsdCeil('30');
      flagModified({
        risk: 'Risk → same as 2023 (your default)',
        currency: 'USD ceiling → 30% (your default)',
        liquidity: 'MBA reserve only (your default)',
        modeling: 'Drafting proposal…',
        narrative: 'Suitability narrative auto-drafted',
      });
      showToast({
        title: 'Accepting all my recommendations',
        body: 'Risk → same as 2023. USD ceiling → 30%. Liquidity → MBA reserve only. Drafting the proposal now.',
        meta: '3 decisions auto-accepted',
      });
      setStage('review');
    } else if (t.includes('back') || t.includes('start over') || t.includes('restart')) {
      restart();
      showToast({ title: 'Reset', body: 'Cleared the case. Ready for a new brief.', meta: '' });
    } else {
      showToast({
        title: 'Got it — adding to the brief',
        body: '"' + cmd + '" — I\'ll factor this into the modeling step. Continue with the current question.',
        meta: 'context updated · no decisions changed',
      });
    }
  }

  function showToast(t) { setToast(t); setTimeout(() => setToast(null), 4500); }

  // ----- answer pills (shown above the active card so prior answers are visible) -----
  const showRiskPill = riskAnswer && stage !== 'risk' && stage !== 'discovery' && stage !== 'intro';
  const showStressPill = stressAnswer && stage !== 'stress' && stage !== 'risk' && stage !== 'discovery' && stage !== 'intro';
  const showLiqPill = liqItems.some(i => i.on) && !['intro','discovery','risk','stress','liquidity'].includes(stage);
  const showPropertyPill = propertyAnswer && !['intro','discovery','risk','stress','liquidity','property'].includes(stage);
  const showCurrencyPill = usdCeil && !['intro','discovery','risk','stress','liquidity','property','currency'].includes(stage);

  const answerPills = [];
  if (showRiskPill) {
    const opt = QUESTIONS.risk.options.find(o => o.v === riskAnswer);
    answerPills.push({
      key: 'risk', label: 'Risk tolerance', value: opt.label,
      detail: QUESTIONS.risk.readouts[riskAnswer]?.split('.')[0] + '.',
    });
  }
  if (showStressPill) {
    const opt = QUESTIONS.stress.options.find(o => o.v === stressAnswer);
    answerPills.push({
      key: 'stress', label: 'Stress-test confirm', value: opt.label,
      detail: 'Conditional follow-up · seat-belt for the aggressive stance.',
    });
  }
  if (showLiqPill) {
    const onItems = liqItems.filter(i => i.on);
    answerPills.push({
      key: 'liquidity', label: 'Liquidity reserves',
      value: onItems.map(i => i.name).join(' · '),
      detail: `${onItems.length} reserve sleeve${onItems.length > 1 ? 's' : ''} carved`,
    });
  }
  if (showPropertyPill) {
    const opt = QUESTIONS.property.options.find(o => o.v === propertyAnswer);
    answerPills.push({
      key: 'property', label: 'Property timing', value: opt.label,
      detail: 'Sized the maturity ladder inside the reserve sleeve.',
    });
  }
  if (showCurrencyPill) {
    answerPills.push({
      key: 'currency', label: 'USD ceiling', value: `${usdCeil}% on liquid assets`,
      detail: `Down from current 58% USD-denominated`,
    });
  }

  // ----- render center stage -----
  let stageBody = null;
  if (stage === 'intro') {
    stageBody = (
      <IntroStage
        briefText={briefText}
        setBriefText={setBriefText}
        onSubmit={() => submitBrief()}
        onStarter={handleStarter}
      />
    );
  } else if (stage === 'discovery') {
    stageBody = (
      <DiscoveryStage
        brief={submittedBrief}
        sideQuest={!!discoveryReturnTo && discoveryVisited === false ? false : true}
        returnLabel={discoveryReturnTo || 'risk'}
        onResume={leaveDiscovery}
      />
    );
  } else if (stage === 'risk') {
    stageBody = (
      <QuestionStage
        eyebrow="Required decision · 1 of 3"
        title="Risk tolerance"
        sub="Sarah and Daniel last confirmed this in 2023. The slider below is anchored to today's S$ paper-loss numbers, not abstract 1–10."
        answerPills={answerPills} jumpTo={jumpTo} modified={modified}
        question={QUESTIONS.risk}
        whyOpen={whyOpen} setWhyOpen={setWhyOpen}
        canContinue={!!riskAnswer}
        onContinue={goNext}
        continueLabel="Continue"
      >
        <RiskSlider q={QUESTIONS.risk} value={riskAnswer} onChange={answerRisk} />
      </QuestionStage>
    );
  } else if (stage === 'stress') {
    stageBody = (
      <QuestionStage
        eyebrow="Conditional · unlocked by your risk choice"
        title="One last seat-belt before we go aggressive"
        sub="This step only appears when you push outside the prior risk band. Skip it once and the stance is on the record."
        answerPills={answerPills} jumpTo={jumpTo} modified={modified}
        question={QUESTIONS.stress}
        whyOpen={whyOpen} setWhyOpen={setWhyOpen}
        canContinue={!!stressAnswer}
        onContinue={goNext}
        continueLabel="Continue"
      >
        <ChipQuestion q={QUESTIONS.stress} value={stressAnswer} onChange={answerStress} />
      </QuestionStage>
    );
  } else if (stage === 'liquidity') {
    stageBody = (
      <QuestionStage
        eyebrow="Required decision · 2 of 3"
        title="Liquidity reserves"
        sub="I already pulled two known reserves from the doc vault and your brief. Toggle anything you don't need; toggling the property reserve unlocks a follow-up timing question."
        answerPills={answerPills} jumpTo={jumpTo} modified={modified}
        question={QUESTIONS.liquidity}
        whyOpen={whyOpen} setWhyOpen={setWhyOpen}
        canContinue={true}
        onContinue={goNext}
        continueLabel={propertyOn ? 'Confirm timing →' : 'Continue'}
      >
        <LiquidityWidget items={liqItems} onToggle={toggleLiq} onAdd={() => {}} />
      </QuestionStage>
    );
  } else if (stage === 'property') {
    stageBody = (
      <QuestionStage
        eyebrow="Conditional · unlocked by reserve toggle"
        title="When does the property top-up need to be liquid?"
        sub="This step only appears when the property reserve is toggled on. Default ladders to the doc-vault date."
        answerPills={answerPills} jumpTo={jumpTo} modified={modified}
        question={QUESTIONS.property}
        whyOpen={whyOpen} setWhyOpen={setWhyOpen}
        canContinue={!!propertyAnswer}
        onContinue={goNext}
        continueLabel="Continue"
      >
        <ChipQuestion q={QUESTIONS.property} value={propertyAnswer} onChange={answerProperty} />
      </QuestionStage>
    );
  } else if (stage === 'esg') {
    stageBody = (
      <QuestionStage
        eyebrow="Optional decision · un-skipped by your prompt"
        title="ESG screen"
        sub="You opted in to a light ESG screen in the brief. Pick the strength — the modeling universe filters accordingly."
        answerPills={answerPills} jumpTo={jumpTo} modified={modified}
        question={QUESTIONS.esg}
        whyOpen={whyOpen} setWhyOpen={setWhyOpen}
        canContinue={!!esgAnswer}
        onContinue={goNext}
        continueLabel="Continue"
      >
        <ChipQuestion q={QUESTIONS.esg} value={esgAnswer} onChange={answerEsg} />
      </QuestionStage>
    );
  } else if (stage === 'currency') {
    stageBody = (
      <QuestionStage
        eyebrow="Required decision · 3 of 3"
        title="Currency exposure"
        sub="The single highest-impact lever for this case — the USD overweight is what amplifies drawdown risk during the tuition window."
        answerPills={answerPills} jumpTo={jumpTo} modified={modified}
        question={QUESTIONS.currency}
        whyOpen={whyOpen} setWhyOpen={setWhyOpen}
        canContinue={!!usdCeil}
        onContinue={goNext}
        continueLabel="Build the proposal"
      >
        <CurrencyWidget q={QUESTIONS.currency} value={usdCeil} onChange={answerCurrency} />
      </QuestionStage>
    );
  } else if (stage === 'review') {
    stageBody = (
      <>
        <StageHeader eyebrow="Review and ship" title="Three decisions, one auto-modeled allocation, one suitability draft." sub="Every claim links back to its source. Plan tree on the left lets you revisit any decision; the canvas on the right will re-render." />
        <AnswersTrail answers={answerPills} onJumpTo={jumpTo} modified={modified} />
        <ProposalDoc baseline={TAN_BASELINE.current} proposed={proposed} kpis={kpis} />
        <div className="doc-actions">
          <div className="ico">S</div>
          <div className="txt">
            <b>Send for supervisor sign-off</b>
            <small>Routes to head of advisory · audit log preserves the LLM-v1 → human diff</small>
          </div>
          <button className="ghost">Open audit log</button>
          <button>Send for approval</button>
        </div>
      </>
    );
  }

  return (
    <div className="app">
      {/* TOP BAR */}
      <div className="topbar">
        <div className="brand">
          Adaptive wizard <span className="sub">— portfolio construction</span>
        </div>
        <div className="crumb">Direction 02 · interactive prototype</div>
        <div className="grow" />
        {stage !== 'intro' && (
          <div className="case-pill">
            <span className="dot" />
            {TAN_BASELINE.client.name} · {TAN_BASELINE.client.aum_label} · {TAN_BASELINE.client.label}
          </div>
        )}
        <button className="ghost" onClick={restart}>Restart demo</button>
      </div>

      {/* MAIN GRID */}
      <div className="main">
        {/* LEFT — clickable plan tree */}
        <aside className="rail">
          <h6>Plan tree · click to navigate</h6>
          {stage === 'intro' ? (
            <div className="plan-empty">
              <div className="plan-empty-ico">∿</div>
              <div className="plan-empty-ttl">No plan yet</div>
              <div className="plan-empty-sub">
                The plan is generated from your brief. Different workflows
                — new mandate, top-up, estate planning — get different
                trees. Submit a brief or pick a sample to begin.
              </div>
            </div>
          ) : (
            <PlanTree steps={planView} onNavigate={jumpTo} />
          )}

          <div className="meta-block">
            <strong>How this stays short</strong>
            The classic wizard has 14 steps. I auto-fill what I can, mark "n/a" what doesn't apply, and only surface conditional follow-ups when an earlier answer triggers them.
          </div>

          {stage !== 'intro' && (
            <div className="meta-block">
              <strong>Sources used</strong>
              CRM · custodian feed · IPS document vault · your one-paragraph brief.
            </div>
          )}
        </aside>

        {/* CENTER — generative stage */}
        <main className="stage">
          <div className="stage-scroll">
            <div className="stage-inner">{stageBody}</div>
          </div>

          {stage !== 'intro' && (
            <div className="cmdbar">
              <NLCmdBar onSubmit={handleCommand} stage={stage} value={cmdValue} setValue={setCmdValue} />
              <div className="cmdbar-hint">
                Tap a sample to see how the agent re-plans across multiple steps at once.
              </div>
            </div>
          )}
          {stage !== 'intro' && toast && <NLToast {...toast} />}
        </main>

        {/* RIGHT — live portfolio canvas */}
        <aside className="canvas-rail">
          <CanvasRail
            stage={stage}
            decisionCount={completedDecisionCount}
            current={TAN_BASELINE.current}
            proposed={proposed}
            kpis={kpis}
            prevKpis={prevKpis}
            liqItems={liqItems}
            usdCeil={usdCeil}
            riskAnswer={riskAnswer}
          />
        </aside>
      </div>
    </div>
  );
}

// ----- Stage header -----
function StageHeader({ eyebrow, title, sub }) {
  return (
    <div className="stagehead">
      <div className="eyebrow">{eyebrow}</div>
      <h2>{title}</h2>
      <div className="sub">{sub}</div>
    </div>
  );
}

// ----- Wrap a question step with header, pills, gencard, footer -----
function QuestionStage({ eyebrow, title, sub, answerPills, jumpTo, modified, question, whyOpen, setWhyOpen, canContinue, onContinue, continueLabel, children }) {
  return (
    <>
      <StageHeader eyebrow={eyebrow} title={title} sub={sub} />
      <AnswersTrail answers={answerPills} onJumpTo={jumpTo} modified={modified} />
      <GenCard
        q={question}
        whyOpen={whyOpen}
        onToggleWhy={() => setWhyOpen(v => !v)}
        footer={
          <button className="next" disabled={!canContinue} onClick={onContinue}>
            {canContinue
              ? <>{continueLabel} <span className="arr" style={{fontFamily:'var(--mono)'}}>→</span></>
              : 'Pick one to continue'}
          </button>
        }
      >
        {children}
      </GenCard>
    </>
  );
}

// ----- Generic chip question (used by stress + property steps) -----
function ChipQuestion({ q, value, onChange }) {
  return (
    <div className="chips">
      {q.options.map(o => (
        <button
          key={o.v}
          className={`chip ${value === o.v ? 'sel' : ''} ${o.recommended ? 'recommended' : ''}`}
          onClick={() => onChange(o.v)}
        >
          {o.label}
        </button>
      ))}
    </div>
  );
}

// ----- Intro stage -----
function IntroStage({ briefText, setBriefText, onSubmit, onStarter }) {
  const ta = useRef(null);
  useEffect(() => { ta.current?.focus(); }, []);
  const hasBrief = !!briefText.trim();
  return (
    <div className="intro-wrap">
      <div className="intro-hero">
        <div className="ey">No fields. No 14-step form.</div>
        <h1>
          Tell me what you want to build.<br />
          <span className="em">I'll go gather everything else.</span>
        </h1>
        <p>
          Type, dictate, or paste a brief — the way you'd brief a junior analyst. I'll pull the
          rest from CRM, the custodian feed, and the document vault. Then I'll only ask you the
          questions that actually move the recommendation.
        </p>
      </div>

      <div className="composer">
        <textarea
          ref={ta}
          value={briefText}
          onChange={e => setBriefText(e.target.value)}
          onKeyDown={e => {
            if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) onSubmit();
          }}
          placeholder={'e.g. "Annual review for the Tan family — Sarah\u2019s daughter is starting an MBA, they need US$220k over 2 years, and Sarah wants to push retirement back a year."'}
        />
        <div className="composer-foot">
          <button className="icobtn" title="Voice input">🎙</button>
          <button className="icobtn" title="Attach a document">⎘</button>
          <button className="icobtn" title="Pull from CRM">⌘</button>
          <span className="gap" />
          <button className="submit" disabled={!hasBrief} onClick={onSubmit}>
            Build the proposal <span className="arr">→</span>
          </button>
        </div>
      </div>

      <div className="starters">
        <div className="starters-label">— Or start from one of these</div>
        <div className="starter-grid">
          {STARTERS.map(s => (
            <button key={s.id} className="starter" onClick={() => onStarter(s)}>
              <div className="stl">{s.title}</div>
              <div className="sds">{s.desc}</div>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// ----- Discovery stage wrapper -----
function DiscoveryStage({ brief, isSideQuest, alreadyVisited, returnLabel, onResume, adjustments, onAdjust }) {
  const returnLabels = {
    risk: 'risk tolerance', stress: 'stress-test confirm', liquidity: 'liquidity reserves',
    property: 'property timing', currency: 'currency exposure', review: 'review',
  };
  return (
    <>
      {isSideQuest && (
        <div className="sidequest-banner">
          <div className="ico">⚲</div>
          <div className="txt">
            <b>Pre-fill review · you jumped here from another step.</b> Click any pre-filled row to override or flag it. Resume sends you back to <b>{returnLabels[returnLabel] || 'where you were'}</b>.
          </div>
          <button className="primary" onClick={onResume}>Resume →</button>
        </div>
      )}
      <StageHeader
        eyebrow={alreadyVisited ? 'Pre-fill review · revisit' : 'Step 1 · Review what I pulled'}
        title="Click any pre-filled row to adjust it before we begin."
        sub={brief ? `From your brief: "${brief.length > 120 ? brief.slice(0, 117) + '…' : brief}"` : 'Reviewing the pre-fill snapshot.'}
      />
      <DiscoveryView
        rows={DISCOVERY_ROWS}
        ready={true}
        adjustments={adjustments}
        onAdjust={onAdjust}
        onContinue={onResume}
        ctaLabel={isSideQuest ? 'Resume →' : 'Looks right — first decision →'}
      />
    </>
  );
}

// ----- NL command bar -----
function NLCmdBar({ onSubmit, stage, value, setValue }) {
  const val = value;
  const setVal = setValue;
  const placeholders = {
    discovery: 'Ask about any pre-filled field — "where did the AUM come from?" · "resume" · "skip ahead"',
    risk:      'Override anytime — "actually, more conservative" · "show me the pre-fill" · "skip — I\'m done"',
    stress:    'Override anytime — "roll back to same as 2023" · "flag for live re-confirm"',
    liquidity: 'Override anytime — "also add the property reserve" · "drop the MBA, they\'re paying cash"',
    property:  'Override anytime — "match the doc-vault date" · "earlier — Q1 2027"',
    currency:  'Override anytime — "30% is fine" · "go to 20% and re-run"',
    review:    'Talk to the proposal — "trim US equity another 2%" · "rewrite the rationale paragraph"',
  };
  // Tap a sample → send immediately so the user sees the cascade play out.
  const samples = [
    { label: 'Make it more conservative',                 cmd: 'Make it more conservative — we want a smaller drawdown',                tag: '→ risk + currency + stress' },
    { label: 'Push retirement to age 65',                  cmd: 'Sarah wants to push retirement back to age 65 instead of 63',           tag: '→ horizon + risk + liquidity' },
    { label: 'Add the property reserve',                   cmd: 'Also add the 2027 property top-up to the liquidity reserves',           tag: '→ liquidity + property' },
    { label: 'Add a light ESG screen',                     cmd: 'Sarah opted in to a light ESG screen — ethical funds where we can',     tag: '→ ESG + modeling' },
    { label: 'Skip ahead — accept your recommendations',  cmd: 'Skip ahead — I\'m done. Accept all your recommendations and draft it.', tag: '→ ships the proposal' },
  ];
  return (
    <>
      <div className="cmdbar-samples">
        <div className="cs-label">Try a sample prompt:</div>
        <div className="cs-list">
          {samples.map(s => (
            <button
              key={s.label}
              className={`cs-chip ${val === s.cmd ? 'sel' : ''}`}
              onClick={() => setVal(s.cmd)}
              title={`Load this into the prompt bar — then press Enter to send.`}
            >
              <span className="cs-chip-lbl">{s.label}</span>
              <span className="cs-chip-tag">{s.tag}</span>
            </button>
          ))}
        </div>
      </div>
      <div className="cmdbar-inner">
        <span className="glyph">A</span>
        <input
          value={val}
          onChange={e => setVal(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') { onSubmit(val); setVal(''); } }}
          placeholder={placeholders[stage] || 'Tell the wizard anything in plain English'}
        />
        <button className="send" onClick={() => { onSubmit(val); setVal(''); }}>↵</button>
      </div>
    </>
  );
}

// ----- Toast -----
function NLToast({ title, body, meta }) {
  return (
    <div className="nl-toast">
      <div className="ico">A</div>
      <div>
        <b>{title}</b>
        {body}
        {meta && <small>{meta}</small>}
      </div>
    </div>
  );
}

// ----- Right canvas rail -----
function CanvasRail({ stage, decisionCount, current, proposed, kpis, prevKpis, liqItems, usdCeil, riskAnswer }) {
  const isIntro = stage === 'intro';
  const allocation = isIntro ? current : proposed;

  return (
    <>
      <div className="canvas-head">
        <div className="ey">Live proposal canvas</div>
        <h4>{isIntro ? 'Empty — waiting for brief' : 'Tan Family · v' + (decisionCount + 1) + ' draft'}</h4>
        <div className="case-line">
          {isIntro
            ? 'The canvas updates the moment you submit a brief.'
            : `Updates with every decision · ${decisionCount}/3 required answers`
          }
        </div>
      </div>

      <div className="canvas-body">
        <div className="donut-wrap">
          <Donut allocation={allocation} />
          <AllocationLegend current={current} proposed={allocation} />
        </div>

        {!isIntro && (
          <>
            <KpiGrid kpis={kpis} prevKpis={prevKpis} />

            <div className="canvas-section">
              <div className="ttl">Reserve sleeves</div>
              {liqItems.filter(i => i.on).length === 0 ? (
                <div className="empty">None carved yet — answer the liquidity step.</div>
              ) : (
                liqItems.filter(i => i.on).map(i => (
                  <div key={i.id} className="item">
                    <span className="nm">{i.name}</span>
                    <span className="vl">{i.amt}</span>
                  </div>
                ))
              )}
            </div>

            <div className="canvas-section">
              <div className="ttl">Decisions on record</div>
              <div className="item">
                <span className="nm">Risk tolerance</span>
                <span className="vl">
                  {riskAnswer
                    ? QUESTIONS.risk.options.find(o => o.v === riskAnswer).label
                    : <span style={{color:'var(--ink-4)'}}>—</span>}
                </span>
              </div>
              <div className="item">
                <span className="nm">USD ceiling</span>
                <span className="vl">
                  {usdCeil ? `${usdCeil}%` : <span style={{color:'var(--ink-4)'}}>—</span>}
                </span>
              </div>
              <div className="item">
                <span className="nm">Sleeves</span>
                <span className="vl">{allocation.length}</span>
              </div>
            </div>

            <div className="canvas-section" style={{
              background: stage === 'review' ? 'var(--good-soft)' : 'var(--panel)',
              borderColor: stage === 'review' ? '#c8e1d3' : 'var(--line)',
            }}>
              <div className="ttl" style={{ color: stage === 'review' ? 'var(--good)' : 'var(--ink-3)' }}>
                {stage === 'review' ? 'Proposal ready' : 'Status'}
              </div>
              <div style={{ fontSize: 12.5, lineHeight: 1.55, color: stage === 'review' ? 'var(--ink)' : 'var(--ink-2)' }}>
                {stage === 'review'
                  ? 'Suitability narrative drafted. Calc engine signed off. Ready for advisor edits and supervisor sign-off.'
                  : `Modeling will rerun automatically when you confirm the next required answer.`
                }
              </div>
            </div>
          </>
        )}
      </div>
    </>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
