/* Poise onboarding - engine: flow data, audio, insight branches, shared chrome.
   Spec: POISE ONBOARDING - CANONICAL SPEC (2026-06-10). LOCKED items appear verbatim. */

const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;

/* ---------- Flow context ---------- */
const FlowCtx = createContext(null);
const useFlow = () => useContext(FlowCtx);

/* ---------- Instrumentation (console only - port-time requirement) ---------- */
function track(event, props) {
  try { console.info('[poise-analytics]', event, props || {}); } catch (e) {}
}

/* ---------- Audio: soft chime + selection tick (Web Audio) ---------- */
const PoiseAudio = (() => {
  let ctx = null;
  let enabled = true;
  function ac() {
    if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
    if (ctx.state === 'suspended') ctx.resume();
    return ctx;
  }
  function tone(freq, at, dur, peak, type) {
    const c = ac();
    const o = c.createOscillator();
    const g = c.createGain();
    o.type = type || 'sine';
    o.frequency.value = freq;
    o.connect(g); g.connect(c.destination);
    g.gain.setValueAtTime(0, at);
    g.gain.linearRampToValueAtTime(peak, at + 0.012);
    g.gain.exponentialRampToValueAtTime(0.0001, at + dur);
    o.start(at); o.stop(at + dur + 0.05);
  }
  return {
    setEnabled(v) { enabled = v; },
    tick() { if (!enabled) return; try { tone(1320, ac().currentTime, 0.07, 0.03, 'triangle'); } catch (e) {} },
    /* The nudge: 0.3s, two soft partials - a tap on the shoulder, not an alarm */
    chime() {
      if (!enabled) return;
      try { const t = ac().currentTime; tone(740, t, 0.32, 0.055); tone(1108, t + 0.07, 0.3, 0.04); } catch (e) {}
    },
    success() {
      if (!enabled) return;
      try { const t = ac().currentTime; tone(659, t, 0.3, 0.04); tone(880, t + 0.09, 0.34, 0.04); tone(1318, t + 0.18, 0.42, 0.03); } catch (e) {}
    },
  };
})();

/* ---------- Screens ---------- */
const SCREENS = [
  { id: 'splash',        label: 'S0 · Splash' },
  { id: 'welcome',       label: 'S1 · Welcome' },
  { id: 'hook',          label: 'S2 · The hook' },
  { id: 'why-read',      label: 'S3 · Why: seen and felt' },
  { id: 'why-compound',  label: 'S4 · Why: nobody notices' },
  { id: 'why-you',       label: 'S5 · How it works: priming' },
  { id: 'q0',            label: 'S6 · Q1: meaning' },
  { id: 'q1',            label: 'S7 · Q2: workstyle' },
  { id: 'q2',            label: 'S8 · Q3: trigger' },
  { id: 'q3',            label: 'S9 · Q4: tried' },
  { id: 'insight',       label: 'S11 · Personalized insight' },
  { id: 'motion',        label: 'S12 · Motion permission' },
  { id: 'notifications', label: 'S13 · Notifications' },
  { id: 'airpods',       label: 'S14 · AirPods check' },
  { id: 'airpods-result', label: 'S14a · AirPods result' },
  { id: 'calibration',   label: 'S15 · Calibration' },
  { id: 'demo',          label: 'S16 · Live demo' },
  { id: 'showcase',      label: 'S17 · Feature showcase' },
  { id: 'anchor',        label: 'S18 · Price anchor' },
  { id: 'coaching',      label: 'S19 · Coaching style' },
  { id: 'desk-schedule', label: 'S20 · Desk sessions' },
  { id: 'commit',        label: 'S21 · Hold to commit' },
  { id: 'commit-day1',   label: 'S21 · Hold to commit', hideFromJump: true }, // same beat as S21: hold-to-commit auto-advances into this celebration; one entry in the screen jumper
  { id: 'preparing',     label: 'S22 · Preparing profile' },
  { id: 'paywall',       label: 'S23 · Paywall' },
  { id: 'success',       label: 'S24 · Success' },
  { id: 'end',           label: 'End of flow' },
];

