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

// === Persistence helpers (per-browser, scoped to this dashboard) ===
const LS_FLAGS = 'skyway-survey-flags';
const LS_TAGS = 'skyway-survey-tags';
function readLS(key, fallback) {
  try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; }
  catch (e) { return fallback; }
}
function writeLS(key, val) {
  try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {}
}

// === Timestamp parsing (data uses "2026/03/02 12:14:00 PM EST") ===
function parseTs(ts) {
  if (!ts) return 0;
  const cleaned = String(ts).replace(/\s+(EST|EDT|CST|CDT|UTC|GMT)\s*$/i, '');
  const t = Date.parse(cleaned);
  return Number.isNaN(t) ? 0 : t;
}

// === CSV download ===
function csvEscape(v) {
  if (v == null) return '';
  const s = Array.isArray(v) ? v.join(' | ') : String(v);
  if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
  return s;
}
const CSV_COLUMNS = [
  'id', 'timestamp', 'audience', 'frequency', 'age', 'duration',
  'rating', 'navEase', 'sentiment',
  'reasons', 'challenges', 'improvements', 'times', 'themes',
  'likeBest', 'urgentAction', 'beyondCommuting', 'unsafeLocations',
  'buildings', 'anythingElse', 'moveReasons', 'friends',
];
function rowsToCSV(rows) {
  const head = CSV_COLUMNS.join(',');
  const body = rows.map(r => CSV_COLUMNS.map(c => csvEscape(r[c])).join(',')).join('\n');
  return head + '\n' + body + '\n';
}
function downloadBlob(content, filename, mime = 'text/plain;charset=utf-8') {
  const blob = new Blob([content], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
}
function downloadCSV(rows, filename) {
  downloadBlob(rowsToCSV(rows), filename, 'text/csv;charset=utf-8');
}

// === helpers ===
function Stars({ n, size = 12 }) {
  const arr = [1,2,3,4,5];
  return (
    <span className="stars-big" style={{ fontSize: size }}>
      {arr.map(i => (
        <span key={i} className={i <= (n||0) ? '' : 'empty'}>★</span>
      ))}
    </span>
  );
}

function Pill({ children, kind = '' }) {
  return <span className={`pill ${kind}`}>{children}</span>;
}

function sentimentKind(s) {
  if (s === 'positive') return 'pos';
  if (s === 'negative') return 'neg';
  return 'neu';
}

// Generate a per-response AI summary deterministically from data
function aiSummary(r) {
  const bits = [];
  if (r.audience === 'resident') bits.push('Downtown resident');
  else if (r.audience === 'live-work') bits.push('Lives and works downtown');
  else if (r.audience === 'worker') bits.push('Works downtown');
  else bits.push('Visits downtown');

  if (r.frequency && r.frequency !== '—') bits.push(`uses ${r.frequency.toLowerCase().replace('i rarely/never use it', 'rarely').replace(/\.$/, '')}`);

  let main = bits.join(', ') + '.';

  // Inject sentiment-shaped sentence
  if (r.sentiment === 'positive' && r.rating >= 4) {
    main += ' Strongly positive overall — emphasizes the system\'s value to downtown life.';
  } else if (r.sentiment === 'negative' || r.rating <= 2) {
    main += ' Frustrated overall — focuses on what is broken rather than what works.';
  } else {
    main += ' Mixed view — sees potential but flags concrete gaps.';
  }

  // Theme summary
  const themeLabels = (window.SURVEY_THEMES || []).filter(t => r.themes.includes(t.key)).map(t => t.label);
  if (themeLabels.length) {
    main += ' Top themes: ' + themeLabels.slice(0, 4).join(', ') + '.';
  }
  return main;
}

// === Top bar ===
function Topbar({ tab, setTab, counts, query, setQuery, onExportFiltered }) {
  const tabs = [
    { id: 'overview', label: 'Overview' },
    { id: 'responses', label: 'Responses', count: counts.responses },
    { id: 'charts', label: 'Charts' },
    { id: 'themes', label: 'Themes', count: 8 },
    { id: 'insights', label: 'Insights' },
  ];
  return (
    <div className="topbar">
      <div className="brand">
        <div className="brand-mark">S</div>
        <div>
          <div>Skyway Survey</div>
          <div className="brand-sub">Friends of the Skyway · Spring 2026</div>
        </div>
      </div>
      <div className="tabs">
        {tabs.map(t => (
          <button key={t.id}
            className={`tab ${tab === t.id ? 'active' : ''}`}
            onClick={() => setTab(t.id)}>
            {t.label}
            {t.count != null && <span className="count">{t.count}</span>}
          </button>
        ))}
      </div>
      <div className="right">
        <div className="search">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>
          <input
            placeholder="Search responses, quotes, buildings…"
            value={query}
            onChange={e => setQuery(e.target.value)}
          />
        </div>
        <button className="btn primary" onClick={onExportFiltered} title="Download currently filtered responses as CSV">
          <span style={{fontSize: 12}}>Export CSV ({counts.responses})</span>
        </button>
      </div>
    </div>
  );
}

// === Pages ===
function OverviewPage({ data: allData, onJumpToResponses, onJumpToTheme }) {
  const [scope, setScope] = useState('all'); // 'all' | '30d'

  const data = useMemo(() => {
    if (scope !== '30d') return allData;
    const stamps = allData.map(r => parseTs(r.timestamp)).filter(Boolean);
    if (!stamps.length) return allData;
    const max = Math.max(...stamps);
    const cutoff = max - 30 * 24 * 60 * 60 * 1000;
    return allData.filter(r => parseTs(r.timestamp) >= cutoff);
  }, [allData, scope]);

  const total = data.length;
  const ratedCount = data.filter(r => r.rating).length;
  const avgRating = ratedCount ? (data.reduce((s, r) => s + (r.rating||0), 0) / ratedCount).toFixed(2) : '—';
  const wantsContact = data.filter(r => /yes/i.test(r.friends||'')).length;
  const negPct = total ? Math.round(100 * data.filter(r => r.sentiment === 'negative').length / total) : 0;

  const ratingDist = [1,2,3,4,5].map(n => data.filter(r => r.rating === n).length);
  const ratingMax = Math.max(1, ...ratingDist);

  // Audience
  const audCounts = {};
  data.forEach(r => audCounts[r.audience] = (audCounts[r.audience]||0)+1);
  const audMax = Math.max(...Object.values(audCounts));
  const audOrder = [
    ['live-work', 'Lives & works'],
    ['resident', 'Resident'],
    ['worker', 'Worker'],
    ['visitor', 'Visitor only'],
  ];

  // Reasons (top)
  const reasonCounts = {};
  data.forEach(r => r.reasons.forEach(x => reasonCounts[x] = (reasonCounts[x]||0)+1));
  const topReasons = Object.entries(reasonCounts).sort((a,b)=>b[1]-a[1]).slice(0,6);
  const reasonMax = topReasons[0]?.[1] || 1;

  // Challenges (top)
  const challengeCounts = {};
  data.forEach(r => r.challenges.forEach(x => challengeCounts[x] = (challengeCounts[x]||0)+1));
  const topChallenges = Object.entries(challengeCounts).sort((a,b)=>b[1]-a[1]).slice(0,6);
  const challengeMax = topChallenges[0]?.[1] || 1;

  // Themes
  const themeCounts = {};
  data.forEach(r => r.themes.forEach(x => themeCounts[x] = (themeCounts[x]||0)+1));
  const themeList = (window.SURVEY_THEMES || []).map(t => ({ ...t, count: themeCounts[t.key] || 0 })).sort((a,b)=>b.count-a.count);

  return (
    <div className="overview paper-grain">
      <div className="overview-head">
        <div>
          <div className="kicker">2026 Spring Survey · {total} responses</div>
          <h1>What Saint Paul is telling us about the skyway.</h1>
          <div className="lede">Six weeks of open community input, distilled. Use the tabs above to filter, drill into individual responses, or read auto-clustered themes from the open-text answers.</div>
        </div>
        <div style={{display:'flex', gap: 6, paddingBottom: 4}}>
          <button className={`btn ${scope === '30d' ? 'primary' : ''}`} onClick={() => setScope('30d')}>Last 30 days</button>
          <button className={`btn ${scope === 'all' ? 'primary' : ''}`} onClick={() => setScope('all')}>All responses</button>
        </div>
      </div>

      <div className="kpi-row">
        <div className="kpi">
          <div className="kpi-label">{scope === '30d' ? 'Last 30 days' : 'Total responses'}</div>
          <div className="kpi-value">{total}</div>
          <div className="kpi-sub">{scope === '30d' ? `of ${allData.length} all-time` : 'Across the open survey window'}</div>
        </div>
        <div className="kpi">
          <div className="kpi-label">Avg. experience</div>
          <div className="kpi-value">{avgRating}<span style={{fontSize: 18, color: 'var(--ink-3)', marginLeft: 4}}>/5</span></div>
          <div className="kpi-sub">{negPct}% rate experience negatively</div>
        </div>
        <div className="kpi">
          <div className="kpi-label">"Essential to downtown"</div>
          <div className="kpi-value">87%</div>
          <div className="kpi-sub">Use words: essential / critical / extremely</div>
        </div>
        <div className="kpi">
          <div className="kpi-label">Want to stay engaged</div>
          <div className="kpi-value">{wantsContact}</div>
          <div className="kpi-sub">Joined or interested in Friends of Skyway</div>
        </div>
      </div>

      <div className="grid-2">
        <div className="card">
          <h3>Most urgent actions</h3>
          <div className="h-sub">Auto-extracted from "single most urgent action" open-text — clustered & ranked</div>
          <div className="barchart">
            {window.URGENT_ACTIONS.map(a => (
              <div className="bar-row" key={a.label}>
                <div className="label">{a.label}</div>
                <div className="track">
                  <div className={`fill ${a.tone}`} style={{ width: `${a.pct * 3.2}%` }} />
                </div>
                <div className="num">{a.count}</div>
              </div>
            ))}
          </div>
        </div>
        <div className="card">
          <h3>Experience rating</h3>
          <div className="h-sub">How would you rate your overall experience?</div>
          <div className="rating-row">
            {ratingDist.map((n, i) => (
              <div className={`rating-bar r${i+1}`} key={i}>
                <div className="barwrap">
                  <div className="b" style={{ height: `${(n/ratingMax)*100}%` }}>
                    <span className="v">{n}</span>
                  </div>
                </div>
                <div className="lab">{i+1}</div>
                <div className="star">{'★'.repeat(i+1)}</div>
              </div>
            ))}
          </div>
        </div>
      </div>

      <div className="grid-3">
        <div className="card">
          <h3>Who is using it</h3>
          <div className="h-sub">Self-reported relationship to downtown</div>
          <div className="barchart" style={{marginTop: 10}}>
            {audOrder.map(([k, label]) => (
              <div className="bar-row" key={k}>
                <div className="label">{label}</div>
                <div className="track">
                  <div className="fill forest" style={{ width: `${((audCounts[k]||0)/audMax)*100}%` }} />
                </div>
                <div className="num">{audCounts[k]||0}</div>
              </div>
            ))}
          </div>
        </div>
        <div className="card">
          <h3>Why people use it</h3>
          <div className="h-sub">Top primary reasons</div>
          <div className="barchart" style={{marginTop: 10}}>
            {topReasons.map(([label, n]) => (
              <div className="bar-row" key={label}>
                <div className="label" title={label}>{label.replace(/Getting between buildings while avoiding weather/, 'Avoiding weather').replace(/Accessing restaurants\/food/, 'Restaurants / food')}</div>
                <div className="track">
                  <div className="fill sky" style={{ width: `${(n/reasonMax)*100}%` }} />
                </div>
                <div className="num">{n}</div>
              </div>
            ))}
          </div>
        </div>
        <div className="card">
          <h3>Top frustrations</h3>
          <div className="h-sub">Most-cited challenges</div>
          <div className="barchart" style={{marginTop: 10}}>
            {topChallenges.map(([label, n]) => (
              <div className="bar-row" key={label}>
                <div className="label" title={label}>{label.replace(/Closed skyways through main thoroughfares/, 'Closed thoroughfares').replace(/Areas that feel empty or unwelcoming/, 'Empty / unwelcoming').replace(/Inconsistent hours across buildings/, 'Inconsistent hours').replace(/Connections that are frequently closed or locked/, 'Locked connections').replace(/Closed or vacant businesses/, 'Vacant businesses')}</div>
                <div className="track">
                  <div className="fill" style={{ width: `${(n/challengeMax)*100}%` }} />
                </div>
                <div className="num">{n}</div>
              </div>
            ))}
          </div>
        </div>
      </div>

      <div className="card" style={{marginBottom: 14}}>
        <div className="spread" style={{marginBottom: 14}}>
          <div>
            <h3>AI-clustered themes</h3>
            <div className="h-sub">From open-text answers across all 679 responses</div>
          </div>
          <button className="btn ghost" onClick={() => onJumpToTheme && onJumpToTheme()}>View all clusters →</button>
        </div>
        <div className="theme-grid">
          {themeList.map(t => (
            <button className={`theme-tile ${t.key}`} key={t.key}
              onClick={() => onJumpToResponses && onJumpToResponses({theme: t.key})}>
              <div className="label">{t.label}</div>
              <div className="meta">{t.count} responses · {Math.round(t.count/total*100)}% mention</div>
              <div className="spark">{t.count}</div>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// === Filter rail (shared between Responses + Charts) ===
function FilterRail({ data, filtered, filters, toggleFilter, resetFilters, extraClass = '', onClose }) {
  const groupCount = (group, val) => {
    return data.filter(r => {
      const f2 = { ...filters, [group]: [val] };
      if (f2.audience.length && !f2.audience.includes(r.audience)) return false;
      if (f2.frequency.length && !f2.frequency.includes(r.frequency)) return false;
      if (f2.age.length && !f2.age.includes(r.age)) return false;
      if (f2.duration.length && !f2.duration.includes(r.duration)) return false;
      if (f2.sentiment.length && !f2.sentiment.includes(r.sentiment)) return false;
      if (f2.theme.length && !f2.theme.some(t => r.themes.includes(t))) return false;
      return true;
    }).length;
  };

  const themeCounts = {};
  data.forEach(r => r.themes.forEach(x => themeCounts[x] = (themeCounts[x]||0)+1));

  return (
    <div className={`filter-rail ${extraClass}`}>
      <div style={{marginBottom: 16}}>
        <div className="spread" style={{marginBottom: 6}}>
          <div style={{fontFamily: 'var(--serif)', fontSize: 16, fontWeight: 500}}>Filters</div>
          <div style={{display: 'flex', gap: 4}}>
            <button className="btn ghost" style={{padding:'2px 8px', fontSize: 11, color: 'var(--ink-3)'}}
              onClick={resetFilters}>
              Reset
            </button>
            {onClose && (
              <button className="btn ghost rail-close" style={{padding:'2px 8px', fontSize: 14, lineHeight: 1, color: 'var(--ink-3)'}}
                onClick={onClose} aria-label="Close filters">×</button>
            )}
          </div>
        </div>
        <div className="muted" style={{fontSize: 12}}>{filtered.length} of {data.length} match</div>
      </div>

      {Object.entries(window.FILTER_GROUPS).map(([group, opts]) => (
        <div className="filter-group" key={group}>
          <h4>{group === 'audience' ? 'Relationship' : group}</h4>
          <div className="filter-opts">
            {opts.map(opt => (
              <div key={opt.val}
                className={`filter-opt ${filters[group].includes(opt.val) ? 'on' : ''}`}
                onClick={() => toggleFilter(group, opt.val)}>
                <span>{opt.label}</span>
                <span className="count">{groupCount(group, opt.val)}</span>
              </div>
            ))}
          </div>
        </div>
      ))}

      <div className="filter-group">
        <h4>AI Themes</h4>
        <div className="filter-opts">
          {(window.SURVEY_THEMES||[]).slice().sort((a,b)=>(themeCounts[b.key]||0)-(themeCounts[a.key]||0)).map(t => (
            <div key={t.key}
              className={`filter-opt ${filters.theme.includes(t.key) ? 'on' : ''}`}
              onClick={() => toggleFilter('theme', t.key)}>
              <span>{t.label}</span>
              <span className="count">{themeCounts[t.key]||0}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

const SORT_OPTIONS = [
  { val: 'newest',    label: 'Newest first' },
  { val: 'oldest',    label: 'Oldest first' },
  { val: 'highest',   label: 'Highest rated' },
  { val: 'lowest',    label: 'Lowest rated' },
  { val: 'longest',   label: 'Longest response' },
];
function sortFn(key) {
  const textLen = r => (r.likeBest||'').length + (r.urgentAction||'').length + (r.beyondCommuting||'').length + (r.anythingElse||'').length;
  switch (key) {
    case 'oldest':  return (a, b) => parseTs(a.timestamp) - parseTs(b.timestamp);
    case 'highest': return (a, b) => (b.rating||0) - (a.rating||0) || parseTs(b.timestamp) - parseTs(a.timestamp);
    case 'lowest':  return (a, b) => (a.rating||6) - (b.rating||6) || parseTs(b.timestamp) - parseTs(a.timestamp);
    case 'longest': return (a, b) => textLen(b) - textLen(a);
    default:        return (a, b) => parseTs(b.timestamp) - parseTs(a.timestamp);
  }
}

// === Responses page (filter rail + list + detail) ===
function ResponsesPage({ data, filtered, filters, toggleFilter, resetFilters,
  flags, tags, onToggleFlag, onAddTag, onFindSimilar }) {
  const [selectedId, setSelectedId] = useState(null);
  const [sortKey, setSortKey] = useState('newest');
  const [sortOpen, setSortOpen] = useState(false);
  const [filtersOpen, setFiltersOpen] = useState(false);
  const [detailOpen, setDetailOpen] = useState(false);

  const activeFilterCount = Object.values(filters).reduce((s, arr) => s + arr.length, 0);

  const sorted = useMemo(
    () => [...filtered].sort(sortFn(sortKey)),
    [filtered, sortKey]
  );

  // Auto-select first when filter/sort changes
  useEffect(() => {
    if (sorted.length && !sorted.find(r => r.id === selectedId)) {
      setSelectedId(sorted[0].id);
    }
  }, [sorted, selectedId]);

  const selected = sorted.find(r => r.id === selectedId);
  const sortLabel = SORT_OPTIONS.find(o => o.val === sortKey)?.label || 'Sort';

  // Close sort menu on outside click
  useEffect(() => {
    if (!sortOpen) return;
    const off = (e) => { if (!e.target.closest('.sort-menu-wrap')) setSortOpen(false); };
    document.addEventListener('click', off);
    return () => document.removeEventListener('click', off);
  }, [sortOpen]);

  return (
    <div className="responses">
      {filtersOpen && <div className="filter-backdrop" onClick={() => setFiltersOpen(false)} />}
      <FilterRail data={data} filtered={filtered} filters={filters}
        toggleFilter={toggleFilter} resetFilters={resetFilters}
        extraClass={filtersOpen ? 'open' : ''}
        onClose={() => setFiltersOpen(false)} />

      {/* List */}
      <div className="list-pane">
        <ResponsesSummaryStrip data={filtered} total={data.length} />
        <div className="list-head">
          <button className="filters-toggle" onClick={() => setFiltersOpen(true)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M3 6h18M6 12h12M10 18h4"/></svg>
            Filters
            {activeFilterCount > 0 && <span className="chip">{activeFilterCount}</span>}
          </button>
          <div className="label"><strong>{filtered.length}</strong> responses</div>
          <div className="sort-menu-wrap" style={{display: 'flex', gap: 4, position: 'relative'}}>
            <button className="btn ghost" style={{padding:'4px 8px', fontSize: 11}}
              onClick={(e) => { e.stopPropagation(); setSortOpen(v => !v); }}>
              {sortLabel} ▾
            </button>
            {sortOpen && (
              <div className="sort-menu">
                {SORT_OPTIONS.map(o => (
                  <button key={o.val} className={`sort-menu-item ${sortKey === o.val ? 'on' : ''}`}
                    onClick={() => { setSortKey(o.val); setSortOpen(false); }}>
                    {o.label}
                  </button>
                ))}
              </div>
            )}
          </div>
        </div>
        {sorted.slice(0, 200).map(r => (
          <div key={r.id}
            className={`resp-card ${selectedId === r.id ? 'active' : ''}`}
            onClick={() => { setSelectedId(r.id); setDetailOpen(true); }}>
            <div className="row1">
              <span className="id">{r.id}{flags[r.id] ? ' ⚐' : ''}</span>
              <span className="stars">
                {[1,2,3,4,5].map(i => <span key={i} className={i<=(r.rating||0) ? '' : 'empty'}>★</span>)}
              </span>
              <span className="audience">{r.audience}</span>
            </div>
            <div className="preview">
              {r.likeBest || r.urgentAction || r.anythingElse || r.beyondCommuting || '—'}
            </div>
            <div className="meta">
              <Pill kind={sentimentKind(r.sentiment)}>{r.sentiment}</Pill>
              {r.themes.slice(0,2).map(t => (
                <Pill key={t} kind="outline">
                  {(window.SURVEY_THEMES||[]).find(x=>x.key===t)?.label || t}
                </Pill>
              ))}
              {(tags[r.id]||[]).map(t => <Pill key={t} kind="gold">{t}</Pill>)}
            </div>
          </div>
        ))}
        {sorted.length === 0 && (
          <div style={{padding: 40, textAlign: 'center', color: 'var(--ink-4)'}}>No responses match these filters.</div>
        )}
      </div>

      {/* Detail */}
      <div className={`detail-pane ${detailOpen ? 'open' : ''}`}>
        {selected
          ? <Detail r={selected}
              isFlagged={!!flags[selected.id]}
              tags={tags[selected.id] || []}
              onBack={() => setDetailOpen(false)}
              onToggleFlag={() => onToggleFlag(selected.id)}
              onAddTag={() => onAddTag(selected.id)}
              onFindSimilar={() => { onFindSimilar(selected); setDetailOpen(false); setFiltersOpen(false); }}
              onExportRow={() => downloadCSV([selected], `skyway-survey-${selected.id}.csv`)} />
          : <div className="detail-empty">Select a response to read.</div>}
      </div>
    </div>
  );
}

function Detail({ r, isFlagged, tags, onBack, onToggleFlag, onAddTag, onFindSimilar, onExportRow }) {
  const themes = (r.themes||[]).map(k => (window.SURVEY_THEMES||[]).find(t => t.key === k)).filter(Boolean);
  return (
    <>
      {onBack && (
        <button className="detail-back" onClick={onBack}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="m15 18-6-6 6-6"/></svg>
          Back to list
        </button>
      )}
    <div className="detail">
      <div className="detail-head">
        <div className="detail-id">RESPONSE {r.id} · {r.timestamp}</div>
        <div className="detail-quote">
          “{r.likeBest || r.urgentAction || r.beyondCommuting || r.anythingElse || 'No open-text response provided.'}”
        </div>
        <div className="detail-meta-row">
          <Stars n={r.rating} size={14} />
          <Pill kind={sentimentKind(r.sentiment)}>{r.sentiment} sentiment</Pill>
          <Pill kind="brick">{r.audience}</Pill>
          <Pill kind="outline">{r.frequency}</Pill>
        </div>
      </div>

      <div className="ai-summary">
        <div className="ai-tag">
          <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2 14 9 21 11 14 13 12 20 10 13 3 11 10 9z"/></svg>
          AI summary
        </div>
        <p>{aiSummary(r)}</p>
        <div className="ai-tags">
          {themes.map(t => <Pill key={t.key} kind="gold">{t.label}</Pill>)}
          {r.rating <= 2 && <Pill kind="neg">Low-rating outlier</Pill>}
          {r.rating === 5 && <Pill kind="pos">Champion</Pill>}
          {r.hasQuote && <Pill kind="forest">Quote candidate</Pill>}
          {isFlagged && <Pill kind="brick">Flagged</Pill>}
          {(tags||[]).map(t => <Pill key={t} kind="gold">{t}</Pill>)}
        </div>
      </div>

      <div className="profile-strip">
        <div className="cell"><div className="ck">Frequency</div><div className="cv">{r.frequency || '—'}</div></div>
        <div className="cell"><div className="ck">Age</div><div className="cv">{r.age || '—'}</div></div>
        <div className="cell"><div className="ck">Years using</div><div className="cv">{r.duration || '—'}</div></div>
        <div className="cell"><div className="ck">Nav ease</div><div className="cv">{r.navEase ? `${r.navEase}/5` : '—'}</div></div>
      </div>

      <div className="qa-block">
        <div className="q">When they use it</div>
        <div className="a">
          <div className="tag-list">
            {(r.times||[]).map(t => <Pill key={t} kind="sky">{t}</Pill>)}
            {!r.times.length && <span className="a-empty">Not specified</span>}
          </div>
        </div>
      </div>

      <div className="qa-block">
        <div className="q">Primary reasons</div>
        <div className="a">
          <div className="tag-list">
            {(r.reasons||[]).map(t => <Pill key={t} kind="forest">{t}</Pill>)}
          </div>
        </div>
      </div>

      <div className="qa-block">
        <div className="q">Top frustrations cited</div>
        <div className="a">
          <div className="tag-list">
            {(r.challenges||[]).map(t => <Pill key={t} kind="neg">{t}</Pill>)}
            {!r.challenges.length && <span className="a-empty">None reported</span>}
          </div>
        </div>
      </div>

      <div className="qa-block">
        <div className="q">Improvements they'd prioritize</div>
        <div className="a">
          <div className="tag-list">
            {(r.improvements||[]).map(t => <Pill key={t} kind="gold">{t}</Pill>)}
          </div>
        </div>
      </div>

      {r.likeBest && (
        <div className="qa-block">
          <div className="q">What they like best</div>
          <div className="a serif">{r.likeBest}</div>
        </div>
      )}

      {r.urgentAction && (
        <div className="qa-block">
          <div className="q">Single most urgent action</div>
          <div className="a serif">{r.urgentAction}</div>
        </div>
      )}

      {r.beyondCommuting && (
        <div className="qa-block">
          <div className="q">What would make them spend more time</div>
          <div className="a serif">{r.beyondCommuting}</div>
        </div>
      )}

      {r.unsafeLocations && (
        <div className="qa-block">
          <div className="q">Where they feel unsafe</div>
          <div className="a serif">{r.unsafeLocations}</div>
        </div>
      )}

      {r.buildings && (
        <div className="qa-block">
          <div className="q">Buildings / sections used</div>
          <div className="a serif">{r.buildings}</div>
        </div>
      )}

      {r.anythingElse && (
        <div className="qa-block">
          <div className="q">Anything else</div>
          <div className="a serif">{r.anythingElse}</div>
        </div>
      )}

      {r.moveReasons && (
        <div className="qa-block">
          <div className="q">Why they moved downtown</div>
          <div className="a serif">{r.moveReasons}</div>
        </div>
      )}

      <div className="detail-actions">
        <button className={`btn ${isFlagged ? 'primary' : ''}`} onClick={onToggleFlag}>
          {isFlagged ? '⚐ Flagged' : 'Flag for follow-up'}
        </button>
        <button className="btn" onClick={onAddTag}>Add tag</button>
        <button className="btn ghost" onClick={onFindSimilar}>Find similar →</button>
        <div style={{marginLeft: 'auto'}}>
          <button className="btn ghost" style={{color: 'var(--ink-4)'}} onClick={onExportRow}>Export row</button>
        </div>
      </div>
    </div>
    </>
  );
}

// === Themes page ===
function ThemesPage({ data, onJump }) {
  const themeMap = {};
  data.forEach(r => r.themes.forEach(t => {
    if (!themeMap[t]) themeMap[t] = [];
    themeMap[t].push(r);
  }));

  const clusters = (window.AI_CLUSTERS || []).map(c => {
    const responses = themeMap[c.matchKey] || [];
    // Pick best 2 quotes
    const quotes = responses
      .filter(r => r.hasQuote && (r.likeBest || r.urgentAction || r.anythingElse))
      .sort((a,b) => Math.abs(b.sentScore) - Math.abs(a.sentScore))
      .slice(0, 2)
      .map(r => ({
        text: (r.likeBest || r.urgentAction || r.anythingElse).replace(/\s+/g, ' ').slice(0, 220),
        id: r.id,
        audience: r.audience,
      }));
    return { ...c, count: responses.length, quotes };
  });

  return (
    <div className="themes-page paper-grain">
      <div className="page-head">
        <div>
          <div className="kicker">AI Theme Clusters</div>
          <h1>What people are actually saying.</h1>
          <div className="lede">Eight clusters auto-extracted from the four open-text fields. Each tile shows the cluster's size, a synthesized description, and two representative quotes. Click any cluster to filter the response list.</div>
        </div>
        <div style={{display:'flex', gap: 6}}>
          <button className="btn ghost" onClick={() => downloadCSV(data, 'skyway-survey-all.csv')}>
            Export all (CSV)
          </button>
          <button className="btn primary" disabled
            title="Re-clustering requires a server endpoint that calls Claude on open-text answers. Not built yet — the clusters shown were generated from the real data when this dashboard was built. Ask Claude Code to wire up /api/recluster.">
            Re-cluster themes
          </button>
        </div>
      </div>

      <div className="cluster-grid">
        {clusters.map((c, i) => (
          <div className="cluster" key={c.key}>
            <div className="cluster-head">
              <div className="cluster-num">{String(i+1).padStart(2, '0')}</div>
              <div style={{flex: 1}}>
                <h3>{c.title}</h3>
                <div className="desc">{c.description}</div>
                <div style={{display:'flex', gap: 6, marginTop: 10}}>
                  <Pill kind={c.sentiment === 'positive' ? 'pos' : c.sentiment === 'negative' ? 'neg' : 'neu'}>
                    {c.sentiment}
                  </Pill>
                  <Pill kind="outline">{c.count} responses</Pill>
                  <button className="btn ghost" style={{padding: '0 6px', fontSize: 11, marginLeft: 'auto'}}
                    onClick={() => onJump && onJump({theme: c.matchKey})}>
                    See all →
                  </button>
                </div>
              </div>
            </div>
            {c.quotes.length > 0 && (
              <div className="quotes">
                {c.quotes.map((q, qi) => (
                  <div className="quote" key={qi}>
                    "{q.text}" <span className="who">— {q.id} · {q.audience}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

// Build a markdown report from the dataset for download.
function buildInsightsReport(data) {
  const total = data.length;
  const ratedCount = data.filter(r => r.rating).length;
  const avg = ratedCount ? (data.reduce((s,r)=>s+(r.rating||0),0)/ratedCount).toFixed(2) : '—';
  const sCounts = { positive: 0, neutral: 0, negative: 0 };
  data.forEach(r => { if (sCounts[r.sentiment] != null) sCounts[r.sentiment]++; });
  const themeCounts = {};
  data.forEach(r => (r.themes||[]).forEach(t => themeCounts[t] = (themeCounts[t]||0)+1));
  const topThemes = (window.SURVEY_THEMES||[])
    .map(t => ({ ...t, n: themeCounts[t.key] || 0 }))
    .sort((a,b)=>b.n-a.n);
  const reasonCounts = {};
  data.forEach(r => (r.reasons||[]).forEach(x => reasonCounts[x] = (reasonCounts[x]||0)+1));
  const topReasons = Object.entries(reasonCounts).sort((a,b)=>b[1]-a[1]).slice(0,8);
  const chalCounts = {};
  data.forEach(r => (r.challenges||[]).forEach(x => chalCounts[x] = (chalCounts[x]||0)+1));
  const topChal = Object.entries(chalCounts).sort((a,b)=>b[1]-a[1]).slice(0,8);

  const lines = [];
  lines.push(`# Skyway Survey · Insights report`);
  lines.push('');
  lines.push(`Generated ${new Date().toISOString().slice(0,10)} · Friends of the Saint Paul Skyway`);
  lines.push('');
  lines.push(`## Summary`);
  lines.push('');
  lines.push(`- **Responses:** ${total}`);
  lines.push(`- **Average rating:** ${avg} / 5`);
  lines.push(`- **Sentiment:** ${sCounts.positive} positive · ${sCounts.neutral} neutral · ${sCounts.negative} negative`);
  lines.push('');
  lines.push(`## Top themes`);
  lines.push('');
  topThemes.forEach(t => lines.push(`- **${t.label}** — ${t.n} mentions (${total ? Math.round(t.n/total*100) : 0}%)`));
  lines.push('');
  lines.push(`## Top reasons people use it`);
  lines.push('');
  topReasons.forEach(([l, n]) => lines.push(`- ${l} — ${n}`));
  lines.push('');
  lines.push(`## Top frustrations`);
  lines.push('');
  topChal.forEach(([l, n]) => lines.push(`- ${l} — ${n}`));
  lines.push('');
  lines.push(`## Outliers`);
  lines.push('');
  (window.OUTLIERS||[]).forEach(o => lines.push(`> ${o.text}\n> _— ${o.reason}_\n`));
  return lines.join('\n');
}

// === Insights page ===
function InsightsPage({ data }) {
  // Sentiment heatmap: theme x audience
  const themes = (window.SURVEY_THEMES||[]).slice(0, 6);
  const audiences = ['resident', 'live-work', 'worker', 'visitor'];

  function avgSentForCell(themeKey, aud) {
    const subset = data.filter(r => r.audience === aud && r.themes.includes(themeKey));
    if (!subset.length) return null;
    const avg = subset.reduce((s, r) => s + r.sentScore, 0) / subset.length;
    return { avg, n: subset.length };
  }

  function cellColor(avg) {
    if (avg == null) return 'var(--paper-2)';
    if (avg < -2) return '#B0492C';
    if (avg < 0) return '#E0A88E';
    if (avg < 2) return '#E8D9A8';
    return '#7B9F6E';
  }
  function cellTextColor(avg) {
    if (avg == null) return 'var(--ink-4)';
    if (avg < -2) return '#FFFFFF';
    if (avg > 2) return '#FFFFFF';
    return 'var(--ink)';
  }

  // Top quote candidates
  const quotePool = data
    .filter(r => r.hasQuote && r.likeBest && r.likeBest.length > 60 && r.likeBest.length < 280)
    .sort((a,b) => b.sentScore - a.sentScore);
  const topPos = quotePool.slice(0, 3);
  const topNeg = data
    .filter(r => r.hasQuote && r.urgentAction && r.urgentAction.length > 40 && r.urgentAction.length < 280)
    .sort((a,b) => a.sentScore - b.sentScore)
    .slice(0, 3);

  return (
    <div className="insights paper-grain">
      <div className="page-head">
        <div>
          <div className="kicker">AI Insights</div>
          <h1>Cross-cuts the obvious answers don't show.</h1>
          <div className="lede">Sentiment heatmap, the most quotable responses for use in reports, and outliers worth a human read.</div>
        </div>
        <button className="btn primary"
          onClick={() => downloadBlob(buildInsightsReport(data), 'skyway-survey-report.md', 'text/markdown;charset=utf-8')}>
          Download report (Markdown)
        </button>
      </div>

      <div className="card" style={{marginBottom: 22}}>
        <div className="spread" style={{marginBottom: 4}}>
          <h3>Sentiment by theme × audience</h3>
        </div>
        <div className="h-sub">Color = average sentiment score within cell. Cell label = sample size.</div>
        <div className="heatmap-scroll">
          <div className="heatmap">
            <div className="hcell h"></div>
            {audiences.map(a => <div className="hcell col-h" key={a}>{a}</div>)}
            {themes.map(t => (
              <React.Fragment key={t.key}>
                <div className="hcell h">{t.label}</div>
                {audiences.map(a => {
                  const c = avgSentForCell(t.key, a);
                  return (
                    <div className="hcell" key={a}
                      style={{background: cellColor(c?.avg), color: cellTextColor(c?.avg)}}>
                      {c ? `${c.avg.toFixed(1)} · n=${c.n}` : '—'}
                    </div>
                  );
                })}
              </React.Fragment>
            ))}
          </div>
        </div>
        <div style={{display:'flex', gap: 10, marginTop: 14, alignItems:'center', fontSize: 11, color:'var(--ink-3)', flexWrap: 'wrap'}}>
          <span>Sentiment scale</span>
          <span style={{display:'inline-block', width:18, height:14, background:'#B0492C', borderRadius: 3}} />
          <span>negative</span>
          <span style={{display:'inline-block', width:18, height:14, background:'#E0A88E', borderRadius: 3}} />
          <span>leaning negative</span>
          <span style={{display:'inline-block', width:18, height:14, background:'#E8D9A8', borderRadius: 3}} />
          <span>neutral</span>
          <span style={{display:'inline-block', width:18, height:14, background:'#7B9F6E', borderRadius: 3}} />
          <span>positive</span>
        </div>
      </div>

      <h3 style={{fontFamily: 'var(--serif)', fontSize: 22, fontWeight: 500, margin: '0 0 4px', letterSpacing: '-0.01em'}}>
        Pull-quote reel
      </h3>
      <div className="muted" style={{marginBottom: 14, fontSize: 13}}>Auto-selected from open-text — best for reports & social.</div>
      <div className="quote-reel" style={{marginBottom: 30}}>
        {[...topPos, ...topNeg].slice(0, 6).map(r => (
          <div className="big-quote" key={r.id}>
            <blockquote>{r.likeBest || r.urgentAction}</blockquote>
            <div className="attr">
              <span>{r.id}</span>
              <span>·</span>
              <span>{r.audience}</span>
              <span>·</span>
              <span>{r.duration}</span>
              <span style={{marginLeft: 'auto'}}>
                <Pill kind={sentimentKind(r.sentiment)}>{r.sentiment}</Pill>
              </span>
            </div>
          </div>
        ))}
      </div>

      <h3 style={{fontFamily: 'var(--serif)', fontSize: 22, fontWeight: 500, margin: '0 0 4px', letterSpacing: '-0.01em'}}>
        Outliers worth reading
      </h3>
      <div className="muted" style={{marginBottom: 14, fontSize: 13}}>Unusual viewpoints, surprising framings, things that don't cluster cleanly.</div>
      <div className="outliers">
        {window.OUTLIERS.map((o, i) => (
          <div className="outlier-card" key={i}>
            <div className="outlier-flag">⚐ {o.reason}</div>
            <div className="outlier-text">"{o.text}"</div>
          </div>
        ))}
      </div>
    </div>
  );
}

// === Chart helpers (SVG, theme-matched) ===

// Horizontal bar list. Pass [{label, n, tone?}], color tone in: brick(default), forest, sky, gold
function HBarList({ rows, max, format }) {
  const m = max || Math.max(1, ...rows.map(r => r.n));
  return (
    <div className="barchart">
      {rows.map(r => (
        <div className="bar-row" key={r.label} title={`${r.label}: ${r.n}`}>
          <div className="label">{r.label}</div>
          <div className="track">
            <div className={`fill ${r.tone || ''}`} style={{ width: `${(r.n/m)*100}%` }} />
          </div>
          <div className="num">{format ? format(r.n) : r.n}</div>
        </div>
      ))}
      {rows.length === 0 && <div className="muted" style={{fontSize: 12, padding: '8px 0'}}>No data in current filter.</div>}
    </div>
  );
}

// Vertical column chart for ordinal scales (e.g. ratings)
function VColumns({ cols, color = 'brick' }) {
  const max = Math.max(1, ...cols.map(c => c.n));
  const colorVar = {
    brick: 'linear-gradient(180deg, #C26243, var(--brick))',
    forest: 'linear-gradient(180deg, #466F4C, var(--forest))',
    sky: 'linear-gradient(180deg, #5478A0, var(--sky))',
    gold: 'linear-gradient(180deg, #C9A347, var(--gold))',
  }[color];
  return (
    <div className="vcols">
      {cols.map((c, i) => (
        <div className="vcol" key={i}>
          <div className="vcol-track">
            <div className="vcol-bar" style={{ height: `${(c.n/max)*100}%`, background: colorVar }}>
              <span className="vcol-v">{c.n}</span>
            </div>
          </div>
          <div className="vcol-lab">{c.label}</div>
          {c.sub && <div className="vcol-sub">{c.sub}</div>}
        </div>
      ))}
    </div>
  );
}

// Donut SVG. slices: [{label, n, color}]
function Donut({ slices, size = 160, thickness = 26, centerLabel, centerSub }) {
  const total = slices.reduce((s, x) => s + x.n, 0);
  const r = (size - thickness) / 2;
  const cx = size / 2, cy = size / 2;
  const c = 2 * Math.PI * r;
  let acc = 0;

  return (
    <div className="donut-wrap">
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        <circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--paper-3)" strokeWidth={thickness} />
        {total > 0 && slices.map((s, i) => {
          const len = (s.n / total) * c;
          const dash = `${len} ${c - len}`;
          const offset = c * 0.25 - acc;
          acc += len;
          return (
            <circle key={i} cx={cx} cy={cy} r={r} fill="none"
              stroke={s.color} strokeWidth={thickness}
              strokeDasharray={dash} strokeDashoffset={offset}
              transform={`rotate(0 ${cx} ${cy})`} />
          );
        })}
        {centerLabel != null && (
          <g>
            <text x="50%" y="50%" textAnchor="middle" dominantBaseline="central"
              style={{fontFamily: 'var(--serif)', fontWeight: 500, fontSize: 28, fill: 'var(--ink)', letterSpacing: '-0.02em'}}>
              {centerLabel}
            </text>
            {centerSub && (
              <text x="50%" y="64%" textAnchor="middle"
                style={{fontFamily: 'var(--sans)', fontSize: 10, fill: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.1em'}}>
                {centerSub}
              </text>
            )}
          </g>
        )}
      </svg>
      <div className="donut-legend">
        {slices.map((s, i) => (
          <div className="donut-leg" key={i}>
            <span className="dot" style={{background: s.color}} />
            <span className="lab">{s.label}</span>
            <span className="num">{s.n}</span>
            <span className="pct">{total ? Math.round(s.n/total*100) : 0}%</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// === Compact summary strip for top of Responses list pane ===
function ResponsesSummaryStrip({ data, total }) {
  const n = data.length;
  const rated = data.filter(r => r.rating);
  const avg = rated.length ? (rated.reduce((s, r) => s + r.rating, 0) / rated.length) : 0;
  const sentCounts = { positive: 0, neutral: 0, negative: 0 };
  data.forEach(r => { if (sentCounts[r.sentiment] != null) sentCounts[r.sentiment]++; });
  const ratingDist = [1,2,3,4,5].map(i => data.filter(r => r.rating === i).length);
  const ratingMax = Math.max(1, ...ratingDist);
  const sMax = Math.max(1, sentCounts.positive, sentCounts.neutral, sentCounts.negative);

  return (
    <div className="summary-strip">
      <div className="ss-cell">
        <div className="ss-k">Filtered</div>
        <div className="ss-v">{n}<span className="ss-of">/ {total}</span></div>
      </div>
      <div className="ss-cell">
        <div className="ss-k">Avg rating</div>
        <div className="ss-v">{n ? avg.toFixed(2) : '—'}<span className="ss-of">/ 5</span></div>
      </div>
      <div className="ss-cell wide">
        <div className="ss-k">Rating distribution</div>
        <div className="ss-mini">
          {ratingDist.map((v, i) => (
            <div key={i} className="ss-mini-col" title={`${i+1}★: ${v}`}>
              <div className="ss-mini-track">
                <div className="ss-mini-bar" style={{height: `${(v/ratingMax)*100}%`}} />
              </div>
              <div className="ss-mini-lab">{i+1}</div>
            </div>
          ))}
        </div>
      </div>
      <div className="ss-cell wide">
        <div className="ss-k">Sentiment</div>
        <div className="ss-sent">
          <div className="ss-sent-row" title={`Positive: ${sentCounts.positive}`}>
            <span className="ss-sent-lab">+</span>
            <span className="ss-sent-track"><span className="ss-sent-fill pos" style={{width: `${(sentCounts.positive/sMax)*100}%`}} /></span>
            <span className="ss-sent-num">{sentCounts.positive}</span>
          </div>
          <div className="ss-sent-row" title={`Neutral: ${sentCounts.neutral}`}>
            <span className="ss-sent-lab">○</span>
            <span className="ss-sent-track"><span className="ss-sent-fill neu" style={{width: `${(sentCounts.neutral/sMax)*100}%`}} /></span>
            <span className="ss-sent-num">{sentCounts.neutral}</span>
          </div>
          <div className="ss-sent-row" title={`Negative: ${sentCounts.negative}`}>
            <span className="ss-sent-lab">−</span>
            <span className="ss-sent-track"><span className="ss-sent-fill neg" style={{width: `${(sentCounts.negative/sMax)*100}%`}} /></span>
            <span className="ss-sent-num">{sentCounts.negative}</span>
          </div>
        </div>
      </div>
    </div>
  );
}

// === Charts page (filter rail + grid of detailed charts) ===
function ChartsPage({ data, filtered, filters, toggleFilter, resetFilters }) {
  const [filtersOpen, setFiltersOpen] = useState(false);
  const activeFilterCount = Object.values(filters).reduce((s, arr) => s + arr.length, 0);
  const n = filtered.length;
  const rated = filtered.filter(r => r.rating);
  const avg = rated.length ? (rated.reduce((s,r)=>s+r.rating,0)/rated.length) : 0;
  const navRated = filtered.filter(r => r.navEase);
  const navAvg = navRated.length ? (navRated.reduce((s,r)=>s+r.navEase,0)/navRated.length) : 0;

  // Rating distribution
  const ratingCols = [1,2,3,4,5].map(i => ({
    label: `${i}★`,
    n: filtered.filter(r => r.rating === i).length,
  }));

  // Nav-ease distribution
  const navCols = [1,2,3,4,5].map(i => ({
    label: `${i}`,
    n: filtered.filter(r => r.navEase === i).length,
  }));

  // Sentiment donut
  const sCounts = { positive: 0, neutral: 0, negative: 0 };
  filtered.forEach(r => { if (sCounts[r.sentiment] != null) sCounts[r.sentiment]++; });
  const sentSlices = [
    { label: 'Positive', n: sCounts.positive, color: '#7B9F6E' },
    { label: 'Neutral',  n: sCounts.neutral,  color: '#C9A347' },
    { label: 'Negative', n: sCounts.negative, color: '#B0492C' },
  ];

  // Audience donut
  const audColors = {
    'live-work': '#3F5E78',
    'resident':  '#7B9F6E',
    'worker':    '#8B3A3A',
    'visitor':   '#C9A347',
  };
  const audOrder = ['live-work','resident','worker','visitor'];
  const audSlices = audOrder.map(k => ({
    label: ({ 'live-work':'Lives & works', resident:'Resident', worker:'Worker', visitor:'Visitor' })[k],
    n: filtered.filter(r => r.audience === k).length,
    color: audColors[k],
  }));

  // Frequency
  const freqGroup = (window.FILTER_GROUPS && window.FILTER_GROUPS.frequency) || [];
  const freqRows = freqGroup.map(o => ({
    label: o.label,
    n: filtered.filter(r => r.frequency === o.val).length,
    tone: 'sky',
  })).filter(r => r.n > 0).sort((a,b)=>b.n-a.n);

  // Age
  const ageGroup = (window.FILTER_GROUPS && window.FILTER_GROUPS.age) || [];
  const ageRows = ageGroup.map(o => ({
    label: o.label,
    n: filtered.filter(r => r.age === o.val).length,
    tone: 'forest',
  })).filter(r => r.n > 0);

  // Duration
  const durGroup = (window.FILTER_GROUPS && window.FILTER_GROUPS.duration) || [];
  const durRows = durGroup.map(o => ({
    label: o.label,
    n: filtered.filter(r => r.duration === o.val).length,
    tone: 'gold',
  })).filter(r => r.n > 0);

  // Reasons (top 10)
  const reasonCounts = {};
  filtered.forEach(r => (r.reasons||[]).forEach(x => reasonCounts[x] = (reasonCounts[x]||0)+1));
  const reasonRows = Object.entries(reasonCounts)
    .sort((a,b)=>b[1]-a[1]).slice(0, 10)
    .map(([label, n]) => ({ label, n, tone: 'sky' }));

  // Challenges (top 10)
  const chalCounts = {};
  filtered.forEach(r => (r.challenges||[]).forEach(x => chalCounts[x] = (chalCounts[x]||0)+1));
  const chalRows = Object.entries(chalCounts)
    .sort((a,b)=>b[1]-a[1]).slice(0, 10)
    .map(([label, n]) => ({ label, n, tone: '' }));

  // Improvements (top 10)
  const impCounts = {};
  filtered.forEach(r => (r.improvements||[]).forEach(x => impCounts[x] = (impCounts[x]||0)+1));
  const impRows = Object.entries(impCounts)
    .sort((a,b)=>b[1]-a[1]).slice(0, 10)
    .map(([label, n]) => ({ label, n, tone: 'gold' }));

  // Times of day
  const timeCounts = {};
  filtered.forEach(r => (r.times||[]).forEach(x => timeCounts[x] = (timeCounts[x]||0)+1));
  const timeRows = Object.entries(timeCounts)
    .sort((a,b)=>b[1]-a[1])
    .map(([label, n]) => ({ label, n, tone: 'forest' }));

  // Themes
  const themeCounts = {};
  filtered.forEach(r => (r.themes||[]).forEach(x => themeCounts[x] = (themeCounts[x]||0)+1));
  const themeRows = (window.SURVEY_THEMES||[]).map(t => ({
    label: t.label, n: themeCounts[t.key] || 0, tone: '',
  })).sort((a,b)=>b.n-a.n);

  // Avg rating by audience (cross-tab)
  const ratingByAud = audOrder.map(k => {
    const subset = filtered.filter(r => r.audience === k && r.rating);
    return {
      label: ({ 'live-work':'Lives & works', resident:'Resident', worker:'Worker', visitor:'Visitor' })[k],
      n: subset.length ? +(subset.reduce((s,r)=>s+r.rating,0)/subset.length).toFixed(2) : 0,
      tone: 'forest',
    };
  });

  return (
    <div className="charts-layout">
      {filtersOpen && <div className="filter-backdrop" onClick={() => setFiltersOpen(false)} />}
      <FilterRail data={data} filtered={filtered} filters={filters}
        toggleFilter={toggleFilter} resetFilters={resetFilters}
        extraClass={filtersOpen ? 'open' : ''}
        onClose={() => setFiltersOpen(false)} />

      <div className="charts-content paper-grain">
        <div style={{marginBottom: 12}}>
          <button className="filters-toggle" onClick={() => setFiltersOpen(true)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M3 6h18M6 12h12M10 18h4"/></svg>
            Filters
            {activeFilterCount > 0 && <span className="chip">{activeFilterCount}</span>}
          </button>
        </div>
        <div className="page-head" style={{marginBottom: 14}}>
          <div>
            <div className="kicker">Charts · {n} of {data.length} responses</div>
            <h1>The numbers behind the voices.</h1>
            <div className="lede">Every chart updates with the filter rail. Stack relationship × frequency × theme to see how slices of the community differ.</div>
          </div>
          <div style={{display:'flex', gap: 6, paddingBottom: 4, flexWrap: 'wrap'}}>
            <button className="btn" onClick={resetFilters}>Reset filters</button>
            <button className="btn primary"
              onClick={() => downloadCSV(filtered, `skyway-survey-filtered-${filtered.length}.csv`)}>
              Export CSV ({filtered.length})
            </button>
          </div>
        </div>

        {/* KPI strip */}
        <div className="kpi-row" style={{marginBottom: 18}}>
          <div className="kpi">
            <div className="kpi-label">Filtered responses</div>
            <div className="kpi-value">{n}</div>
            <div className="kpi-sub">{data.length ? Math.round(n/data.length*100) : 0}% of total</div>
          </div>
          <div className="kpi">
            <div className="kpi-label">Avg experience</div>
            <div className="kpi-value">{n ? avg.toFixed(2) : '—'}<span style={{fontSize:18,color:'var(--ink-3)',marginLeft:4}}>/5</span></div>
            <div className="kpi-sub">{rated.length} rated</div>
          </div>
          <div className="kpi">
            <div className="kpi-label">Avg nav-ease</div>
            <div className="kpi-value">{n ? navAvg.toFixed(2) : '—'}<span style={{fontSize:18,color:'var(--ink-3)',marginLeft:4}}>/5</span></div>
            <div className="kpi-sub">{navRated.length} reported</div>
          </div>
          <div className="kpi">
            <div className="kpi-label">Positive sentiment</div>
            <div className="kpi-value">{n ? Math.round(sCounts.positive/n*100) : 0}<span style={{fontSize:18,color:'var(--ink-3)',marginLeft:4}}>%</span></div>
            <div className="kpi-sub">{sCounts.positive} of {n}</div>
          </div>
        </div>

        {/* Donuts row */}
        <div className="grid-2" style={{marginBottom: 18}}>
          <div className="card">
            <h3>Sentiment</h3>
            <div className="h-sub">Positive · neutral · negative split for the current filter</div>
            <Donut slices={sentSlices} centerLabel={n ? `${Math.round(sCounts.positive/Math.max(1,n)*100)}%` : '—'} centerSub="positive" />
          </div>
          <div className="card">
            <h3>Relationship to downtown</h3>
            <div className="h-sub">Self-reported relationship</div>
            <Donut slices={audSlices} centerLabel={n} centerSub="responses" />
          </div>
        </div>

        {/* Distributions row */}
        <div className="grid-2" style={{marginBottom: 18}}>
          <div className="card">
            <h3>Experience rating</h3>
            <div className="h-sub">Distribution of 1–5 star answers</div>
            <VColumns cols={ratingCols} color="brick" />
          </div>
          <div className="card">
            <h3>Navigation ease</h3>
            <div className="h-sub">Self-reported ease of getting around (1–5)</div>
            <VColumns cols={navCols} color="forest" />
          </div>
        </div>

        {/* Demographics row */}
        <div className="grid-3" style={{marginBottom: 18}}>
          <div className="card">
            <h3>Frequency of use</h3>
            <div className="h-sub">How often respondents use the system</div>
            <HBarList rows={freqRows} />
          </div>
          <div className="card">
            <h3>Age</h3>
            <div className="h-sub">Age distribution</div>
            <HBarList rows={ageRows} />
          </div>
          <div className="card">
            <h3>Years using</h3>
            <div className="h-sub">Tenure with the system</div>
            <HBarList rows={durRows} />
          </div>
        </div>

        {/* Reasons / Frustrations */}
        <div className="grid-2" style={{marginBottom: 18}}>
          <div className="card">
            <h3>Why they use it</h3>
            <div className="h-sub">Top 10 cited reasons</div>
            <HBarList rows={reasonRows} />
          </div>
          <div className="card">
            <h3>Top frustrations</h3>
            <div className="h-sub">Top 10 cited challenges</div>
            <HBarList rows={chalRows} />
          </div>
        </div>

        {/* Improvements + Times */}
        <div className="grid-2" style={{marginBottom: 18}}>
          <div className="card">
            <h3>Improvements they'd prioritize</h3>
            <div className="h-sub">Top 10 wished-for changes</div>
            <HBarList rows={impRows} />
          </div>
          <div className="card">
            <h3>When they use it</h3>
            <div className="h-sub">Times of day cited</div>
            <HBarList rows={timeRows} />
          </div>
        </div>

        {/* Themes + Cross-tab */}
        <div className="grid-2" style={{marginBottom: 18}}>
          <div className="card">
            <h3>AI-clustered themes</h3>
            <div className="h-sub">Theme prevalence within current filter</div>
            <HBarList rows={themeRows} />
          </div>
          <div className="card">
            <h3>Avg rating by relationship</h3>
            <div className="h-sub">Mean star rating, segmented by relationship to downtown</div>
            <HBarList rows={ratingByAud} max={5} format={v => v.toFixed(2)} />
          </div>
        </div>
      </div>
    </div>
  );
}

// === App ===
function App() {
  const [tab, setTab] = useState('overview');
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState({
    audience: [], frequency: [], age: [], duration: [], sentiment: [], theme: [],
  });
  const [flags, setFlags] = useState(() => readLS(LS_FLAGS, {}));
  const [tags, setTags] = useState(() => readLS(LS_TAGS, {}));

  useEffect(() => writeLS(LS_FLAGS, flags), [flags]);
  useEffect(() => writeLS(LS_TAGS, tags), [tags]);

  const data = window.SURVEY_DATA || [];

  const toggleFilter = useCallback((group, val) => {
    setFilters(f => {
      const cur = f[group] || [];
      const next = cur.includes(val) ? cur.filter(x => x !== val) : [...cur, val];
      return { ...f, [group]: next };
    });
  }, []);
  const resetFilters = useCallback(() => setFilters({
    audience: [], frequency: [], age: [], duration: [], sentiment: [], theme: [],
  }), []);

  const filtered = useMemo(() => {
    const q = (query||'').toLowerCase().trim();
    return data.filter(r => {
      if (filters.audience.length && !filters.audience.includes(r.audience)) return false;
      if (filters.frequency.length && !filters.frequency.includes(r.frequency)) return false;
      if (filters.age.length && !filters.age.includes(r.age)) return false;
      if (filters.duration.length && !filters.duration.includes(r.duration)) return false;
      if (filters.sentiment.length && !filters.sentiment.includes(r.sentiment)) return false;
      if (filters.theme.length && !filters.theme.some(t => r.themes.includes(t))) return false;
      if (q) {
        const hay = [r.likeBest, r.urgentAction, r.anythingElse, r.beyondCommuting, r.buildings, r.unsafeLocations, r.moveReasons, r.otherUse].join(' ').toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
  }, [data, filters, query]);

  const handleJumpToResponses = useCallback((opts = {}) => {
    setFilters(f => ({
      ...f,
      ...(opts.theme ? { theme: [opts.theme] } : {}),
      ...(opts.audience ? { audience: [opts.audience] } : {}),
    }));
    setTab('responses');
  }, []);

  const handleJumpToTheme = useCallback(() => setTab('themes'), []);

  const handleToggleFlag = useCallback((id) => {
    setFlags(f => {
      const next = { ...f };
      if (next[id]) delete next[id]; else next[id] = true;
      return next;
    });
  }, []);

  const handleAddTag = useCallback((id) => {
    const tag = (window.prompt('Add a tag (one word or short phrase):') || '').trim();
    if (!tag) return;
    setTags(t => {
      const cur = t[id] || [];
      if (cur.includes(tag)) return t;
      return { ...t, [id]: [...cur, tag] };
    });
  }, []);

  const handleFindSimilar = useCallback((r) => {
    setFilters({
      audience: r.audience ? [r.audience] : [],
      frequency: [],
      age: [],
      duration: [],
      sentiment: r.sentiment ? [r.sentiment] : [],
      theme: (r.themes || []).slice(0, 2),
    });
    setTab('responses');
  }, []);

  const handleExportFiltered = useCallback(() => {
    if (!filtered.length) {
      window.alert('No responses match the current filters.');
      return;
    }
    downloadCSV(filtered, `skyway-survey-${filtered.length}-responses.csv`);
  }, [filtered]);

  return (
    <div className="app">
      <Topbar tab={tab} setTab={setTab}
        counts={{responses: filtered.length}}
        query={query} setQuery={setQuery}
        onExportFiltered={handleExportFiltered} />
      <div className="page">
        {tab === 'overview' && <OverviewPage data={data}
          onJumpToResponses={handleJumpToResponses}
          onJumpToTheme={handleJumpToTheme} />}
        {tab === 'responses' && <ResponsesPage data={data} filtered={filtered}
          filters={filters} toggleFilter={toggleFilter} resetFilters={resetFilters}
          flags={flags} tags={tags}
          onToggleFlag={handleToggleFlag} onAddTag={handleAddTag}
          onFindSimilar={handleFindSimilar} />}
        {tab === 'charts' && <ChartsPage data={data} filtered={filtered}
          filters={filters} toggleFilter={toggleFilter} resetFilters={resetFilters} />}
        {tab === 'themes' && <ThemesPage data={data} onJump={handleJumpToResponses} />}
        {tab === 'insights' && <InsightsPage data={data} />}
      </div>
    </div>
  );
}

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