/* global React */
const { useState, useEffect, useMemo, useRef, useCallback, createContext, useContext } = React;

/* =========================================================================
   App context — theme, tier, density, font, accent, empty-dashboard toggle
   ========================================================================= */
const AppCtx = createContext(null);
const useApp = () => useContext(AppCtx);

const TIERS = ['free', 'pro', 'team', 'enterprise'];
const TIER_LABEL = { free: 'Free', pro: 'Pro', team: 'Team', enterprise: 'Enterprise', admin: 'Admin' };

/* =========================================================================
   Tiny icon set — single-glyph mono, drawn as inline SVG paths
   ========================================================================= */
function Icon({ name, size = 14, className = '' }) {
  const paths = {
    dash:    <><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="4" rx="1"/><rect x="14" y="10" width="7" height="11" rx="1"/><rect x="3" y="13" width="7" height="8" rx="1"/></>,
    plus:    <><path d="M12 5v14M5 12h14" /></>,
    clock:   <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
    tag:     <><path d="M3 12V4h8l10 10-8 8L3 12z"/><circle cx="8" cy="8" r="1.5"/></>,
    user:    <><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></>,
    chev:    <><path d="M6 9l6 6 6-6"/></>,
    check:   <><path d="M4 12l5 5L20 6"/></>,
    x:       <><path d="M6 6l12 12M18 6L6 18"/></>,
    lock:    <><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 018 0v3"/></>,
    sparkle: <><path d="M12 3v6M12 15v6M3 12h6M15 12h6M6 6l3 3M15 15l3 3M6 18l3-3M15 9l3-3"/></>,
    download:<><path d="M12 3v13M6 11l6 6 6-6M4 21h16"/></>,
    play:    <><path d="M6 4l14 8-14 8V4z"/></>,
    retry:   <><path d="M4 12a8 8 0 0114-5.3L20 9"/><path d="M20 4v5h-5"/><path d="M20 12a8 8 0 01-14 5.3L4 15"/><path d="M4 20v-5h5"/></>,
    dot:     <><circle cx="12" cy="12" r="3"/></>,
    eye:     <><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></>,
    eyeoff:  <><path d="M3 3l18 18M10.6 6.1A10.6 10.6 0 0112 6c6.5 0 10 7 10 7a18 18 0 01-3.3 4.1M6.2 6.2A18 18 0 002 12s3.5 7 10 7a10.6 10.6 0 004.8-1.1"/><path d="M9.9 9.9a3 3 0 004.2 4.2"/></>,
    mail:    <><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 7 9-7"/></>,
    warn:    <><path d="M12 3l10 18H2L12 3z"/><path d="M12 10v5M12 18v.5"/></>,
    file:    <><path d="M6 3h8l6 6v12H6V3z"/><path d="M14 3v6h6"/></>,
    grid:    <><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></>,
    dollar:  <><path d="M12 2v20M17 6H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></>,
    shield:  <><path d="M12 3l8 3v6c0 5-3.5 8.5-8 9-4.5-.5-8-4-8-9V6l8-3z"/></>,
    send:    <><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></>,
    list:    <><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></>,
    code:    <><path d="M8 6l-5 6 5 6M16 6l5 6-5 6"/></>,
    cpu:     <><rect x="5" y="5" width="14" height="14" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4"/></>,
    leaf:    <><path d="M4 20c10 0 16-6 16-16-8 0-14 3-16 10 3 2 5 4 6 6M4 20l5-5"/></>,
    gear:    <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 00.3 1.8l.1.1a2 2 0 11-2.8 2.8l-.1-.1a1.7 1.7 0 00-1.8-.3 1.7 1.7 0 00-1 1.5V21a2 2 0 11-4 0v-.1a1.7 1.7 0 00-1.1-1.5 1.7 1.7 0 00-1.8.3l-.1.1a2 2 0 11-2.8-2.8l.1-.1a1.7 1.7 0 00.3-1.8 1.7 1.7 0 00-1.5-1H3a2 2 0 110-4h.1a1.7 1.7 0 001.5-1.1 1.7 1.7 0 00-.3-1.8l-.1-.1a2 2 0 112.8-2.8l.1.1a1.7 1.7 0 001.8.3H9a1.7 1.7 0 001-1.5V3a2 2 0 114 0v.1a1.7 1.7 0 001 1.5 1.7 1.7 0 001.8-.3l.1-.1a2 2 0 112.8 2.8l-.1.1a1.7 1.7 0 00-.3 1.8V9a1.7 1.7 0 001.5 1H21a2 2 0 110 4h-.1a1.7 1.7 0 00-1.5 1z"/></>,
    'thumbs-up': <><path d="M7 10v11"/><path d="M14 4l-1 6h6a2 2 0 012 2l-2 7a2 2 0 01-2 2H7V10l4-6a2 2 0 013 0z"/></>,
    'thumbs-down': <><path d="M17 14V3"/><path d="M10 20l1-6H5a2 2 0 01-2-2l2-7a2 2 0 012-2h10v11l-4 6a2 2 0 01-3 0z"/></>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" className={className}>
      {paths[name] || null}
    </svg>
  );
}
window.Icon = Icon;