/* ---------- LOCKED questions (keys + option meanings fixed) ---------- */
const QUESTIONS = [
  {
    key: 'onboarding_meaning', short: 'meaning',
    title: 'What does better posture mean to you?',
    options: [
      { k: 'confident', label: 'Looking more confident' },
      { k: 'serious',   label: 'People taking me more seriously' },
      { k: 'healthy',    label: 'Having a healthier lifestyle' },
      { k: 'pain',      label: 'Avoiding pain later in life' },
    ],
  },
  {
    key: 'onboarding_workstyle', short: 'workstyle',
    title: 'How do you spend most of your day?',
    options: [
      { k: 'sit',   label: 'Mostly sitting' },
      { k: 'move',  label: 'Mostly standing or walking' },
      { k: 'mixed', label: 'Half and half' },
    ],
  },
  {
    key: 'onboarding_trigger', short: 'trigger',
    title: 'Why do you want to improve your posture?',
    options: [
      { k: 'catch',     label: 'I catch myself slouching' },
      { k: 'confident', label: 'I want to feel more confident' },
      { k: 'pointed',   label: 'Someone pointed it out' },
      { k: 'tiktok',    label: 'I saw it on TikTok' },
      { k: 'tension',   label: 'Neck or back tension' },
    ],
  },
  {
    key: 'onboarding_tried', short: 'tried',
    multi: true, exclusive: 'nothing',
    title: 'Have you tried anything else for your posture?',
    options: [
      { k: 'braces',    label: 'Posture braces' },
      { k: 'exercises', label: 'Exercises or stretches' },
      { k: 'desk',      label: 'Standing desks' },
      { k: 'yoga',      label: 'Yoga or Pilates' },
      { k: 'nothing',   label: 'Nothing. This is my first try' },
    ],
  },
];

/* ---------- S8 insight branches ----------
   Pared down from 13 named branches + fallback to 4. Rationale (per design
   review): Q1 "what posture means to you" is the one signal worth building a
   headline on, it maps straight to a value prop. "Confident" and "serious"
   turned out to be the same underlying insight (posture as a nonverbal read
   others form of you), so they now share one branch rather than forking on
   near-identical intent. Workstyle/tried used to get cross-referenced into
   a combinatorial matrix along with awareness (since cut); that's gone,
   those answers still feed analytics and other screens, just not this one.
   Two cross-cutting overrides are checked first, ahead of the meaning-based
   branches: skipped_all (none of the four questions answered - a distinct
   experience worth acknowledging on its own, not just silently landing on
   the generic fallback) and trigger === 'pointed' (being told by someone
   else is a genuinely different entry narrative from self-noticing,
   regardless of stated motivation). Answering at least one question but
   not matching pointed/meaning defaults silently to the "healthy
   lifestyle" branch, last in the list with test () => true: that copy is
   broad enough to land for anyone, so it doubles as both an explicit
   answer and the fallback, no separate fallback entry to maintain.
   Evaluated top-down, first match wins. */
const BRANCHES = [
  {
    id: 'skipped_all', name: 'Skipped every question',
    test: (a) => !a.meaning && !a.workstyle && !a.trigger && !a.tried,
    headline: 'Your time is important to you.',
    body: [
      'Skipping these questions is okay, and shows that you respect your time.',
      'Poise respects it too, which is why we’ve worked hard to make the posture monitoring experience smooth and seamless.',
    ],
  },
  {
    id: 'called_out', name: 'Someone pointed it out',
    test: (a) => a.trigger === 'pointed',
    headline: 'It took someone else to say it.',
    body: [
      'That’s how posture works: everyone else sees it before you do.',
      'From now on, Poise will notice for you.',
    ],
  },
  {
    id: 'serious', name: 'Confident / taken more seriously',
    test: (a) => a.meaning === 'serious' || a.meaning === 'confident',
    headline: 'Your presence speaks before you do.',
    body: [
      'How you carry yourself sets the first impression.',
      'Poise keeps it working in your favor.',
    ],
  },
  {
    id: 'pain', name: 'Avoiding pain later',
    test: (a) => a.meaning === 'pain',
    headline: 'You\u2019d rather stay ahead of stiffness than manage it later.',
    body: [
      'Tightness doesn\u2019t show up overnight.',
      'The sooner you notice it, the less there is to undo.',
      'Poise helps you notice.',
    ],
  },
  {
    id: 'healthy', name: 'Having a healthier lifestyle (default)',
    test: () => true,
    headline: 'Posture is the healthy habit you haven\u2019t gotten to yet.',
    body: [
      'That\u2019s because it\u2019s not a one-time thing. It\u2019s easy to lose track of.',
      'Poise keeps track of it for you.',
    ],
  },
];