/* =========================================================================
   Top navigation
   ========================================================================= */
function TopNav({ route, onNavigate }) {
  const { tier, setTier, isAuthed, signOut, user, quotaExceeded } = useApp();
  const displayName = user?.name || 'Account';
  const displayEmail = user?.email || '';
  const initials = user?.initials || (user?.email?.[0]?.toUpperCase() ?? '?');
  const items = [
    { id: 'dashboard', label: 'Dashboard', icon: 'dash' },
    { id: 'new',       label: 'New chat', icon: 'plus', disabled: quotaExceeded, disabledTitle: 'Free-tier quota reached for this month' },
    { id: 'history',   label: 'History', icon: 'list' },
  ];
  const [menuOpen, setMenuOpen] = useState(false);
  return (
    <header className="topnav">
      <div className="topnav-inner">
        <button className="brand" onClick={() => onNavigate('dashboard')}>
          <span className="brand-mark">{window.Logo ? <window.Logo size={16}/> : 'C'}</span>
          <span>Cloud Architect <span style={{color:'var(--ink-2)'}}>Copilot</span></span>
        </button>
        <div className="nav-links">
          {items.map(it => (
            <button
              key={it.id}
              className={`nav-link ${route === it.id ? 'active' : ''}`}
              onClick={() => onNavigate(it.id)}
              disabled={it.disabled}
              title={it.disabled ? it.disabledTitle : undefined}
              style={it.disabled ? { opacity: 0.5, cursor: 'not-allowed' } : undefined}
            >
              <Icon name={it.icon} size={13} />
              {it.label}
            </button>
          ))}
        </div>
        <div className="nav-spacer" />
        <span className={`tier-pill ${tier}`}>
          <span className="dot" />
          {TIER_LABEL[tier]}
        </span>
        <div style={{position:'relative'}}>
          <button className="avatar" onClick={() => setMenuOpen(o => !o)} title={displayEmail || 'Account'}>
            {initials}
          </button>
          {menuOpen && (
            <>
              <div style={{position:'fixed', inset:0, zIndex:30}} onClick={() => setMenuOpen(false)} />
              <div style={{
                position:'absolute', top:'calc(100% + 6px)', right:0, zIndex:31,
                minWidth:240, background:'var(--bg-2)', border:'1px solid var(--line-strong)',
                borderRadius:6, boxShadow:'var(--shadow-2)', padding:6
              }}>
                <div style={{padding:'10px 10px 6px'}}>
                  <div style={{fontSize:12.5, fontWeight:600}}>{displayName}</div>
                  <div style={{fontSize:11.5, color:'var(--ink-2)', fontFamily:'var(--font-mono)'}}>{displayEmail}</div>
                </div>
                <div className="hairline" style={{margin:'6px 0'}} />
                <MenuItem icon="user"    label="Account settings" onClick={() => { setMenuOpen(false); onNavigate('account'); }} />
                <MenuItem icon="tag"     label="Billing & plan"   onClick={() => { setMenuOpen(false); onNavigate('pricing'); }} />
                <MenuItem icon="gear"    label="Preferences"      onClick={() => { setMenuOpen(false); onNavigate('preferences'); }} />
                {tier === 'admin' && (
                  <>
                    <div className="hairline" style={{margin:'6px 0'}} />
                    <MenuItem icon="shield" label="Admin panel"   onClick={() => { setMenuOpen(false); onNavigate('admin'); }} />
                  </>
                )}
                <div className="hairline" style={{margin:'6px 0'}} />
                <MenuItem icon="x"       label="Sign out"         onClick={() => { setMenuOpen(false); signOut(); }} />
              </div>
            </>
          )}
        </div>
      </div>
    </header>
  );
}
function MenuItem({ icon, label, onClick }) {
  return (
    <button onClick={onClick} style={{
      width:'100%', textAlign:'left', display:'flex', alignItems:'center', gap:10,
      padding:'8px 10px', borderRadius:4, fontSize:12.5, color:'var(--ink-1)'
    }} onMouseEnter={e => e.currentTarget.style.background='var(--bg-sunken)'} onMouseLeave={e => e.currentTarget.style.background='transparent'}>
      <Icon name={icon} size={13} />{label}
    </button>
  );
}
window.TopNav = TopNav;