/* Conditional screens: the S17 desk-schedule prompt fires off the S5 Q3
   workstyle answer (walking has no fixed schedule, so it isn't asked; Poise's
   existing activity detection already goes stricter while walking with
   nothing to configure). Shown for 'sit' or 'mixed'. next/back hop over it
   when disabled. */
function screenEnabled(id, answers) {
  const w = (answers || {}).workstyle;
  if (id === 'desk-schedule') return w === 'sit' || w === 'mixed';
  return true;
}

function pickBranch(answers, forced) {
  if (forced && forced !== 'Auto') {
    const f = BRANCHES.find((b) => b.id === forced || b.name === forced);
    if (f) return f;
  }
  const a = answers || {};
  return BRANCHES.find((b) => b.test(a));
}

/* ---------- Answer presets (Tweaks) ---------- */
const PRESETS = {
  'Mia · confidence seeker': { meaning: 'confident', workstyle: 'mixed', trigger: 'catch', tried: 'nothing' },
  'Taken seriously':         { meaning: 'serious', workstyle: 'sit', trigger: 'catch', tried: 'exercises' },
  'Healthier lifestyle':    { meaning: 'healthy', workstyle: 'sit', trigger: 'tiktok', tried: 'exercises' },
  'Avoiding pain':           { meaning: 'pain', workstyle: 'sit', trigger: 'tension', tried: 'braces' },
  'Pointed out by someone':  { meaning: 'confident', workstyle: 'move', trigger: 'pointed', tried: 'yoga' },
  'Skipped everything':      {},
};

/* ---------- Shared chrome ---------- */

/* Top bar: back chevron · center cue · skip. 54px tall, sits under the status bar.
   backFadeDelay (ms): chevron eases in late - for choreographed screens where
   chrome should arrive after the content starts speaking. */
/* Per-screen header: back chevron (left) and optional skip (right). The progress
   bar itself is NOT here — it lives in OnbProgressBar at app level so it stays
   static and fills smoothly. This row is rendered OUTSIDE the screen's entrance
   animation (see Screen) so the back arrow never slides in. cue is accepted for
   back-compat but no longer rendered (the bar conveys progress). */
function TopBar({ onBack, onSkip, skipLabel, backFadeDelay }) {
  return (
    <div style={{ height: 48, display: 'flex', alignItems: 'center', gap: 4, padding: '0 10px', flexShrink: 0 }}>
      <div style={{ width: 60, flexShrink: 0 }}>
        {onBack && (
          <button onClick={onBack} aria-label="Back" className={`ghost-link${backFadeDelay != null ? ' fade' : ''}`} style={{ display: 'flex', alignItems: 'center', padding: '8px 10px', animationDelay: backFadeDelay != null ? `${backFadeDelay}ms` : undefined, animationDuration: backFadeDelay != null ? '900ms' : undefined }}>
            <window.IconChevron size={20} sw={2} style={{ transform: 'rotate(180deg)' }} />
          </button>
        )}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}></div>
      <div style={{ width: 60, flexShrink: 0, display: 'flex', justifyContent: 'flex-end' }}>
        {onSkip && (
          <button onClick={onSkip} className="ghost-link" style={{ fontSize: 14 }}>{skipLabel || 'Skip'}</button>
        )}
      </div>
    </div>
  );
}

/* Persistent onboarding progress bar — rendered ONCE at app level (outside the
   per-screen keyed container) so it never re-animates on screen change and its
   fill grows smoothly. Sits in the exact row as each screen's back/skip (matched
   54px top inset, 48px row, 60px side gutters), pointer-events off so the back
   and skip buttons beneath it stay tappable. Shows from S2 (the hook) through the
   last pre-paywall screen, including the five questions. */