/* =========================================================================
   Upgrade CTA — single source of truth for paid-tier conversion buttons.
   Routes to the pricing screen.
   ========================================================================= */
function UpgradeCta({ className = 'btn accent', size, label = 'Upgrade to Pro', icon = 'sparkle' }) {
  const { setRoute } = useApp();
  const cls = size === 'sm' ? `${className} sm` : className;
  return (
    <button className={cls} onClick={() => setRoute('pricing')}>
      <Icon name={icon} size={13}/>{label}
    </button>
  );
}
window.UpgradeCta = UpgradeCta;

/* =========================================================================
   Locked overlay — for gated features
   ========================================================================= */
function LockedOverlay({ title, sub, cta = 'Upgrade to Pro', onUpgrade }) {
  return (
    <div className="locked-overlay">
      <div className="locked-card">
        <div className="lock-icon"><Icon name="lock" size={15} /></div>
        <div className="title">{title}</div>
        <div className="sub">{sub}</div>
        <button className="btn accent" onClick={onUpgrade}>
          <Icon name="sparkle" size={13} />{cta}
        </button>
      </div>
    </div>
  );
}
window.LockedOverlay = LockedOverlay;

/* =========================================================================
   Score ring
   ========================================================================= */
function ScoreRing({ value, size = 120, stroke = 8 }) {
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const pct = Math.max(0, Math.min(100, value));
  const color = pct >= 80 ? 'var(--ok)' : pct >= 60 ? 'var(--warn)' : 'var(--err)';
  return (
    <div className="score-ring" style={{width:size, height:size}}>
      <svg>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="var(--bg-sunken)" strokeWidth={stroke} />
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeDasharray={`${c*pct/100} ${c}`} />
      </svg>
      <div className="num">{pct}</div>
    </div>
  );
}
window.ScoreRing = ScoreRing;

/* =========================================================================
   Diagram (ASCII) — renders mono text with subtle color tagging on specific tokens
   ========================================================================= */
function AsciiDiagram({ text, height = '100%', bare = false }) {
  const [zoom, setZoom] = useState(1);
  // Colorize specific markers: ⚠ → warn, words in backticks → accent
  const lines = text.split('\n');
  return (
    <div className={`diagram ${bare ? 'bare' : ''}`} style={{height, minHeight: 320}}>
      <div className="diagram-toolbar">
        <button className={`btn`} onClick={() => setZoom(z => Math.max(0.6, +(z - 0.1).toFixed(2)))}>−</button>
        <button className={`btn`} onClick={() => setZoom(1)} title="Reset">100%</button>
        <button className={`btn`} onClick={() => setZoom(z => Math.min(1.6, +(z + 0.1).toFixed(2)))}>+</button>
        <div style={{width:1, background:'var(--line)', margin:'0 2px'}} />
        <button className="btn"><Icon name="download" size={11}/> .drawio</button>
        <button className="btn"><Icon name="download" size={11}/> .png</button>
      </div>
      <div style={{ transform: `scale(${zoom})`, transformOrigin: 'top left', transition: 'transform .15s ease' }}>
        {lines.map((ln, i) => {
          // Very light syntax coloring
          const colored = ln
            .replace(/⚠/g, '§W§⚠§/§')
            .replace(/▶/g, '§A§▶§/§');
          const parts = colored.split(/§W§|§A§|§\/§/);
          return (
            <div key={i} style={{whiteSpace:'pre'}}>
              {parts.map((p, j) => {
                if (p === '⚠') return <span key={j} className="diag-warn">⚠</span>;
                if (p === '▶') return <span key={j} className="diag-accent">▶</span>;
                return <span key={j}>{p}</span>;
              })}
            </div>
          );
        })}
      </div>
    </div>
  );
}
window.AsciiDiagram = AsciiDiagram;