function OnbProgressBar({ screenId, fading }) {
  // airpods-result is an overlay on S11 — don't advance the bar beyond airpods
  const resolvedId = screenId === 'airpods-result' ? 'airpods' : screenId;
  const idx = SCREENS.findIndex((s) => s.id === resolvedId);
  const hookIdx = SCREENS.findIndex((s) => s.id === 'hook');
  // The bar runs from S2 (hook) through S18a (day 1 committed), reaching full
  // there. The paywall (S20) and everything after sit outside the tracked flow.
  // It fades out near the end of S18a, before the screen advances to S19 (see
  // Day1Screen), rather than snapping away on screen change.
  const endIdx = SCREENS.findIndex((s) => s.id === 'commit-day1');
  const show = hookIdx >= 0 && endIdx >= 0 && idx >= hookIdx && idx <= endIdx && !fading;
  const frac = endIdx > hookIdx ? Math.min(1, Math.max(0, idx / endIdx)) : 0;
  return (
    <div aria-hidden="true" style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 15, paddingTop: 'var(--status-bar-inset)', pointerEvents: 'none', opacity: show ? 1 : 0, transition: 'opacity 700ms linear' }}>
      <div style={{ height: 48, display: 'flex', alignItems: 'center', gap: 4, padding: '0 10px' }}>
        <div style={{ width: 60, flexShrink: 0 }}></div>
        <div style={{ flex: 1, position: 'relative', height: 5, borderRadius: 3, background: 'var(--surface-elevated)', overflow: 'hidden' }}>
          <div style={{ position: 'absolute', insetBlock: 0, left: 0, width: `${frac * 100}%`, background: 'var(--mint)', borderRadius: 3, transition: 'width 360ms var(--ease-out)' }}></div>
        </div>
        <div style={{ width: 60, flexShrink: 0 }}></div>
      </div>
    </div>
  );
}

/* Screen body: 20px screen margins, content column + pinned CTA area.
   The content column scrolls (scrollbar hidden) if it's taller than the
   viewport - most screens are tuned to fit exactly and never trigger it,
   but on a short real mobile browser viewport (address bar chrome, etc.)
   some content no longer fits, and it should scroll rather than clip. The
   CTA stays pinned outside the scroll area either way. */
function ScreenBody({ children, cta, style }) {
  return (
    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '0 var(--screen-margin)', minHeight: 0, ...style }}>
      <div className="onb-scroll-y" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>{children}</div>
      {cta && <div style={{ flexShrink: 0, paddingBottom: 34, paddingTop: 12, display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' }}>{cta}</div>}
    </div>
  );
}

/* Standard screen wrapper. The header (topBar) sits OUTSIDE the entrance
   animation so the back arrow stays static across screen changes; only the body
   rises/fades on enter. */
function Screen({ id, label, topBar, children, cta, bodyStyle, enter, noEnter }) {
  return (
    <div data-screen-label={label || id} style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', paddingTop: 'var(--status-bar-inset)' }}>
      {topBar || <div style={{ height: 48, flexShrink: 0 }}></div>}
      <div className={noEnter ? '' : `onb-screen-enter${enter ? ' ' + enter : ''}`} style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
        <ScreenBody cta={cta} style={bodyStyle}>{children}</ScreenBody>
      </div>
    </div>
  );
}

/* iOS system permission alert */
function IOSAlert({ title, message, deny, allow, onDeny, onAllow }) {
  return (
    <div className="ios-alert-dim">
      <div className="ios-alert" role="alertdialog">
        <div className="ios-alert-title">{title}</div>
        <div className="ios-alert-msg">{message}</div>
        <div className="ios-alert-row">
          <button className="ios-alert-btn" onClick={onDeny}>{deny || 'Don’t Allow'}</button>
          <button className="ios-alert-btn bold" onClick={onAllow}>{allow || 'Allow'}</button>
        </div>
      </div>
    </div>
  );
}

/* Side-profile head - data-driven functional illustration (allowed by spec).
   pitch: degrees forward (0 = upright). airpod: draws the bud in the ear;
   flashKey: bump to fire the nudge pulse rings around it. */
function HeadProfile({ pitch = 0, size = 150, color = 'var(--text-primary)', accent = 'var(--mint)', dur = 700, ease = 'var(--ease-out)', airpod = false, flashKey = 0 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 120 120" fill="none" aria-hidden="true">
      {/* shoulder base */}
      <path d="M18 112 C 34 96 56 92 78 96 L 102 102" stroke={color} strokeOpacity="0.32" strokeWidth="3" strokeLinecap="round"></path>
      {/* neck + head group, rotates around neck base (60,94) */}
      <g style={{ transform: `rotate(${pitch}deg)`, transformOrigin: '60px 94px', transition: `transform ${dur}ms ${ease}` }}>
        <path d="M54 94 C 54 82 52 76 52 68" stroke={color} strokeWidth="3" strokeLinecap="round"></path>
        <path d="M70 94 C 70 84 72 78 72 70" stroke={color} strokeWidth="3" strokeLinecap="round"></path>
        {/* head outline, side profile facing right */}
        <path d="M52 68 C 44 60 44 44 52 36 C 58 29 70 27 78 32 C 86 37 89 46 86 54 C 84 60 84 62 86 66 L 82 68 C 82 72 80 74 76 74 C 73 74 71 72 70 70"
          stroke={accent} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"></path>
        {airpod && (
          <g>
            {flashKey > 0 && (
              <g key={flashKey}>
                <circle className="pod-pulse" cx="61" cy="57" r="7" stroke="var(--mint)" strokeWidth="1.6" fill="none"></circle>
                <circle className="pod-pulse p2" cx="61" cy="57" r="7" stroke="var(--mint)" strokeWidth="1.2" fill="none"></circle>
              </g>
            )}
            <ellipse cx="61" cy="57" rx="3.2" ry="4" fill="var(--mint)"></ellipse>
            <path d="M60.6 60.5 L 59.4 66" stroke="var(--mint)" strokeWidth="2.2" strokeLinecap="round"></path>
          </g>
        )}
      </g>
    </svg>
  );
}

/* Wordmark lockup - chevron + standard-width SF Pro Bold (never compressed) */
function Wordmark({ size = 30, chevron = true }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: size * 0.32, justifyContent: 'center' }}>
      {/* Chevron inlined (not an <img>) so it paints in the same frame as the
          text - an external src decodes a beat late and the lockup tears. */}
      {chevron && (
        <svg viewBox="0 0 92 76" fill="none" aria-hidden="true" style={{ height: size * 0.78, display: 'block' }}>
          <defs>
            <linearGradient id="poiseChevronWm" x1="10" y1="60" x2="82" y2="24" gradientUnits="userSpaceOnUse">
              <stop offset="0" stopColor="#7DE0A6"></stop>
              <stop offset="0.55" stopColor="#65DAB6"></stop>
              <stop offset="1" stopColor="#56D2C8"></stop>
            </linearGradient>
          </defs>
          <polyline points="14,60 46,20 78,60" stroke="url(#poiseChevronWm)" strokeWidth="15" strokeLinecap="round" strokeLinejoin="round"></polyline>
        </svg>
      )}
      <span style={{ fontFamily: 'var(--font-sans)', fontWeight: 700, fontSize: size, letterSpacing: '-0.01em', color: 'var(--text-primary)' }}>Poise</span>
    </div>
  );
}

/* Headline / body helpers */
function H({ children, style }) {
  return <h1 className="poise-headline" style={{ margin: 0, textWrap: 'pretty', ...style }}>{children}</h1>;
}
function P({ children, style }) {
  return <p className="poise-body" style={{ margin: 0, color: 'var(--text-secondary)', textWrap: 'pretty', ...style }}>{children}</p>;
}

Object.assign(window, {
  FlowCtx, useFlow, track, PoiseAudio, SCREENS, QUESTIONS, BRANCHES, pickBranch, screenEnabled, PRESETS,
  TopBar, OnbProgressBar, ScreenBody, Screen, IOSAlert, HeadProfile, Wordmark, H, P,
});