/* =========================================================================
   DiagramImage — renders the real PNG/SVG returned by the diagram service
   ========================================================================= */
function DiagramImage({ url, svgUrl, drawioUrl, height = '100%', bare = false }) {
  // draw.io-style viewport: zoom + pan (translate). The inner wrapper is
  // transformed; the outer viewport clips. Drag-to-pan with mouse, wheel to
  // zoom (ctrl/cmd or plain wheel) centered on the cursor, +/−/Fit buttons.
  const MIN_ZOOM = 0.2;
  const MAX_ZOOM = 5;
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({ x: 0, y: 0 });
  const [dragging, setDragging] = useState(false);
  const viewportRef = useRef(null);
  const dragStart = useRef(null);

  const displayUrl = svgUrl || url;

  // Inline the SVG when available so it stays vector under CSS transforms.
  // <img src="*.svg"> rasterises once and then `transform: scale()` samples
  // that bitmap — fine for fit-view, fuzzy when zoomed in. Inline SVG keeps
  // the browser re-painting at every zoom level.
  const [svgMarkup, setSvgMarkup] = useState(null);
  useEffect(() => {
    setSvgMarkup(null);
    if (!svgUrl) return;
    let cancelled = false;
    fetch(svgUrl)
      .then(r => r.ok ? r.text() : Promise.reject(new Error(`svg fetch ${r.status}`)))
      .then(text => { if (!cancelled) setSvgMarkup(text); })
      .catch(() => { /* fall back to <img> path below */ });
    return () => { cancelled = true; };
  }, [svgUrl]);

  const reset = () => { setZoom(1); setPan({ x: 0, y: 0 }); };

  const zoomAt = (nextZoom, cx, cy) => {
    const clamped = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, +nextZoom.toFixed(3)));
    setZoom(prevZoom => {
      // Keep the point under the cursor stationary across the zoom change.
      setPan(prevPan => {
        const k = clamped / prevZoom;
        return { x: cx - k * (cx - prevPan.x), y: cy - k * (cy - prevPan.y) };
      });
      return clamped;
    });
  };

  const onWheel = (e) => {
    e.preventDefault();
    const rect = viewportRef.current?.getBoundingClientRect();
    if (!rect) return;
    const cx = e.clientX - rect.left;
    const cy = e.clientY - rect.top;
    const factor = Math.exp(-e.deltaY * 0.0015);
    setZoom(prevZoom => {
      const next = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, prevZoom * factor));
      setPan(prevPan => {
        const k = next / prevZoom;
        return { x: cx - k * (cx - prevPan.x), y: cy - k * (cy - prevPan.y) };
      });
      return next;
    });
  };

  // Attach wheel listener manually so we can passive:false (preventDefault).
  useEffect(() => {
    const el = viewportRef.current;
    if (!el) return;
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, []);

  const onMouseDown = (e) => {
    if (e.button !== 0 && e.button !== 1) return;
    dragStart.current = { x: e.clientX - pan.x, y: e.clientY - pan.y };
    setDragging(true);
  };
  const onMouseMove = (e) => {
    if (!dragging || !dragStart.current) return;
    setPan({ x: e.clientX - dragStart.current.x, y: e.clientY - dragStart.current.y });
  };
  const endDrag = () => { setDragging(false); dragStart.current = null; };

  const stepZoom = (delta) => {
    const el = viewportRef.current;
    const cx = el ? el.clientWidth / 2 : 0;
    const cy = el ? el.clientHeight / 2 : 0;
    zoomAt(zoom + delta, cx, cy);
  };

  return (
    <div className={`diagram ${bare ? 'bare' : ''}`} style={{height, minHeight: 320, padding: 0}}>
      <div className="diagram-toolbar">
        <button className="btn" onClick={() => stepZoom(-0.1)}>−</button>
        <button className="btn" onClick={reset} title="Fit">{Math.round(zoom*100)}%</button>
        <button className="btn" onClick={() => stepZoom(0.1)}>+</button>
        <div style={{width:1, background:'var(--line)', margin:'0 2px'}}/>
        {drawioUrl && <a className="btn" href={drawioUrl} download><Icon name="download" size={11}/>.drawio</a>}
        {url && <a className="btn" href={url} download><Icon name="download" size={11}/>.png</a>}
      </div>
      <div
        ref={viewportRef}
        onMouseDown={onMouseDown}
        onMouseMove={onMouseMove}
        onMouseUp={endDrag}
        onMouseLeave={endDrag}
        onDoubleClick={reset}
        style={{
          height:'100%',
          width:'100%',
          overflow:'hidden',
          position:'relative',
          cursor: displayUrl ? (dragging ? 'grabbing' : 'grab') : 'default',
          userSelect:'none',
          touchAction:'none',
        }}
      >
        {displayUrl ? (
          <div
            style={{
              position:'absolute',
              inset:0,
              transform:`translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
              transformOrigin:'0 0',
              display:'grid',
              placeItems:'center',
              willChange:'transform',
            }}
          >
            {svgMarkup ? (
              <div
                className="diagram-svg-host"
                style={{
                  maxWidth:'100%',
                  maxHeight:'100%',
                  display:'flex',
                  alignItems:'center',
                  justifyContent:'center',
                  pointerEvents:'none',
                }}
                dangerouslySetInnerHTML={{ __html: svgMarkup }}
              />
            ) : (
              <img
                src={displayUrl}
                alt="Architecture diagram"
                draggable={false}
                style={{
                  maxWidth:'100%',
                  maxHeight:'100%',
                  width:'auto',
                  height:'auto',
                  objectFit:'contain',
                  pointerEvents:'none',
                }}
              />
            )}
          </div>
        ) : (
          <div style={{position:'absolute', inset:0, display:'flex', alignItems:'center', justifyContent:'center', color:'var(--ink-3)', fontFamily:'var(--font-mono)', fontSize:12, gap:10, flexDirection:'column'}}>
            <span className="spinner" style={{width:18, height:18}}/>
            rendering diagram…
          </div>
        )}
      </div>
    </div>
  );
}
window.DiagramImage = DiagramImage;

/* =========================================================================
   Backend → UI shape converters
   ========================================================================= */
const PILLAR_KEYS = {
  'operational-excellence': { key: 'ops',  name: 'Operational Excellence' },
  'security':               { key: 'sec',  name: 'Security' },
  'reliability':            { key: 'rel',  name: 'Reliability' },
  'performance':            { key: 'perf', name: 'Performance Efficiency' },
  'cost':                   { key: 'cost', name: 'Cost Optimization' },
  'sustainability':         { key: 'sus',  name: 'Sustainability' },
};

function scorecardToPillars(scorecard) {
  if (!scorecard) return [];
  const counts = {};
  (scorecard.results || []).forEach(r => {
    const k = r.pillar;
    counts[k] = counts[k] || { rules: 0, fail: 0, warn: 0 };
    counts[k].rules++;
    if (!r.passed) {
      if (r.severity === 'HIGH') counts[k].fail++;
      else counts[k].warn++;
    }
  });
  return Object.entries(scorecard.pillarScores || {}).map(([apiKey, score]) => {
    const meta = PILLAR_KEYS[apiKey] || { key: apiKey, name: apiKey };
    const c = counts[apiKey] || { rules: 0, fail: 0, warn: 0 };
    return { key: meta.key, apiKey, name: meta.name, score: Math.round(score), ...c };
  });
}

function scorecardToFindings(scorecard) {
  if (!scorecard) return [];
  return (scorecard.results || []).filter(r => !r.passed).map(r => ({
    pillar: PILLAR_KEYS[r.pillar]?.key || r.pillar,
    sev: r.severity,
    rule: r.ruleId,
    desc: r.recommendation,
    ref: r.ruleId,
  }));
}

function costToLines(cost) {
  if (!cost) return [];
  const hidden = new Set(cost.hiddenCostFlags || []);
  return (cost.lineItems || []).map(l => ({
    // The "resource" column shows the canonical AWS service ("Amazon RDS"),
    // not the user-supplied identifier ("main-db") — users want to see what
    // they're paying AWS for, not the label they gave it.
    resource: l.awsServiceName || l.serviceType,
    detail: l.serviceName ? `${l.serviceName} · ${l.notes}` : l.notes,
    qty: '—',
    unit: l.unit || '—',
    mo: l.monthlyEstimate,
    hidden: hidden.has(l.serviceId),
    unavailable: l.priceSource === 'unavailable',
  }));
}

// Pull the headline score/cost numbers out of a `getConversationVersion`
// response. Used by the dashboard and history list to hydrate row summaries.
function extractScoreCost(snap) {
  const score = snap?.wafReview?.scorecard?.overallScore ?? null;
  const totalMonthly = snap?.costEstimate?.totalMonthly;
  const cost = typeof totalMonthly === 'number' ? totalMonthly : null;
  return { score, cost };
}

window.scorecardToPillars = scorecardToPillars;
window.scorecardToFindings = scorecardToFindings;
window.costToLines = costToLines;
window.extractScoreCost = extractScoreCost;

const PAGE_SIZE_OPTIONS = [10, 25, 50];
const DEFAULT_PAGE_SIZE = 25;

// Footer with page-size selector ("10 / 25 / 50") + Prev/Next + range label.
// Total is the count of items being paginated; the parent slices the visible
// page itself.
function Pagination({ total, page, pageSize, onPage, onPageSize, label = 'conversation' }) {
  const totalPages = Math.max(1, Math.ceil(total / pageSize));
  const safePage = Math.min(Math.max(page, 1), totalPages);
  const start = total === 0 ? 0 : (safePage - 1) * pageSize + 1;
  const end = Math.min(total, safePage * pageSize);
  return (
    <div style={{padding:'12px 16px', borderTop:'1px solid var(--line)', display:'flex', alignItems:'center', gap:12, background:'var(--bg-1)'}}>
      <div style={{fontSize:12.5, color:'var(--ink-2)'}}>
        Showing <span className="mono">{start}–{end}</span> of <span className="mono">{total}</span> {total === 1 ? label : `${label}s`}
      </div>
      <div style={{flex:1}}/>
      <label style={{fontSize:12, color:'var(--ink-2)', display:'flex', alignItems:'center', gap:6}}>
        Per page
        <select
          className="field"
          style={{height:26, fontSize:12, padding:'0 6px', width:'auto'}}
          value={pageSize}
          onChange={e => onPageSize(Number(e.target.value))}
        >
          {PAGE_SIZE_OPTIONS.map(n => <option key={n} value={n}>{n}</option>)}
        </select>
      </label>
      <div style={{display:'flex', gap:6, alignItems:'center'}}>
        <button className="btn sm" onClick={() => onPage(safePage - 1)} disabled={safePage <= 1}>Prev</button>
        <div style={{fontSize:12, color:'var(--ink-2)'}}>
          Page <span className="mono">{safePage}</span> of <span className="mono">{totalPages}</span>
        </div>
        <button className="btn sm" onClick={() => onPage(safePage + 1)} disabled={safePage >= totalPages}>Next</button>
      </div>
    </div>
  );
}
window.Pagination = Pagination;
window.PAGE_SIZE_OPTIONS = PAGE_SIZE_OPTIONS;
window.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;

// Persist a per-list page-size choice across reloads. `key` namespaces it
// (e.g. "dashboard", "history") so each table remembers its own setting.
function usePersistedPageSize(key) {
  const storageKey = `cac.pageSize.${key}`;
  const [pageSize, setPageSize] = useState(() => {
    try {
      const raw = localStorage.getItem(storageKey);
      const n = raw ? Number(raw) : NaN;
      return PAGE_SIZE_OPTIONS.includes(n) ? n : DEFAULT_PAGE_SIZE;
    } catch {
      return DEFAULT_PAGE_SIZE;
    }
  });
  const set = useCallback(n => {
    setPageSize(n);
    try { localStorage.setItem(storageKey, String(n)); } catch { /* quota / private mode */ }
  }, [storageKey]);
  return [pageSize, set];
}
window.usePersistedPageSize = usePersistedPageSize;

/* =========================================================================
   Tweak surface registration
   ========================================================================= */
function AppProvider({ children }) {
  // Tweak defaults (edit-mode block)
  const defaults = /*EDITMODE-BEGIN*/ {
    "theme": "light",
    "accent": "cyan",
    "tier": "pro",
    "density": "comfortable",
    "font": "inter-mono",
    "pipelineState": "pending"
  } /*EDITMODE-END*/;

  const [state, setState] = useState(defaults);
  // Full preferences doc (defaults, analysis, notifications). Loaded from
  // the backend on auth. `null` means "not yet loaded" — components should
  // fall back to their own defaults until it arrives.
  const [preferences, setPreferences] = useState(null);
  // With a real backend wired up, gate auth on a stored token. Without one,
  // the prototype stays "signed in" so the design always renders.
  const hasBackend = window.API?.hasBackend;
  const [isAuthed, setAuthed] = useState(() => hasBackend ? Boolean(window.API.isAuthed()) : true);
  const [user, setUser] = useState(() => (hasBackend ? window.API.currentUser() : null));
  useEffect(() => {
    setUser(isAuthed && hasBackend ? window.API.currentUser() : null);
  }, [isAuthed, hasBackend]);

  // Server-authoritative usage info (current month). Loaded from /user; the
  // backend is the source of truth — never compute the count from the
  // conversation list (that conflates lifetime conversations with monthly
  // analyses) or trust localStorage.
  const [usage, setUsage] = useState(null);
  const [analysesLimit, setAnalysesLimit] = useState(null);

  // Load tier + preferences + usage from backend whenever auth flips on.
  useEffect(() => {
    if (!isAuthed || !hasBackend) return;
    let cancelled = false;
    window.API.getMe().then(me => {
      if (cancelled) return;
      const next = {};
      if (me?.subscription?.tier) next.tier = me.subscription.tier;
      const a = me?.preferences?.appearance;
      if (a) {
        if (a.theme)   next.theme   = a.theme;
        if (a.accent)  next.accent  = a.accent;
        if (a.density) next.density = a.density;
        if (a.font)    next.font    = a.font;
      }
      if (Object.keys(next).length) setState(s => ({ ...s, ...next }));
      if (me?.preferences) setPreferences(me.preferences);
      if (me?.usage) setUsage(me.usage);
      if (typeof me?.subscription?.analysesLimit === 'number') {
        setAnalysesLimit(me.subscription.analysesLimit);
      }
    }).catch(() => { /* leave defaults in place */ });
    return () => { cancelled = true; };
  }, [isAuthed, hasBackend]);

  // Force-refresh /user (busts the 5-min client cache) so the dashboard /
  // composer can re-pull the authoritative usage count after a run finishes.
  const refreshUsage = useCallback(async () => {
    if (!isAuthed || !hasBackend) return;
    try {
      window.API.invalidate?.('user');
      const me = await window.API.getMe();
      if (me?.usage) setUsage(me.usage);
      if (typeof me?.subscription?.analysesLimit === 'number') {
        setAnalysesLimit(me.subscription.analysesLimit);
      }
    } catch { /* leave previous value in place */ }
  }, [isAuthed, hasBackend]);
  const [route, _setRoute] = useState(() => {
    const h = window.location.hash.replace('#', '');
    return h || 'dashboard';
  });
  const [routeParams, setRouteParams] = useState({});

  const setRoute = useCallback((r, params = {}) => {
    _setRoute(r);
    setRouteParams(params);
    window.location.hash = r;
    window.scrollTo({ top: 0 });
  }, []);

  useEffect(() => {
    const onHash = () => {
      const h = window.location.hash.replace('#', '') || 'dashboard';
      _setRoute(h);
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  // Persist to host (edit-mode iframe) for design-tweak round-tripping.
  const set = useCallback((patch) => {
    setState(s => ({ ...s, ...patch }));
    try {
      window.parent.postMessage({ type: '__edit_mode_set_keys', edits: patch }, '*');
    } catch (e) {}
  }, []);

  // Save appearance prefs (theme/accent/density/font) to the backend whenever
  // they change, but only after the initial /user load has populated state
  // — otherwise we'd overwrite the persisted values with the EDITMODE defaults
  // on first render. We gate on `preferences` being non-null for that reason.
  const persistAppearance = useCallback((patch) => {
    if (!isAuthed || !hasBackend || !preferences) return;
    const nextPrefs = {
      ...preferences,
      appearance: { ...preferences.appearance, ...patch },
    };
    setPreferences(nextPrefs);
    window.API.savePreferences(nextPrefs).catch(() => {});
  }, [isAuthed, hasBackend, preferences]);

  // Persist the full preferences doc (used by the Preferences screen's Save).
  const savePreferences = useCallback(async (next) => {
    setPreferences(next);
    if (next.appearance) {
      setState(s => ({
        ...s,
        theme: next.appearance.theme ?? s.theme,
        accent: next.appearance.accent ?? s.accent,
        density: next.appearance.density ?? s.density,
        font: next.appearance.font ?? s.font,
      }));
    }
    if (!isAuthed || !hasBackend) return next;
    const result = await window.API.savePreferences(next);
    if (result?.preferences) setPreferences(result.preferences);
    return result?.preferences ?? next;
  }, [isAuthed, hasBackend]);

  // Apply theme/density/font/accent to document
  useEffect(() => {
    document.documentElement.dataset.theme = state.theme;
    document.documentElement.dataset.density = state.density;
    document.documentElement.dataset.font = state.font;
    const hues = { cyan: 215, green: 150, violet: 280, amber: 60, rose: 15 };
    document.documentElement.style.setProperty('--accent-h', hues[state.accent] ?? 215);
  }, [state.theme, state.density, state.font, state.accent]);

  // Quota tier-cap: only `free` blocks at the limit (overage allowed for paid).
  // Treat unloaded usage (`null`) as "not exceeded" so the gate doesn't flicker
  // open during the initial /user fetch.
  const tierLimitFromConfig = { free: 3, pro: 25, team: 100, enterprise: Infinity, admin: Infinity };
  const effectiveLimit = analysesLimit ?? tierLimitFromConfig[state.tier] ?? tierLimitFromConfig.free;
  const usageCount = usage?.count ?? 0;
  const quotaExceeded = state.tier === 'free' && Number.isFinite(effectiveLimit) && usageCount >= effectiveLimit;

  const value = {
    theme: state.theme, setTheme: t => { set({ theme: t }); persistAppearance({ theme: t }); },
    accent: state.accent, setAccent: a => { set({ accent: a }); persistAppearance({ accent: a }); },
    tier: state.tier, setTier: t => set({ tier: t }),
    density: state.density, setDensity: d => { set({ density: d }); persistAppearance({ density: d }); },
    font: state.font, setFont: f => { set({ font: f }); persistAppearance({ font: f }); },
    pipelineState: state.pipelineState, setPipelineState: v => set({ pipelineState: v }),
    isAuthed, setAuthed,
    user,
    preferences, savePreferences,
    refreshUser: () => setUser(hasBackend ? window.API.currentUser() : null),
    hasBackend,
    usage, usageCount, analysesLimit: effectiveLimit, quotaExceeded, refreshUsage,
    signIn: () => { setAuthed(true); setUser(hasBackend ? window.API.currentUser() : null); },
    signOut: () => {
      if (hasBackend) window.API.signOut();
      setAuthed(false);
      setUser(null);
      setPreferences(null);
      setUsage(null);
      setAnalysesLimit(null);
      _setRoute('login');
      window.location.hash = 'login';
    },
    route, setRoute,
    routeParams,
  };
  return <AppCtx.Provider value={value}>{children}</AppCtx.Provider>;
}
window.AppProvider = AppProvider;
window.useApp = useApp;
window.TIERS = TIERS;
window.TIER_LABEL = TIER_LABEL;
