/* AISO — main app */

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

// ---------- viewport hook ----------
function useViewport() {
  const [vp, setVp] = useState(() => ({
    w: typeof window !== 'undefined' ? window.innerWidth : 1280,
    h: typeof window !== 'undefined' ? window.innerHeight : 800,
  }));
  useEffect(() => {
    const onR = () => setVp({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener('resize', onR);
    return () => window.removeEventListener('resize', onR);
  }, []);
  return vp;
}

function orbSizeFor(vp, max = 520, frac = 0.42) {
  // narrow viewport → single col → bigger orb (capped by height too)
  if (vp.w < 1100) {
    return Math.min(max, vp.w * 0.78, vp.h * 0.62);
  }
  return Math.min(max, vp.w * frac, vp.h * 0.72);
}

// ---------- default brand (used only as placeholder when no profile exists) ----------
const BRAND = {
  name: 'Bekon AI',
  domain: 'bekon.ai',
  industry: 'AI visibility platform',
  location: 'Online',
  initial: 'B',
};

// ---------- count-up hook ----------
function useCountUp(target, durationMs = 1200, startOn = true) {
  const [value, setValue] = useState(0);
  useEffect(() => {
    if (!startOn) return;
    let raf;
    const start = performance.now();
    const tick = (now) => {
      const t = Math.min(1, (now - start) / durationMs);
      const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
      setValue(Math.round(eased * target));
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, startOn, durationMs]);
  return value;
}

// ---------- topbar ----------
function Topbar({ scene, onLogo, onSettings, onNav, theme, onToggleTheme, withBorder, profile, activeSite, setActiveSite, addSite, onAddAndScan, onSelectSite, authed, onSignIn }) {
  const hasAccount = authed; // only show the account chip when truly signed in (Supabase session)
  return (
    <header className={`topbar ${withBorder ? 'with-border' : ''}`}>
      <div className="topbar-left">
        <button onClick={onLogo} className="logo" style={{ background: 'none', border: 'none', padding: 0 }}>
          <span className="dot" />
          <span className="word">AISO</span>
          <span className="em">observatory</span>
        </button>
        {activeSite && (
          <>
            <span className="topbar-divider" />
            <SiteSwitcher
              profile={profile}
              activeSite={activeSite}
              setActiveSite={setActiveSite}
              addSite={addSite}
              onAddAndScan={onAddAndScan}
              onSelectSite={onSelectSite}
              onOpenSettings={onSettings}
            />
          </>
        )}
      </div>
      <nav className="topnav">
        {activeSite && <a href="#reports" className={scene === 'reports' ? 'on' : ''} onClick={(e) => { e.preventDefault(); onNav && onNav('reports'); }}>Reports</a>}
        {activeSite && <a href="#workflows" className={scene === 'workflows' ? 'on' : ''} onClick={(e) => { e.preventDefault(); onNav && onNav('workflows'); }}>Workflows</a>}
        {activeSite && <span aria-hidden="true" style={{ width: 1, height: 15, background: 'var(--line)', margin: '0 6px', alignSelf: 'center' }} />}
        <a href="#how" className={scene === 'how' ? 'on' : ''} onClick={(e) => { e.preventDefault(); onNav && onNav('how'); }}>How it works</a>
        <a href="#pricing" className={scene === 'pricing' ? 'on' : ''} onClick={(e) => { e.preventDefault(); onNav && onNav('pricing'); }}>Pricing</a>
        <a href="#docs" className={scene === 'docs' ? 'on' : ''} onClick={(e) => { e.preventDefault(); onNav && onNav('docs'); }}>Docs</a>
        {hasAccount ? (
          <button className="account-chip" onClick={onSettings} title="Open settings">
            <span className="avatar">{initials(profile.email || profile.name)}</span>
            <span className="ac-text">
              <span className="ac-name">{profile.name || profile.email.split('@')[0]}</span>
              <span className="ac-sub">{profile.plan}</span>
            </span>
          </button>
        ) : (
          <button className="btn btn-ghost" onClick={onSignIn || onSettings}>Sign in</button>
        )}
        <button className="cog-btn" onClick={onToggleTheme} title={theme === 'light' ? 'Switch to dark' : 'Switch to light'} aria-label="Toggle theme">
          {theme === 'light'
            ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
            : <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>}
        </button>
        <button className={`cog-btn ${scene === 'settings' ? 'on' : ''}`} onClick={onSettings} title="Settings">
          <Cog />
        </button>
      </nav>
    </header>
  );
}

// ---------- landing ----------
function Landing({ onScan, initialDomain }) {
  const [domain, setDomain] = useState(initialDomain || 'bekon.ai');
  const [submitting, setSubmitting] = useState(false);
  const vp = useViewport();
  const orbSize = orbSizeFor(vp);

  const handleSubmit = (e) => {
    e?.preventDefault();
    if (!domain.trim() || submitting) return;
    setSubmitting(true);
    setTimeout(() => onScan(domain.trim()), 280);
  };

  return (
    <section className="container fade-in">
      <div className="hero">
        <div className="hero-copy">
          <div className="eyebrow"><span className="pulse" /> Live across 6 AI models</div>
          <h1>
            See how AI <span className="it">recommends</span><br/>
            <span className="grad">— or doesn’t —</span><br/>
            <span className="it">your brand.</span>
          </h1>
          <p className="lede">
            Put in your domain. In about 30 seconds we run 142 vertical-aware prompts across
            Claude, GPT, Gemini, Perplexity, Grok and DeepSeek, and tell you exactly where you stand.
            And we ship your highest-impact fix for free.
          </p>

          <form className="domain-form" onSubmit={handleSubmit}>
            <span className="prefix">https://</span>
            <input
              type="text"
              value={domain}
              onChange={(e) => setDomain(e.target.value)}
              placeholder="yourbrand.com"
              autoFocus
              spellCheck={false}
              autoComplete="off"
            />
            <button type="submit" className="scan-btn" disabled={submitting}>
              {submitting ? 'Tuning…' : (<>Scan <Arrow /></>)}
            </button>
          </form>

          <div className="assurances">
            <span><Tick /> Free first report</span>
            <span><Tick /> No credit card</span>
            <span><Tick /> ~30 seconds</span>
          </div>

          <div className="example-row">
            <div className="label">Try a sample</div>
            <div className="example-chips">
              {['bekon.ai', 'tryprofound.com', 'peec.ai', 'vercel.com', 'linear.app'].map(d => (
                <button key={d} className="example-chip" onClick={() => { setDomain(d); }}>{d}</button>
              ))}
            </div>
          </div>
        </div>

        <div className="hero-orb-wrap">
          <Orb state="dormant" size={orbSize} />
          <div className="hero-orb-label">
            <span>6 models</span>
            <span>142 prompts</span>
            <span>real-time</span>
          </div>
        </div>
      </div>
    </section>
  );
}

// ---------- ScanReveal — the hero "we see you" moment between scanning and report ----------
function ScanReveal({ domain, discovered, progress, orbSize }) {
  const confidence = Math.round((discovered.confidence || 0.5) * 100);
  const score = useCountUp(confidence, 1600);
  const name = discovered.name || domain;
  const industry = discovered.industry || '';
  const location = discovered.location || '';
  const tagline = discovered.tagline || '';

  return (
    <section className="reveal-stage fade-in">
      <div className="reveal-glow" aria-hidden="true" />
      <div className="reveal-inner">
        <div className="reveal-orb">
          <Orb
            state="revealed"
            score={score}
            scanProgress={progress}
            size={orbSize}
            label="discovery confidence"
            viewTransitionName="aiso-orb"
          />
        </div>
        <div className="reveal-text">
          <div className="reveal-eyebrow">
            <span className="pulse" />
            We see you
          </div>
          <h1 className="reveal-name">
            <span className="grad">{name}</span>
          </h1>
          {tagline && <p className="reveal-tagline">{tagline}</p>}
          <div className="reveal-meta">
            {industry && <span className="reveal-pill">{industry}</span>}
            {location && <span className="reveal-pill">{location}</span>}
            {discovered.wikidata && <span className="reveal-pill mono">{discovered.wikidata.id}</span>}
          </div>
          <div className="reveal-footnote">Auditing the technical foundation now…</div>
        </div>
      </div>
    </section>
  );
}

// ---------- scanning (REAL discovery) ----------
function Scanning({ domain, anthropicKey, onDone }) {
  const [steps, setSteps] = useState([]);
  const [progress, setProgress] = useState({});
  // discovering → settling → revealing → done
  const [phase, setPhase] = useState('discovering');
  const [discovered, setDiscovered] = useState(null);
  const [error, setError] = useState(null);
  const [elapsed, setElapsed] = useState(0);
  const [primer, setPrimer] = useState(null); // initial placeholder line so we don't stare at nothing
  const vp = useViewport();
  // During the reveal moment, the orb takes center stage — bigger.
  const baseOrbSize = orbSizeFor(vp);
  const orbSize = phase === 'revealing' ? Math.min(640, baseOrbSize * 1.15) : baseOrbSize;
  const abortRef = useRef(null);

  useEffect(() => {
    const ctrl = new AbortController();
    abortRef.current = ctrl;
    let cancelled = false;
    const startedAt = performance.now();

    // Immediate visual feedback — within 100ms users see SOMETHING moving
    const primerTimer = setTimeout(() => {
      if (cancelled) return;
      setPrimer({ label: `Reaching ${domain}`, sub: 'fetching homepage, robots, sitemap in parallel' });
    }, 80);

    // tick elapsed seconds
    const elapsedTicker = setInterval(() => {
      if (!cancelled) setElapsed(Math.floor((performance.now() - startedAt) / 1000));
    }, 250);

    // orb ambient breathing — animates arcs continuously during discovery
    const orbTicker = setInterval(() => {
      if (cancelled) { clearInterval(orbTicker); return; }
      setProgress((p) => {
        const next = { ...p };
        for (const m of MODELS) {
          const target = (Math.sin(Date.now() / 800 + m.hue) + 1) / 3 + 0.15;
          next[m.id] = Math.min(0.85, (next[m.id] || 0.05) * 0.9 + target * 0.1);
        }
        return next;
      });
    }, 60);

    (async () => {
      try {
        const result = await window.discoverBrand(domain, {
          anthropicKey,
          signal: ctrl.signal,
          onProgress: (s) => {
            if (cancelled) return;
            setPrimer(null); // real steps started — clear the placeholder
            setSteps((arr) => {
              const idx = arr.findIndex((x) => x.step === s.step);
              if (idx >= 0) {
                const copy = arr.slice();
                copy[idx] = { ...copy[idx], ...s };
                return copy;
              }
              return [...arr, s];
            });
          },
        });
        if (cancelled) return;
        setDiscovered(result);
        // Phase 1: 'settling' — the arcs settle into their final positions (1.2s)
        setPhase('settling');
        clearInterval(orbTicker);
        clearInterval(elapsedTicker);
        const baseConf = result.confidence || 0.5;
        const settled = {};
        MODELS.forEach((m, idx) => {
          settled[m.id] = baseConf * (0.55 + (idx * 0.07));
        });
        setProgress(settled);

        // Phase 2: 'revealing' — orb stays, content fades AROUND it, user reads result (2.8s)
        setTimeout(() => { if (!cancelled) setPhase('revealing'); }, 1200);

        // Phase 3: hand off to report scene
        setTimeout(() => { if (!cancelled) onDone(result); }, 1200 + 2800);
      } catch (e) {
        if (cancelled) return;
        clearInterval(orbTicker);
        clearInterval(elapsedTicker);
        setError(e.message || String(e));
      }
    })();

    return () => {
      cancelled = true;
      clearTimeout(primerTimer);
      clearInterval(orbTicker);
      clearInterval(elapsedTicker);
      ctrl.abort();
    };
  }, [domain]);

  const hasRunningStep = steps.some(s => s.status === 'running');
  const showEscape = elapsed >= 25 && hasRunningStep && !discovered;
  const allSteps = primer && steps.length === 0 ? [{ step: 'connect', status: 'running', label: primer.label, detail: primer.sub }] : steps;

  if (phase === 'revealing' && discovered) {
    return <ScanReveal domain={domain} discovered={discovered} progress={progress} orbSize={orbSize} />;
  }

  return (
    <section className={`container fade-in scan-section phase-${phase}`}>
      <div className="scan-scene">
        <div className="scan-left">
          <div className="scan-orb-wrap">
            <Orb
              state="scanning"
              score={phase === 'settling' && discovered ? Math.round((discovered.confidence || 0.5) * 100) : ''}
              scanProgress={progress}
              size={baseOrbSize}
              label={phase === 'settling' ? 'discovery confidence' : 'discovering'}
              viewTransitionName="aiso-orb"
            />
          </div>
        </div>

        <div className="scan-right">
          <div className="scan-meta">
            <span style={{ fontFamily: 'var(--mono)' }}>{domain}</span>
            {discovered?.industry && <><span style={{ opacity: 0.5 }}>·</span><span>{discovered.industry}</span></>}
            {discovered?.location && <><span style={{ opacity: 0.5 }}>·</span><span>{discovered.location}</span></>}
            <span style={{ opacity: 0.5 }}>·</span>
            <span style={{ fontFamily: 'var(--mono)', color: 'var(--fg-3)' }}>{elapsed}s</span>
          </div>
          <h2 className="scan-status">
            {error ? (<>Discovery <span style={{ color: 'var(--bad)' }}>hit a wall</span>.</>)
              : phase === 'settling' ? (<>Got it. <span className="grad">{discovered?.name || 'Brand'}</span> identified.</>)
              : (<>Reading <span className="grad">{domain}</span><span className="dots" /></>)}
          </h2>
          {error && (
            <p style={{ color: 'var(--fg-2)', maxWidth: '50ch' }}>
              {error}. We'll show you the demo report instead.
            </p>
          )}

          <div className="scan-log">
            {allSteps.map((s, i) => (
              <div key={s.step + i} className={`line ${s.status === 'pass' ? 'found' : s.status === 'fail' ? 'miss' : 'running'}`}>
                <div className="ico" style={{
                  background: s.status === 'pass' ? 'oklch(82% 0.16 152 / 0.2)'
                    : s.status === 'fail' ? 'oklch(72% 0.18 22 / 0.18)'
                    : 'oklch(78% 0.16 var(--accent-h) / 0.22)',
                  color: s.status === 'pass' ? 'var(--good)'
                    : s.status === 'fail' ? 'var(--bad)'
                    : 'var(--accent)'
                }}>
                  {s.status === 'pass' ? '✓' : s.status === 'fail' ? '×' : '·'}
                </div>
                <div className="model" style={{ minWidth: 100 }}>{s.step}</div>
                <div className="what" style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                  <span>{s.label}</span>
                  {s.detail && <span style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)' }}>{s.detail}</span>}
                </div>
                <div className="tag">{s.status === 'running' ? `${elapsed}s` : s.status}</div>
              </div>
            ))}
          </div>

          {showEscape && (
            <div style={{
              marginTop: 18, padding: '14px 18px',
              borderRadius: 12,
              background: 'oklch(78% 0.15 78 / 0.08)',
              border: '1px solid oklch(78% 0.15 78 / 0.3)',
              display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap',
            }}>
              <span style={{ fontSize: 13.5, color: 'var(--fg-1)' }}>
                Taking longer than usual ({elapsed}s). Some sites block scraping — we'll continue with whatever we have.
              </span>
              <button className="btn" onClick={() => {
                abortRef.current?.abort();
                onDone({
                  domain,
                  name: domain.split('.')[0].replace(/^./, c => c.toUpperCase()),
                  industry: 'unknown',
                  location: 'unknown',
                  confidence: 0.1,
                  degraded: true,
                  knowsFromTraining: false,
                  uncertaintyNote: `Discovery aborted after ${elapsed}s — ${domain} likely blocks scraping.`,
                  discoveredAt: Date.now(),
                  competitors: [],
                  scanPrompts: [],
                });
              }}>Continue anyway →</button>
            </div>
          )}
        </div>
      </div>
    </section>
  );
}

// ---------- discovered-brand summary card (real data, top of report) ----------
function DiscoveredCard({ brand, onSettings }) {
  if (!brand) return null;
  const logo = brand.logo || brand.favicon;
  const hasAddress = brand.address && brand.address.line;
  const social = brand.social || brand.socialLinks || {};
  const socialKeys = Object.keys(social).filter((k) => social[k]);
  const wikidata = brand.wikidata;
  const competitors = (brand.competitors || []).slice(0, 6);
  const confidence = Math.round((brand.confidence || 0.5) * 100);
  const isDegraded = !!brand.degraded;
  const isUnknown = (brand.confidence || 0) < 0.3;

  return (
    <div className={`discovered-card fade-in-up ${isDegraded ? 'is-degraded' : ''} ${isUnknown ? 'is-unknown' : ''}`}>
      {isUnknown ? (
        <div className="degraded-banner">
          <span className="db-dot warn-pulse" />
          <span>
            <strong>We don't know this brand.</strong> {brand.uncertaintyNote || `Claude has no training-data record of ${brand.domain}, and the homepage was ${isDegraded ? 'bot-blocked' : 'thin on signals'}.`} We'll still audit the technical foundation below — fixes apply regardless of brand recognition.
          </span>
        </div>
      ) : isDegraded ? (
        <div className="degraded-banner">
          <span className="db-dot warn-pulse" />
          <span>
            <strong>Limited data:</strong> {brand.domain} blocked our homepage scrape (likely Cloudflare). We pulled robots.txt + sitemap.xml + Wikidata and asked Claude to identify the brand from training data. Confidence: <strong>{confidence}%</strong>.
          </span>
        </div>
      ) : null}
      <div className="dc-left">
        <div className="dc-logo">
          {logo ? <img src={logo} alt="" onError={(e) => { e.currentTarget.style.display = 'none'; }} /> : (brand.name?.[0]?.toUpperCase() || '·')}
        </div>
        <div className="dc-text">
          <div className="dc-eyebrow">
            <span className={`db-dot ${isUnknown ? 'warn-pulse' : isDegraded ? 'warn-pulse' : 'live-pulse'}`} />
            {isUnknown ? 'Unidentified' : isDegraded ? 'Inferred' : 'Discovered'}{(isUnknown || isDegraded || confidence < 80) ? ` · ${confidence}% confidence` : ''}
            {wikidata && <span className="dc-wd">· {wikidata.id}</span>}
          </div>
          <h3 className="dc-name">{brand.name}</h3>
          {brand.tagline && <p className="dc-tag">{brand.tagline}</p>}
          {!brand.tagline && brand.description && <p className="dc-tag">{brand.description.length > 140 ? brand.description.slice(0, 140) + '…' : brand.description}</p>}
          {!isUnknown && (
            <div className="dc-meta">
              {brand.industry && brand.industry !== 'unknown' && <span className="dc-pill"><LeafIcon /> {brand.industry}</span>}
              {brand.location && brand.location !== 'unknown' && <span className="dc-pill"><PinIcon /> {brand.location}</span>}
              {brand.address?.phone && <span className="dc-pill"><PhoneIcon /> {brand.address.phone}</span>}
              {brand.audience && <span className="dc-pill"><UserIcon /> {brand.audience}</span>}
            </div>
          )}
          {hasAddress && (
            <div className="dc-address">{brand.address.line}</div>
          )}
          {socialKeys.length > 0 && (
            <div className="dc-social">
              {socialKeys.map((k) => (
                <a key={k} href={social[k]} target="_blank" rel="noopener noreferrer" className="dc-soc">{k}</a>
              ))}
            </div>
          )}
        </div>
      </div>
      <div className="dc-right">
        <div className="dc-stat">
          <div className="dc-stat-k">Sources</div>
          <div className="dc-stat-v">
            {[
              brand.homepage?.ok ? `Homepage (${brand.homepage.via})` : null,
              brand.robots ? `robots.txt (${brand.robots.lines} lines)` : null,
              brand.sitemap ? `sitemap (${brand.sitemap.urls} URLs)` : null,
              brand.jsonld?.types?.length ? `${brand.jsonld.types.length} schemas` : null,
              wikidata ? `Wikidata ${wikidata.id}` : null,
              brand.enrichment && !brand.enrichment.error && !isUnknown ? 'Claude inference' : null,
            ].filter(Boolean).join(' · ') || 'no signal'}
          </div>
        </div>
        {competitors.length > 0 && !isUnknown && (
          <div className="dc-stat">
            <div className="dc-stat-k">Inferred competitors</div>
            <div className="dc-comps">
              {competitors.map((c, i) => (
                <span key={i} className="dc-comp" title={c.reason || ''}>{c.name || c}</span>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
function LeafIcon() { return <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M2 10c2-6 7-7 9-7-1 5-3 8-9 7z M2 10l4-4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/></svg>; }
function PinIcon() { return <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 11s4-3.5 4-7a4 4 0 1 0-8 0c0 3.5 4 7 4 7z M6 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" stroke="currentColor" strokeWidth="1.2"/></svg>; }
function PhoneIcon() { return <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M2.5 2.5h2l1 2.5-1.5 1c.5 1.5 1.5 2.5 3 3l1-1.5 2.5 1v2h-1.5C5 10.5 1.5 7 1.5 4V2.5h1z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round"/></svg>; }
function UserIcon() { return <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><circle cx="6" cy="4.5" r="2" stroke="currentColor" strokeWidth="1.2"/><path d="M2 11c0-2.2 1.8-4 4-4s4 1.8 4 4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/></svg>; }

// ---------- report ----------
function Report({ brand, domain, discovered, profile, activeSite, scanState, realFixes, onRunRealScan, onCancelRealScan, onOpenFix, onReset, onSettings, updateNotifications }) {
  const vp = useViewport();
  const orbSize = orbSizeFor(vp);

  // ---- which data are we showing? ----
  const liveResult = scanState?.result;
  const isLive = !!liveResult;
  const isScanning = !!scanState?.running;

  // Open PRs for this site's repo — so each fix card shows if it already has a PR.
  const [openPrs, setOpenPrs] = useState([]);
  useEffect(() => {
    const gh = window.ghLoad ? window.ghLoad() : {};
    const repo = activeSite?.repo;
    if (!gh.pat || !repo || !window.ghListPrs) { setOpenPrs([]); return; }
    let c = false;
    window.ghListPrs(gh.pat, repo).then((prs) => { if (!c) setOpenPrs(prs || []); }).catch(() => {});
    return () => { c = true; };
  }, [activeSite && activeSite.repo, activeSite && activeSite.id]);

  // visibility score — only real data, no sample fallback
  // The orb is AI VISIBILITY only. Pre-scan it's unmeasured (0 + "awaiting first scan"),
  // NOT the discovery-confidence number — they're different metrics. Discovery confidence
  // stays on the brand card; this is "how often AI recommends you", measured by a scan.
  const finalScore = isLive ? liveResult.score : 0;
  const score = useCountUp(finalScore, 1400);
  const target = useCountUp(isLive ? Math.min(100, finalScore + 14) : 0, 1800);

  // per-model values — empty until a real scan runs
  const modelValues = isLive
    ? Object.fromEntries(Object.entries(liveResult.perModel).filter(([, v]) => v != null))
    : {};

  const liveModelIds = isLive ? new Set(liveResult.configured) : new Set();
  // models that actually returned answers — all-errored models are excluded from the
  // headline count so a dead model isn't reported as "live".
  const answeredCount = isLive ? (liveResult.answered?.length ?? liveResult.configured.length) : 0;
  const erroredCount = isLive ? (liveResult.errored?.length ?? 0) : 0;

  // competitors — live scan extraction > discovered (Claude inferred)
  const liveCompetitors = isLive && liveResult.competitors.length > 0;
  const discoveredCompetitors = (discovered?.competitors || []).filter((c) => c && (c.name || typeof c === 'string'));
  const hasDiscoveredCompetitors = !liveCompetitors && discoveredCompetitors.length > 0;

  const competitorSource = liveCompetitors ? 'live' : hasDiscoveredCompetitors ? 'inferred' : null;

  const competitors = liveCompetitors
    ? [
        ...liveResult.competitors.slice(0, 5).map((c, i) => ({
          name: c.name,
          domain: '',
          rank: i + 1,
          color: `oklch(80% 0.16 ${(i * 60) % 360})`,
          count: c.count,
          reason: '',
        })),
        { name: brand.name, domain: brand.domain || domain, rank: liveResult.rank || (liveResult.competitors.length + 1), color: 'var(--accent)', us: true, score: liveResult.score },
      ]
    : hasDiscoveredCompetitors
      ? [
          ...discoveredCompetitors.slice(0, 5).map((c, i) => {
            const name = c.name || c;
            return {
              name,
              domain: '',
              rank: i + 1,
              color: `oklch(80% 0.16 ${(i * 60) % 360})`,
              reason: c.reason || '',
            };
          }),
          { name: brand.name, domain: brand.domain || domain, rank: Math.min(6, discoveredCompetitors.length + 1), color: 'var(--accent)', us: true },
        ]
      : [];

  // fixes — only real, no sample fallback
  const fixesData = realFixes && realFixes.length > 0 ? realFixes : [];
  const fixesIsLive = fixesData.length > 0;

  const verdictText = (() => {
    if (isLive) {
      const ms = answeredCount;
      return (
        <>
          {brand.name} is mentioned in <span className="num">{score}%</span> of real responses across {ms} model{ms === 1 ? '' : 's'} ({liveResult.totalMentions}/{liveResult.totalCalls} prompts).
        </>
      );
    }
    if (isScanning) {
      return (
        <>
          Measuring <span className="num">{brand.name}</span> across the wired models<span className="dots" />
        </>
      );
    }
    // No scan yet — focus on what we discovered, not a fake score
    const ind = (discovered?.industry || brand.industry || 'your category').toLowerCase();
    const loc = discovered?.location || brand.location || 'your market';
    const locClause = loc && loc !== 'Online' && loc !== 'Global' && loc !== 'unknown' ? ` in ${loc}` : '';
    return (
      <>
        Ready to measure how AI assistants recommend
        {' '}<span className="num">{brand.name}</span> for {ind}{locClause}.
      </>
    );
  })();

  return (
    <section className="container fade-in" style={{ paddingTop: 20 }}>
      <div className="report">
        {/* DISCOVERED — real data at the top */}
        {discovered && <DiscoveredCard brand={discovered} onSettings={onSettings} />}

        {/* real-scan banner — what's mock vs live, and the CTA */}
        <RealScanBanner
          profile={profile}
          scanState={scanState}
          onRun={onRunRealScan}
          onCancel={onCancelRealScan}
          onOpenSettings={onSettings}
        />

        {/* verdict */}
        <div className="verdict">
          <div className="verdict-orb">
            <Orb state="revealed" score={score} suffix="" values={modelValues} size={orbSize} label="visibility score" viewTransitionName="aiso-orb" />
            <div className="orb-overlay">
              {isLive
                ? `live · ${liveResult.totalCalls} prompts · ${answeredCount} models${erroredCount ? ` · ${erroredCount} errored` : ''}`
                : isScanning
                  ? `running scan…`
                  : `awaiting first scan`}
            </div>
          </div>

          <div className="verdict-copy">
            <div className="eyebrow">
              <span className="pulse" />
              {isLive ? 'Report verified · just now' : isScanning ? 'Measuring across wired models…' : 'Discovery complete'}
              {isLive && <DataBadge kind="live" />}
            </div>
            <h2>{verdictText}</h2>
            {isLive && (
              <p style={{ color: 'var(--fg-2)', fontSize: 17, lineHeight: 1.55, maxWidth: '52ch' }}>
                {fixesIsLive ? `${fixesData.filter(f => f.autoDeployable).length} top fixes are auto-deployable, and we project a lift to ~${target}% within a week.` : `Fixes engine is running against your homepage now.`}
              </p>
            )}
            {!isLive && !isScanning && (
              <p style={{ color: 'var(--fg-2)', fontSize: 16, lineHeight: 1.55, maxWidth: '52ch' }}>
                This orb is your <strong>AI visibility score</strong> — how often models recommend you — and it stays blank until you run a scan. It's separate from the <strong>discovery confidence</strong> on the brand card above (how sure we are we identified you). The technical audit below runs immediately.
              </p>
            )}
            {isLive && (
              <div className="delta-row">
                <div className="delta">
                  <span className="k">mentions</span>
                  <span className="v up">{liveResult.totalMentions}/{liveResult.totalCalls}</span>
                </div>
                <div className="delta">
                  <span className="k">rank</span>
                  <span className="v">{liveResult.rank ? `#${liveResult.rank}` : '—'}</span>
                </div>
                <div className="delta">
                  <span className="k">models</span>
                  <span className="v">{answeredCount}{erroredCount ? ` · ${erroredCount}✕` : ''}</span>
                </div>
              </div>
            )}
          </div>
        </div>

        {/* per-model rail — only shown when a real scan has run */}
        {isLive && Object.keys(modelValues).length > 0 && (
        <div>
          <div className="section-head">
            <h3>How each model sees you <DataBadge kind={liveModelIds.size === MODELS.length ? 'live' : 'mixed'} detail={`${liveModelIds.size}/${MODELS.length} live`} /></h3>
            <span className="more" style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)' }}>measured</span>
          </div>
          <div className="model-rail stagger">
            {MODELS.filter(m => modelValues[m.id] != null).map(m => {
              const v = modelValues[m.id];
              const color = modelColor(m.hue);
              const isModelLive = liveModelIds.has(m.id);
              return (
                <div key={m.id} className={`model-cell ${isModelLive ? 'is-live' : ''}`} style={{ color }}>
                  <div className="mc-top">
                    <div className="mc-name">{m.name}</div>
                    <div className="mc-dot" style={{ background: color }} />
                  </div>
                  <div className="mc-val" style={{ color: 'var(--fg-0)' }}>{v}<small>%</small></div>
                  <div className="mc-bar"><i style={{ width: `${v}%` }} /></div>
                  <div className="mc-foot">
                    <span>{liveResult.results[m.id]?.mentioned || 0}/{liveResult.results[m.id]?.total || 0} prompts</span>
                    <span className="live-tag">· live</span>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
        )}

        {/* competitors — only shown when we have data (live or inferred) */}
        {competitorSource && competitors.length > 0 && (
        <div>
          <div className="section-head">
            <h3>Who AI recommends instead <DataBadge kind={competitorSource === 'live' ? 'live' : 'mixed'} detail={competitorSource === 'inferred' ? 'inferred by Claude' : null} /></h3>
            <span className="more" style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)' }}>
              {competitorSource === 'live'
                ? `extracted from ${liveResult.totalCalls} responses`
                : `from discovery · ${discovered?.industry || 'category'}`}
            </span>
          </div>
          <div className="comp-grid stagger">
            {competitors.map((c) => (
              <div key={c.name + (c.rank || '')} className={`comp ${c.us ? 'us' : ''}`}>
                <div className="avatar" style={{ background: c.us ? 'var(--accent-grad)' : c.color }}>
                  {(c.name || '·')[0]}
                </div>
                <div>
                  <div className="name">
                    {c.name}
                    {c.us && <span style={{ color: 'var(--accent)', fontFamily: 'var(--mono)', fontSize: 11, marginLeft: 8, letterSpacing: '0.14em' }}>YOU</span>}
                  </div>
                  <div className="dom">{c.count ? `${c.count} mentions` : (c.reason || c.domain || '')}</div>
                </div>
                <div className="rank">#{c.rank}</div>
              </div>
            ))}
          </div>
        </div>
        )}

        {/* fixes — only shown when the engine has results */}
        {fixesIsLive && (
        <div>
          <div className="section-head">
            <h3>Ten things to do — ranked <DataBadge kind="live" detail="from your homepage" /></h3>
            <span className="more" style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)', letterSpacing: '0.14em', textTransform: 'uppercase' }}>by impact</span>
          </div>

          <div className="fixes-wrap stagger">
            {fixesData.slice(0, 10).map((fix, i) => fix.shipping ? (
              <button key={fix.id || i} className="fix shipping" onClick={() => onOpenFix(fix.id)}>
                <div className="num"><span className="grad">①</span></div>
                <div className="body">
                  <div className="ship-badge">
                    <span className="live-dot" /> AISO is shipping this for you — free
                  </div>
                  <h4>{fix.plain || fix.title}</h4>
                  {fix.plain && <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.02em', color: 'var(--fg-3)', marginTop: 3, marginBottom: 2 }}>{fix.title}</div>}
                  <p>{fix.desc}</p>
                  <div className="tags">
                    <span className="tag impact-high">High impact</span>
                    <span className="tag gain">+{fix.gain} score</span>
                    {(fix.tags || []).map(t => <span key={t} className="tag">{t}</span>)}
                    {fix.verifiedLive && <span className="tag verified">verified</span>}
                  </div>
                </div>
                <div className="end">
                  {(() => {
                    const ms = (openPrs || []).filter((p) => fix.title && p.title && p.title.includes(fix.title));
                    const pr = ms.find((p) => p.merged) || ms.find((p) => p.open);
                    if (pr && pr.merged) return (<><div className="eta" style={{ color: 'var(--good, #5ad08a)' }}>✓ Shipped &amp; live</div><span className="cta">View on GitHub <Arrow /></span></>);
                    if (pr && pr.open) return (<><div className="eta" style={{ color: 'var(--accent)' }}>● PR #{pr.number} open</div><span className="cta">Review &amp; merge <Arrow /></span></>);
                    return (<><div className="progress"><i /></div><div className="eta">Ready to ship</div><span className="cta">See it happen <Arrow /></span></>);
                  })()}
                </div>
              </button>
            ) : (
              <button key={fix.id || i} className="fix" onClick={() => onOpenFix(fix.id)}>
                <div className="num">{['②','③','④','⑤','⑥','⑦','⑧','⑨','⑩'][i - 1] || `${i + 1}.`}</div>
                <div className="body">
                  <h4>{fix.plain || fix.title}</h4>
                  {fix.plain && <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.02em', color: 'var(--fg-3)', marginTop: 3, marginBottom: 2 }}>{fix.title}</div>}
                  <p>{fix.desc}</p>
                  <div className="tags">
                    <span className={`tag impact-${fix.impact}`}>{fix.impact[0].toUpperCase()}{fix.impact.slice(1)} impact</span>
                    <span className="tag gain">+{fix.gain} score</span>
                    {(fix.tags || []).map(t => <span key={t} className="tag">{t}</span>)}
                    {fix.verifiedLive && <span className="tag verified">verified</span>}
                  </div>
                </div>
                <div className="end">
                  {(() => {
                    const ms = (openPrs || []).filter((p) => fix.title && p.title && p.title.includes(fix.title));
                    const pr = ms.find((p) => p.merged) || ms.find((p) => p.open);
                    if (pr && pr.merged) return (<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 15px', borderRadius: 999, border: '1px solid var(--good, #5ad08a)', color: 'var(--good, #5ad08a)', fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap' }}>✓ Shipped</span>);
                    if (pr && pr.open) return (<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 15px', borderRadius: 999, border: '1px solid var(--accent)', color: 'var(--accent)', fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap' }}>● PR #{pr.number} · Review <Arrow /></span>);
                    return (<span className="arrow">→</span>);
                  })()}
                </div>
              </button>
            ))}
          </div>
        </div>
        )}

        {/* daily check-in */}
        <Daily brand={discovered || activeSite || brand} scanState={scanState} fixes={realFixes} notifications={profile.notifications} updateNotifications={updateNotifications} />

        {/* TIME SERIES — visibility over time */}
        <VisibilityHistory
          activeSite={activeSite}
          brand={discovered}
          onRunScan={onRunRealScan}
          running={scanState?.running}
        />

        {/* QUERY INSPECTOR — type any question, watch six models answer */}
        <QueryInspector
          brand={discovered}
          profile={profile}
          onSettings={onSettings}
        />

        {/* CREATIVE REVIEW — runs once discovery completes */}
        {discovered && (
          <BrandReview brand={discovered} anthropicKey={profile.keys?.anthropic} onSettings={onSettings} />
        )}
      </div>
    </section>
  );
}

function Daily({ brand, scanState, fixes, notifications, updateNotifications }) {
  const name = (brand?.name || 'your brand').trim();
  const init = (name[0] || '·').toUpperCase();
  const N = notifications || { email: true, slack: false, push: true, sms: false };
  const result = scanState?.result;
  const topFix = (fixes || []).find((f) => f.autoDeployable) || (fixes || [])[0];
  const lead = result?.competitors?.[0]?.name;

  // Preview cards from the REAL active brand + latest scan + fixes (no hardcoded mock).
  const card1 = result
    ? {
        ico: init,
        title: result.rank ? `${name} ranks #${result.rank} in AI answers` : `${name} is at ${result.score}% AI visibility`,
        sub: lead ? `${lead} is ahead in your category — close the gap.` : `${result.totalMentions}/${result.totalCalls} prompts across ${result.configured?.length || 0} models.`,
        time: 'now',
      }
    : { ico: init, title: `Tracking ${name}`, sub: `Run a scan — you'll get a ping here only when something material moves.`, time: '—' };
  const card2 = topFix
    ? { title: topFix.autoDeployable ? 'Auto-fix available' : 'New fix recommended', sub: `${topFix.title}${topFix.autoDeployable ? ' — we can ship this.' : ' — drafted for you.'}`, time: 'now' }
    : null;

  return (
    <div className="daily fade-in-up">
      <div>
        <h3>Stay in the loop — quietly</h3>
        <p>One notification when something material changes. A score shift past your threshold, a competitor takes your spot, a new fix becomes auto-deployable. Otherwise, silence.</p>
        <div className="channels">
          {[
            { k: 'email', label: 'Email digest', sub: 'weekly' },
            { k: 'slack', label: 'Slack', sub: 'on material change' },
            { k: 'push', label: 'Browser push', sub: 'when it moves' },
            { k: 'sms', label: 'SMS', sub: 'critical only' },
          ].map(c => (
            <button
              key={c.k}
              className={`channel ${N[c.k] ? 'on' : ''}`}
              title={c.sub}
              onClick={() => updateNotifications && updateNotifications({ [c.k]: !N[c.k] })}
            >
              {N[c.k] ? <Tick /> : <Plus />}
              {c.label}
            </button>
          ))}
        </div>
      </div>
      <div className="notif-preview">
        <div className="notif">
          <div className="ico">{card1.ico}</div>
          <div>
            <div className="title">{card1.title}</div>
            <div className="sub">{card1.sub}</div>
          </div>
          <div className="time">{card1.time}</div>
        </div>
        {card2 && (
          <div className="notif" style={{ marginTop: 10, transform: 'translateX(8px) scale(0.96)', opacity: 0.7 }}>
            <div className="ico" style={{ background: 'var(--bg-3)', color: 'var(--fg-1)' }}>+</div>
            <div>
              <div className="title">{card2.title}</div>
              <div className="sub">{card2.sub}</div>
            </div>
            <div className="time">{card2.time}</div>
          </div>
        )}
      </div>
    </div>
  );
}

// Distinctive marker from a fix payload, to confirm the fix is live in production HTML.
function verifyNeedle(fix) {
  const code = fix?.payload?.code || '';
  const t = code.match(/"@type"\s*:\s*"([^"]+)"/);
  if (t) return `"${t[1]}"`;
  const line = code.split('\n').map((s) => s.trim()).find((s) => s.length > 12);
  return line ? line.slice(0, 28) : null;
}

// Pre-merge AI review: Claude reads the PR diff (+ original file head for context) and
// flags problems before merge. Returns { safe, summary, issues:[{severity,what,line}] }
// or { unavailable:true } if Claude can't be reached (so we never show a false "clean").
// Regenerate a fix payload to resolve the pre-merge review's issues, so it can re-ship clean.
async function improveFixPayload(fix, brand, issues) {
  if (!window.callModel) throw new Error('AI unavailable');
  const issueList = (issues || []).map((i) => `- [${i.severity}] ${i.what}${i.line ? ` (near: ${i.line})` : ''}`).join('\n');
  const prompt = `You generated this ${fix.category} snippet for the brand "${brand?.name || ''}" (${brand?.domain || ''}). A reviewer flagged the issues below. Regenerate the snippet, FIXING every issue. Remove unverified or incorrect data (e.g. a wrong sameAs / Wikidata ID) rather than guessing; strengthen thin values only where you're confident. Keep it valid ${fix.payload?.language || 'code'}.

CURRENT SNIPPET:
${fix.payload?.code || ''}

ISSUES TO FIX:
${issueList}

Return ONLY the corrected snippet — no explanation, no markdown fences.`;
  const r = await window.callModel('anthropic', null, prompt, { maxTokens: 1500, model: 'claude-sonnet-4-6', timeoutMs: 50000 });
  let code = (r.text || '').trim().replace(/^```[a-z]*\n?/i, '').replace(/\n?```\s*$/, '').trim();
  if (!code) throw new Error('Could not regenerate the fix.');
  return code;
}

async function reviewFixDiff(fix, prFiles, originalContent) {
  if (!window.callModel || !prFiles || !prFiles.length) return { unavailable: true };
  const diff = prFiles.map((f) => `--- ${f.filename} (+${f.additions} -${f.deletions}) ---\n${(f.patch || '').slice(0, 7000)}`).join('\n\n');
  const head = (originalContent || '').split('\n').slice(0, 200).join('\n').slice(0, 6000);
  const prompt = `You are reviewing a pull request before it's auto-merged. This PR was generated by an AI visibility tool (AISO) to improve a website's AI search presence.

Diff:
${diff}
${head ? `\nOriginal file (head, for context):\n${head}\n` : ''}
Check for:
1. ENCODING: Any corrupted characters (â, Ã, ï¿½, mojibake). Flag immediately as severity "error".
2. VALID MARKUP: If JSON-LD was added, is it valid JSON? Are required fields present?
3. UNINTENDED CHANGES: Does the diff touch anything OUTSIDE the intended fix? Flag collateral edits.
4. HTML INTEGRITY: Does the insertion break the <head>? Unclosed tags? Duplicate scripts?
5. PLACEHOLDER DATA: Any "XXX", "TODO", "← required", "← add your" that shouldn't ship to production (severity "warning").

Return ONLY JSON:
{"safe": true|false, "issues": [{"severity":"error"|"warning","what":"short description","line":"relevant snippet"}], "summary":"one sentence — either 'Clean, safe to merge' or 'N issues found — review before merging'"}`;
  try {
    const r = await window.callModel('anthropic', null, prompt, { maxTokens: 900, model: 'claude-sonnet-4-6', timeoutMs: 45000 });
    const m = (r.text || '').match(/\{[\s\S]*\}/);
    if (!m) return { unavailable: true };
    return JSON.parse(m[0].replace(/,(\s*[}\]])/g, '$1'));
  } catch { return { unavailable: true }; }
}

// Inline unified-diff view (added=green, removed=red), styled in AISO's mono.
function DiffView({ file }) {
  const all = (file.patch || '').split('\n');
  const lines = all.slice(0, 80);
  return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)', marginBottom: 6 }}>
        {file.filename} · <span style={{ color: 'var(--good, #5ad08a)' }}>+{file.additions}</span> <span style={{ color: '#f0746e' }}>−{file.deletions}</span>
      </div>
      <div style={{ margin: 0, fontFamily: 'var(--mono)', fontSize: 11.5, lineHeight: 1.55, overflowX: 'auto', borderRadius: 8, border: '1px solid var(--line)', background: 'rgba(0,0,0,0.25)', maxHeight: 320 }}>
        {lines.map((ln, i) => {
          const add = ln.startsWith('+') && !ln.startsWith('+++');
          const del = ln.startsWith('-') && !ln.startsWith('---');
          const hunk = ln.startsWith('@@');
          return <div key={i} style={{ padding: '0 10px', whiteSpace: 'pre', background: add ? 'rgba(70,200,130,0.12)' : del ? 'rgba(240,100,100,0.12)' : 'transparent', color: add ? 'var(--good, #5ad08a)' : del ? '#f0746e' : hunk ? 'var(--accent)' : 'var(--fg-2)' }}>{ln || ' '}</div>;
        })}
        {all.length > 80 && <div style={{ padding: '4px 10px', color: 'var(--fg-3)' }}>… {all.length - 80} more lines</div>}
      </div>
    </div>
  );
}

// Paste-it-yourself deploy guide for sites without a GitHub repo (WordPress, Wix, etc.).
const DEPLOY_PLATFORMS = {
  wordpress:   { name: 'WordPress',   steps: ['Plugins → Add New → install the free "WPCode" plugin', 'Code Snippets → + Add Snippet → "Add Your Custom Code"', 'Code Type: HTML Snippet — paste the code below', 'Insertion: Auto Insert · Site-Wide Header → Save & Activate'] },
  webflow:     { name: 'Webflow',     steps: ['Project Settings → Custom Code', 'Paste into the "Head Code" box', 'Save Changes → Publish'] },
  wix:         { name: 'Wix',         steps: ['Settings → Custom Code → + Add Custom Code', 'Paste the code; place it in the <Head>', 'Apply to: All pages → Apply'] },
  squarespace: { name: 'Squarespace', steps: ['Settings → Advanced → Code Injection', 'Paste into the "Header" box', 'Save'] },
  shopify:     { name: 'Shopify',     steps: ['Online Store → Themes → ⋯ → Edit code', 'Open theme.liquid, paste just before </head>', 'Save'] },
  framer:      { name: 'Framer',      steps: ['Project Settings → General → Custom Code', 'Paste into "End of <head> tag"', 'Save → Publish'] },
  other:       { name: 'Something else', steps: ['Open your site editor and find where you can add custom code to the page <head>', 'Paste the snippet just before the closing </head> tag', 'Save and re-publish your site'] },
};
// Recognizable brand glyphs for the deploy picker (inline SVG — no external deps).
function PlatformIcon({ id, size = 20 }) {
  const c = { width: size, height: size, display: 'block', flex: 'none' };
  switch (id) {
    case 'wordpress':
      return (<svg style={c} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0"/></svg>);
    case 'cloudflare':
      return (<svg style={c} viewBox="0 0 24 24" fill="none" stroke="#f6821f" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" aria-hidden="true"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>);
    case 'github':
      return (<svg style={c} viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>);
    case 'webflow':
      return (<svg style={c} viewBox="0 0 24 24" fill="#146EF5" aria-hidden="true"><path d="M24 4.515l-7.658 14.97H9.149l3.205-6.204h-.144C9.566 16.713 5.621 18.973 0 19.485v-6.118s3.596-.213 5.71-2.435H0V4.515h6.879v5.394l.144-.001 2.681-5.393h5.204v5.36l.144-.001 2.78-5.36H24z"/></svg>);
    case 'squarespace':
      return (<svg style={c} viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="11" height="11" rx="2" fill="currentColor" opacity="0.5"/><rect x="9" y="9" width="11" height="11" rx="2" fill="currentColor"/></svg>);
    case 'wix':
      return (<svg style={c} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="M3 9.5h18"/><circle cx="6" cy="7.25" r="0.4" fill="currentColor" stroke="none"/></svg>);
    case 'replit':
      return (<svg style={c} viewBox="0 0 24 24" fill="none" stroke="#F26207" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M7.5 9l3 3-3 3M13 15h4"/></svg>);
    case 'shopify':
      return (<svg style={c} viewBox="0 0 24 24" fill="none" stroke="#95BF47" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M6 8h12l-1 12H7L6 8z"/><path d="M9.5 8V6.5a2.5 2.5 0 0 1 5 0V8"/></svg>);
    default: // "Something else" / "Any Website" — globe
      return (<svg style={c} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z"/></svg>);
  }
}

// The three strategic channels lead; the rest are copy-paste platforms grouped below.
// NOTE: github/cloudflare are NOT in DEPLOY_PLATFORMS (they have their own panels), so the
// picker uses this label map — never DEPLOY_PLATFORMS[id].name (that would be undefined).
const DEPLOY_GROUPS = {
  primary: [
    { id: 'anyweb',    tag: 'Any site · we do it for you' },
    { id: 'wordpress', tag: 'WordPress plugin' },
    { id: 'github',    tag: 'For developers' },
  ],
  secondary: ['squarespace', 'wix', 'shopify', 'webflow', 'replit', 'other'],
};
const PLATFORM_LABEL = { anyweb: 'Any Website', wordpress: 'WordPress', github: 'GitHub', wix: 'Wix', squarespace: 'Squarespace', webflow: 'Webflow', shopify: 'Shopify', replit: 'Replit', other: 'Something else' };

// Calm, guided steps for the copy-paste platforms — a connected stepper, not a cramped list.
function DeploySteps({ platform, code }) {
  const p = DEPLOY_PLATFORMS[platform] || DEPLOY_PLATFORMS.other;
  return (
    <div>
      <div style={{ fontWeight: 700, fontSize: 15.5, color: 'var(--fg-0)', marginBottom: 2 }}>{platform === 'other' ? 'Add it to your site' : `Add it in ${p.name}`}</div>
      <div style={{ color: 'var(--fg-2)', fontSize: 13, marginBottom: 18 }}>About two minutes — no code knowledge needed.</div>
      <div style={{ position: 'relative' }}>
        <div style={{ position: 'absolute', left: 13, top: 16, bottom: 16, width: 2, background: 'var(--line)' }} />
        {p.steps.map((s, i) => (
          <div key={i} style={{ position: 'relative', display: 'flex', gap: 13, alignItems: 'flex-start', marginBottom: i === p.steps.length - 1 ? 0 : 15 }}>
            <div style={{ flex: '0 0 27px', width: 27, height: 27, borderRadius: 999, background: 'var(--bg-2)', border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12.5, fontWeight: 700, color: 'var(--accent)', position: 'relative', zIndex: 1 }}>{i + 1}</div>
            <div style={{ paddingTop: 4, color: 'var(--fg-1)', fontSize: 14, lineHeight: 1.5 }}>{s}</div>
          </div>
        ))}
      </div>
      <div style={{ marginTop: 18, padding: 14, borderRadius: 12, border: '1px solid var(--line)', background: 'var(--bg-1)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
        <div>
          <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--fg-0)' }}>Your snippet</div>
          <div style={{ fontSize: 12, color: 'var(--fg-3)' }}>Paste it where the steps ask for code.</div>
        </div>
        <CopyButton text={code} />
      </div>
    </div>
  );
}

// Platform-specific copy — all of these funnel to the SAME proxy flow underneath.
const ANYWEB_COPY = {
  anyweb:      { name: 'your site',   blurb: 'Works for any website — Squarespace, Wix, Replit, hand-coded. Point your domain at us once and every fix goes live automatically. No code, no plugins.' },
  replit:      { name: 'Replit',      blurb: 'Connect your Replit domain. We route it through our edge and inject your fixes automatically — nothing to change in your Repl.' },
  squarespace: { name: 'Squarespace', blurb: 'Connect your Squarespace domain. We sit in front of your site and inject your fixes — no template editing.' },
  wix:         { name: 'Wix',         blurb: 'Connect your Wix domain. We route it through our edge and inject your fixes — no Wix code needed.' },
  shopify:     { name: 'Shopify',     blurb: 'Connect your Shopify domain. We inject your fixes at the edge — no theme.liquid editing.' },
  webflow:     { name: 'Webflow',     blurb: 'Connect your Webflow domain. We inject your fixes at the edge — no custom-code edits.' },
  other:       { name: 'your site',   blurb: 'Point your domain at us once and every fix goes live automatically. Works for any site.' },
};

// The "Any Website" channel UI — AISO proxies the site through our edge (server-side, the
// user never sees Cloudflare) and injects fixes. One DNS step, then it's live.
function AnyWebsitePanel({ platform, domain, schema }) {
  const meta = ANYWEB_COPY[platform] || ANYWEB_COPY.anyweb;
  const d = String(domain || '').replace(/^https?:\/\//, '').replace(/\/.*$/, '');
  const [phase, setPhase] = useState('idle'); // idle | starting | await | checking | live | error
  const [ns, setNs] = useState([]);
  const [registrar, setRegistrar] = useState(null);
  const [err, setErr] = useState(null);
  const [showPaste, setShowPaste] = useState(false);
  const hasSnippet = !!DEPLOY_PLATFORMS[platform];

  async function api(action, extra) {
    const r = await fetch('/api/anywebsite', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action, domain: d, ...extra }) });
    const j = await r.json().catch(() => ({}));
    if (j.error) throw new Error(j.hint || j.message || j.error);
    return j;
  }
  async function start() {
    if (!d) { setErr('This site has no domain yet — scan a real site first.'); setPhase('error'); return; }
    setErr(null); setPhase('starting');
    try {
      const r = await api('start');
      setNs(r.nameServers || []);
      fetch('/api/domain-connect', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'detect', domain: d }) }).then((x) => x.json()).then((z) => setRegistrar(z && z.registrar)).catch(() => {});
      if (r.status === 'active') { await check(); } else { setPhase('await'); }
    } catch (e) { setErr(e.message || String(e)); setPhase('error'); }
  }
  async function check() {
    setErr(null); setPhase('checking');
    try {
      const r = await api('status', { schema: schema || '' });
      if (r.active && r.deployed) { setPhase('live'); window.aisoToast && window.aisoToast('Live — your fixes are injected at the edge.', 'success'); }
      else { setNs(r.nameServers || ns); setPhase('await'); }
    } catch (e) { setErr(e.message || String(e)); setPhase('error'); }
  }
  useEffect(() => {
    if (phase !== 'await') return;
    const t = setTimeout(() => { check(); }, 20000);
    return () => clearTimeout(t);
  }, [phase, ns.join(',')]);

  if (phase === 'live') {
    return (
      <div style={{ padding: '14px 16px', borderRadius: 12, border: '1px solid oklch(82% 0.16 152 / 0.4)', background: 'oklch(82% 0.16 152 / 0.08)' }}>
        <div style={{ fontWeight: 700, color: 'var(--good, #5ad08a)', fontSize: 15, marginBottom: 4 }}>✓ Your fixes are live</div>
        <div style={{ fontSize: 13.5, color: 'var(--fg-2)', lineHeight: 1.5 }}>{d} now serves through our edge with your fixes injected. We re-scan in 24h and tell you if visibility moves.</div>
      </div>
    );
  }

  return (
    <div>
      <div style={{ fontSize: 13.5, color: 'var(--fg-2)', lineHeight: 1.55, marginBottom: 14 }}>{meta.blurb}</div>

      {(phase === 'idle' || phase === 'starting' || phase === 'error') && (
        <div className="actions">
          <button className={`approve ${phase === 'starting' ? 'deploying' : ''}`} onClick={start} disabled={phase === 'starting'}>
            {phase === 'starting' ? (<>Setting up…<span className="dots" /></>) : (<><Sparkle /> Make {d || 'my site'} AI-visible →</>)}
          </button>
          <button className="later">Later</button>
        </div>
      )}

      {(phase === 'await' || phase === 'checking') && (
        <div>
          <div style={{ fontWeight: 600, color: 'var(--fg-0)', marginBottom: 6 }}>One step: point {meta.name} to us</div>
          <div style={{ fontSize: 13, color: 'var(--fg-2)', marginBottom: 10, lineHeight: 1.5 }}>Sign in to {registrar || 'your domain settings'} and set your domain's nameservers to these two — that's the whole job:</div>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 10, padding: '10px 14px', marginBottom: 12 }}>
            {(ns || []).map((n, i) => <div key={i} style={{ color: 'var(--fg-0)', padding: '3px 0' }}>{n}</div>)}
          </div>
          <button className="btn btn-primary" onClick={check} disabled={phase === 'checking'} style={{ width: '100%' }}>{phase === 'checking' ? 'Checking…' : 'I changed them — check now'}</button>
          <div style={{ fontSize: 12.5, color: 'var(--fg-3)', marginTop: 8, lineHeight: 1.5 }}>Takes a few minutes to propagate. We'll keep checking and confirm automatically.</div>
        </div>
      )}

      {err && (<div className="approve-error" style={{ marginTop: 10 }}><strong>Hmm.</strong><div style={{ marginTop: 6, fontFamily: 'var(--mono)', fontSize: 12 }}>{err}</div></div>)}

      {hasSnippet && (
        <div style={{ marginTop: 16, borderTop: '1px solid var(--line)', paddingTop: 12 }}>
          {!showPaste ? (
            <button className="link-btn" onClick={() => setShowPaste(true)} style={{ fontSize: 13 }}>Prefer to paste it into {meta.name} yourself? →</button>
          ) : (
            <DeploySteps platform={platform} code={schema} />
          )}
        </div>
      )}
    </div>
  );
}

// ---------- fix detail (DATA-DRIVEN — uses real discovered brand + real payload) ----------
function FixDetail({ fixId, fixes = [], brand, site, siteRepo, sitePrs, onPrOpened, onConnectRepo, onBack, onSettings }) {
  const fix = (fixes || []).find(f => f.id === fixId) || (fixes || [])[0];
  const [deploying, setDeploying] = useState(false);
  const [deployed, setDeployed] = useState(false);
  const [prResult, setPrResult] = useState(null);
  const [deployError, setDeployError] = useState(null);
  const [deploySteps, setDeploySteps] = useState([]);
  const [ghState, setGhState] = useState(() => (window.ghLoad ? window.ghLoad() : {}));

  useEffect(() => {
    const onCh = () => setGhState(window.ghLoad ? window.ghLoad() : {});
    window.addEventListener('aiso:gh-changed', onCh);
    return () => window.removeEventListener('aiso:gh-changed', onCh);
  }, []);

  const [prFiles, setPrFiles] = useState(null);
  const [merging, setMerging] = useState(false);
  const [merged, setMerged] = useState(false);
  const [mergeError, setMergeError] = useState(null);
  const [verify, setVerify] = useState(null); // null | 'checking' | 'ok' | 'pending'
  const [reverting, setReverting] = useState(false);
  const [reverted, setReverted] = useState(false);
  const [prClosed, setPrClosed] = useState(false);

  // Load the PR diff for inline review whenever a PR exists for this fix.
  useEffect(() => {
    const f = (fixes || []).find((x) => x.id === fixId) || (fixes || [])[0];
    if (!f || !ghState.pat || !siteRepo || !window.ghFetchPrFiles) { setPrFiles(null); return; }
    const pr = prResult || (sitePrs || []).find((p) => p.title === f.title);
    if (!pr) { setPrFiles(null); return; }
    let c = false;
    window.ghFetchPrFiles(ghState.pat, siteRepo, pr.number)
      .then((files) => { if (!c) setPrFiles(files); })
      .catch(() => { if (!c) setPrFiles([]); });
    return () => { c = true; };
  }, [fixId, prResult, ghState.pat, siteRepo, merged, reverted]);

  const [review, setReview] = useState(null); // null | 'checking' | { safe, summary, issues } | { unavailable }
  const [improving, setImproving] = useState(false);
  const [deployMethod, setDeployMethod] = useState(siteRepo ? 'github' : 'anyweb'); // github | wordpress | cloudflare | wix | squarespace | webflow | other

  // Cloudflare edge channel — inject schema at the edge, no repo/CMS/file changes.
  const [cfState, setCfState] = useState(() => (window.cfLoad ? window.cfLoad() : {}));
  const [cfDeploying, setCfDeploying] = useState(false);
  const [cfDeployed, setCfDeployed] = useState(false);
  const [cfError, setCfError] = useState(null);
  useEffect(() => {
    const onCh = () => setCfState(window.cfLoad ? window.cfLoad() : {});
    window.addEventListener('aiso:cf-changed', onCh);
    return () => window.removeEventListener('aiso:cf-changed', onCh);
  }, []);
  const cfConnected = !!cfState.token;
  useEffect(() => {
    if (!prFiles || prFiles.length === 0 || merged || reverted) return;
    const f = (fixes || []).find((x) => x.id === fixId) || (fixes || [])[0];
    const pr = prResult || (sitePrs || []).find((p) => p.title === (f && f.title));
    let c = false;
    setReview('checking');
    reviewFixDiff(f, prFiles, pr && pr.originalContent).then((res) => { if (!c) setReview(res || { unavailable: true }); });
    return () => { c = true; };
  }, [prFiles, merged, reverted]);

  if (!fix) {
    return (
      <section className="container fade-in" style={{ paddingTop: 30, textAlign: 'center' }}>
        <p style={{ color: 'var(--fg-2)' }}>No fix selected.</p>
        <button className="btn" onClick={onBack}>Back to report</button>
      </section>
    );
  }

  // Repo is strictly per-site — NO global fallback (otherwise a new site inherits another
  // site's repo). A site has a repo only once you connect one to it.
  const repoForSite = siteRepo || null;
  const ghConnected = !!ghState.pat && !!repoForSite;
  // Entity/Wikidata fixes target wikidata.org, NOT the site's code — never open a code PR for them.
  const appliesToCodebase = !(fix.category === 'Entity' || fix.recipeId === 'wikidata-entity');
  // Dedupe: if a PR for this fix already exists on this site, show/open that one — don't open another.
  const existingPr = (sitePrs || []).find((p) => p.title === fix.title) || null;
  const shippedPr = prResult || existingPr;

  const handleApprove = async () => {
    if (deploying) return;
    if (!appliesToCodebase) { try { window.open('https://www.wikidata.org/wiki/Special:NewItem', '_blank', 'noopener'); } catch {} return; }
    if (shippedPr) { try { window.open(shippedPr.url, '_blank', 'noopener'); } catch {} return; }
    setDeployError(null);
    setDeploySteps([]);

    if (!ghConnected) {
      // Not connected → take the user straight to the connect flow (no fake deploy).
      onSettings && onSettings('integrations');
      return;
    }

    setDeploying(true);
    try {
      const result = await window.ghOpenFixPR({
        pat: ghState.pat,
        repo: repoForSite,
        fix,
        brand,
      });
      setPrResult(result);
      setDeploySteps(result.steps || []);
      setDeployed(true);
      onPrOpened?.({ number: result.number, url: result.url, branch: result.branch, title: fix.title, fixId: fix.id, path: result.path, originalContent: result.originalContent, openedAt: Date.now() });
    } catch (e) {
      setDeployError(e.message || String(e));
    } finally {
      setDeploying(false);
    }
  };

  const handleMerge = async () => {
    if (merging || merged || !shippedPr) return;
    setMergeError(null); setMerging(true);
    try {
      const res = await window.ghMergePR(ghState.pat, repoForSite, shippedPr.number, shippedPr.branch);
      if (!res.merged) throw new Error('Merge did not complete.');
      setMerged(true);
      window.aisoToast && window.aisoToast('Merged & deployed — re-scanning in 24h.', 'success');
      setVerify('checking');
      const needle = verifyNeedle(fix);
      const v = needle ? await window.ghVerifyLive(brand?.domain, needle) : { ok: false };
      setVerify(v.ok ? 'ok' : 'pending');
    } catch (e) {
      setMergeError(e.message || String(e));
    } finally {
      setMerging(false);
    }
  };

  const handleRevert = async () => {
    if (reverting || !shippedPr) return;
    if (!window.confirm('Revert this fix? The original file will be restored on your default branch.')) return;
    setMergeError(null); setReverting(true);
    try {
      await window.ghRevertFix(ghState.pat, repoForSite, { path: shippedPr.path, originalContent: shippedPr.originalContent, title: fix.title });
      setReverted(true); setMerged(false); setVerify(null);
    } catch (e) {
      setMergeError(e.message || String(e));
    } finally {
      setReverting(false);
    }
  };

  const handleClosePr = async () => {
    if (merging || !shippedPr) return;
    if (!window.confirm(`Discard PR #${shippedPr.number}? It will be closed and its branch deleted.`)) return;
    setMergeError(null); setMerging(true);
    try {
      await window.ghClosePr(ghState.pat, repoForSite, shippedPr.number, shippedPr.branch);
      setPrClosed(true);
    } catch (e) { setMergeError(e.message || String(e)); }
    finally { setMerging(false); }
  };

  const handleImprove = async () => {
    if (improving || !shippedPr) return;
    setImproving(true); setMergeError(null);
    try {
      const improved = await improveFixPayload(fix, brand, review && review.issues);
      const improvedFix = { ...fix, payload: { ...(fix.payload || {}), code: improved } };
      await window.ghClosePr(ghState.pat, repoForSite, shippedPr.number, shippedPr.branch);
      const result = await window.ghOpenFixPR({ pat: ghState.pat, repo: repoForSite, fix: improvedFix, brand });
      setReview(null); setPrFiles(null); setPrResult(result);
      onPrOpened?.({ number: result.number, url: result.url, branch: result.branch, title: fix.title, fixId: fix.id, path: result.path, originalContent: result.originalContent, openedAt: Date.now() });
    } catch (e) { setMergeError(e.message || String(e)); }
    finally { setImproving(false); }
  };

  const handleCfDeploy = async () => {
    if (cfDeploying || cfDeployed) return;
    if (!cfConnected) { onSettings && onSettings('integrations'); return; }
    setCfError(null); setCfDeploying(true);
    try {
      await window.cfDeployFix({ token: cfState.token, domain: brand?.domain || fix.brandSnapshot?.domain || '', fixId: fix.id, code: fix.payload.code });
      setCfDeployed(true);
      window.aisoToast && window.aisoToast('Live on your site via Cloudflare — no files changed.', 'success');
    } catch (e) { setCfError(e.message || String(e)); }
    finally { setCfDeploying(false); }
  };

  const brandName = brand?.name || fix.brandSnapshot?.name || 'your brand';
  const domain = brand?.domain || fix.brandSnapshot?.domain || '';
  const industry = brand?.industry || fix.brandSnapshot?.industry || '';
  const location = brand?.location || fix.brandSnapshot?.location || '';

  const aiContent = fix.aiContent; // populated for fix.shipping after Claude enrichment
  const beforeAfter = aiContent?.beforeAfter && aiContent.beforeAfter.length > 0
    ? aiContent.beforeAfter
    : null;

  // Use real PR steps from GitHub if available; otherwise show synthesized timeline
  const fixSteps = (prResult && deploySteps.length > 0) ? [
    { label: `Read homepage at ${domain}`, sub: `discovered ${brand?.jsonld?.types?.length || 0} schema types, ${Object.keys(brand?.social || {}).length} social links`, state: 'done', time: '✓' },
    { label: `Detected: ${fix.title.toLowerCase()}`, sub: `signal: ${fix.detection?.signal || `no ${fix.category} found in homepage HTML`}`, state: 'done', time: '✓' },
    { label: `Generated ${fix.category} payload`, sub: `${(fix.payload?.code || '').length} characters · ${fix.payload?.language || 'code'}`, state: 'done', time: '✓' },
    ...(aiContent ? [{ label: `Claude generated ${aiContent.faqs?.length || 0} Q&A pairs`, sub: 'tuned to your discovered brand', state: 'done', time: '✓' }] : []),
    ...deploySteps.map(s => ({ label: s.label, sub: s.sub, state: 'done', time: '✓' })),
    { label: 'Re-scan visibility', sub: 'measures actual lift after you merge', state: 'pending', time: 'in 24h' },
  ] : [
    { label: `Read homepage at ${domain}`, sub: `discovered ${brand?.jsonld?.types?.length || 0} schema types, ${Object.keys(brand?.social || {}).length} social links`, state: 'done', time: '00:03' },
    { label: `Detected: ${fix.title.toLowerCase()}`, sub: `signal: ${fix.detection?.signal || `no ${fix.category} found in homepage HTML`}`, state: 'done', time: '00:05' },
    { label: `Generated ${fix.category} payload`, sub: `${(fix.payload?.code || '').length} characters · ${fix.payload?.language || 'code'} · ready to deploy`, state: 'done', time: '00:08' },
    ...(aiContent ? [{ label: `Claude generated ${aiContent.faqs?.length || 0} Q&A pairs`, sub: 'tuned to your discovered brand, industry, and audience', state: 'done', time: '00:12' }] : []),
    { label: ghConnected ? `Open PR on ${repoForSite?.fullName}` : 'Open pull request on your repo', sub: ghConnected ? 'click Approve & deploy →' : 'wire GitHub in Settings → Integrations to ship for real', state: deployed ? 'done' : (deploying ? 'active' : 'pending'), time: deployed ? '✓' : (deploying ? 'now' : 'on approve') },
    { label: 'Re-scan visibility across the wired models', sub: 'measures actual lift, feeds back into the engine', state: 'pending', time: 'in 24h' },
  ];

  return (
    <section className="container fade-in" style={{ paddingTop: 8 }}>
      <div className="fix-scene">
        <div className="crumbs">
          <button onClick={onBack}>Report</button>
          <span className="sep">/</span>
          <span>Fix #{fix.id} · {fix.category}</span>
        </div>

        <div className="fix-hero">
          <div>
            <div className="eyebrow">
              <span className="pulse" />
              {fix.autoDeployable
                ? (fix.shipping ? 'Auto-deployable · we’re already on it' : 'Auto-deployable')
                : 'Guided fix · we draft, you deploy'}
            </div>
            <h2>
              <span className="grad">{fix.plain || fix.title}</span>
              {domain && <><br/><span style={{ fontFamily: 'var(--mono)', fontSize: '0.45em', color: 'var(--fg-2)', fontStyle: 'normal', letterSpacing: 0 }}>{domain}{fix.payload?.target?.startsWith('https://') ? new URL(fix.payload.target).pathname : ''}</span></>}
            </h2>
            {fix.plain && <div style={{ fontFamily: 'var(--mono)', fontSize: 12.5, color: 'var(--fg-3)', marginTop: -2, marginBottom: 4, letterSpacing: '0.02em' }}>{fix.title}</div>}
            <p className="narration">{fix.desc}</p>
            {fix.detection?.signal && (
              <div className="detection-card">
                <div className="dc-eyebrow"><span className="pulse" /> What we detected</div>
                <div className="detection-body">
                  <div className="det-row"><span className="det-k">Signal</span><span className="det-v">{fix.detection.signal}</span></div>
                  {fix.detection.data && Object.entries(fix.detection.data).map(([k, v]) => (
                    <div key={k} className="det-row"><span className="det-k">{k}</span><span className="det-v">{String(v)}</span></div>
                  ))}
                  <div className="det-row"><span className="det-k">Source</span><span className="det-v" style={{ fontFamily: 'var(--mono)' }}>homepage of {domain} · live HTML</span></div>
                </div>
              </div>
            )}
            {beforeAfter && (
              <div style={{ marginTop: 30 }}>
                <div className="section-head">
                  <h3>What changes</h3>
                  <div className="more">
                    <span style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)', letterSpacing: '0.14em', textTransform: 'uppercase' }}>generated by Claude · using your data</span>
                  </div>
                </div>
                {beforeAfter.map((ba, i) => (
                  <div className="ba" key={i} style={{ marginBottom: 18 }}>
                    <div className="ba-pane before">
                      <div className="ba-head">
                        <h4>Today</h4>
                        <span className="pill">before</span>
                      </div>
                      <div className="ba-body">
                        <div className="faq-ask">Claude · prompt</div>
                        <div className="faq-ai" style={{ color: 'var(--fg-2)' }}>“{ba.prompt}”</div>
                        <div className="faq-ask">Claude · response</div>
                        <div className="faq-ai bad">{ba.before}</div>
                      </div>
                    </div>
                    <div className="ba-pane after">
                      <div className="ba-head">
                        <h4>After this fix</h4>
                        <span className="pill"><span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: 'currentColor', marginRight: 6 }} />after</span>
                      </div>
                      <div className="ba-body">
                        <div className="faq-ask">Claude · prompt</div>
                        <div className="faq-ai" style={{ color: 'var(--fg-2)' }}>“{ba.prompt}”</div>
                        <div className="faq-ask">Claude · response</div>
                        <div className="faq-ai good">{ba.after}</div>
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            )}

            {fix.payload?.code && (
              <div style={{ marginTop: 30 }}>
                <div className="section-head">
                  <h3>The exact code we’ll ship</h3>
                  <div className="more">
                    <CopyButton text={fix.payload.code} />
                    <span style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)', marginLeft: 12 }}>target: {fix.payload.target}</span>
                  </div>
                </div>
                <pre className={`schema lang-${fix.payload.language}`}>{fix.payload.code}</pre>
                {fix.payload.notes && (
                  <p style={{ color: 'var(--fg-2)', fontSize: 13, marginTop: 12, fontFamily: 'var(--mono)' }}>{fix.payload.notes}</p>
                )}
              </div>
            )}
          </div>

          <aside className="impact-card">
            <div className="head"><Sparkle /> Projected impact</div>
            <div className="big">
              <span className="grad">+{fix.gain}</span>
              <span style={{ fontSize: '0.4em', color: 'var(--fg-2)', marginLeft: 8, fontStyle: 'italic' }}> visibility</span>
            </div>
            <div className="what">First-week measured lift, projected from {fix.category.toLowerCase()} fixes in similar verticals.</div>

            <div className="row"><span className="k">Category</span><span className="v">{fix.category}</span></div>
            <div className="row"><span className="k">Impact</span><span className="v">{fix.impact[0].toUpperCase() + fix.impact.slice(1)}</span></div>
            <div className="row"><span className="k">Deployment</span><span className="v">{fix.autoDeployable ? 'Automatic' : 'Manual'}</span></div>
            <div className="row"><span className="k">Effort from you</span><span className="v">{fix.autoDeployable ? 'One click' : 'Quick review'}</span></div>

            {appliesToCodebase ? (
              <div>
                <div style={{ fontWeight: 700, fontSize: 15.5, color: 'var(--fg-0)', marginBottom: 3 }}>Add this to your site</div>
                <div style={{ color: 'var(--fg-2)', fontSize: 13, marginBottom: 14 }}>Where's your site built?</div>
                <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 8 }}>
                  {DEPLOY_GROUPS.primary.map(({ id, tag }) => {
                    const on = deployMethod === id;
                    return (
                      <button key={id} onClick={() => setDeployMethod(id)} style={{ display: 'flex', alignItems: 'center', gap: 12, textAlign: 'left', padding: '11px 13px', borderRadius: 12, border: `1px solid ${on ? 'var(--accent)' : 'var(--line)'}`, background: on ? 'oklch(78% 0.16 232 / 0.10)' : 'var(--bg-1)', cursor: 'pointer' }}>
                        <span style={{ width: 38, height: 38, borderRadius: 9, background: 'var(--bg-2)', border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--fg-0)', flex: 'none' }}><PlatformIcon id={id} size={21} /></span>
                        <span style={{ flex: 1, minWidth: 0 }}>
                          <span style={{ display: 'block', fontSize: 14.5, fontWeight: 600, color: on ? 'var(--accent)' : 'var(--fg-0)' }}>{PLATFORM_LABEL[id]}</span>
                          <span style={{ display: 'block', fontSize: 12, color: 'var(--fg-3)', marginTop: 1 }}>{tag}</span>
                        </span>
                        <span style={{ width: 18, height: 18, borderRadius: 999, border: `2px solid ${on ? 'var(--accent)' : 'var(--line)'}`, background: on ? 'var(--accent)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>{on && <span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--bg-1)' }} />}</span>
                      </button>
                    );
                  })}
                </div>
                <div style={{ fontSize: 11, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '0.07em', fontWeight: 600, margin: '16px 0 9px' }}>Built somewhere else?</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 7, marginBottom: 16 }}>
                  {DEPLOY_GROUPS.secondary.map((id) => {
                    const on = deployMethod === id;
                    return (
                      <button key={id} onClick={() => setDeployMethod(id)} style={{ display: 'inline-flex', alignItems: 'center', gap: 7, padding: '6px 12px 6px 9px', borderRadius: 999, border: `1px solid ${on ? 'var(--accent)' : 'var(--line)'}`, background: on ? 'oklch(78% 0.16 232 / 0.12)' : 'transparent', color: on ? 'var(--accent)' : 'var(--fg-2)', fontSize: 13, fontWeight: on ? 600 : 400, cursor: 'pointer' }}>
                        <span style={{ display: 'flex', color: on ? 'var(--accent)' : 'var(--fg-2)' }}><PlatformIcon id={id} size={15} /></span>
                        {PLATFORM_LABEL[id]}
                      </button>
                    );
                  })}
                </div>

                {deployMethod === 'github' ? (
                  (ghConnected || shippedPr) ? (
                    <div className="actions">
                      <button className={`approve ${deploying ? 'deploying' : ''}`} onClick={handleApprove} disabled={deploying}>
                        {shippedPr ? (<><Tick /> Change #{shippedPr.number} — review →</>)
                          : deploying ? (<>Working…<span className="dots" /></>)
                          : (<><Sparkle /> Add it to my site →</>)}
                      </button>
                      <button className="later">Later</button>
                    </div>
                  ) : (
                    <div className="inline-connect">
                      <div style={{ fontSize: 13.5, color: 'var(--fg-2)', lineHeight: 1.5, marginBottom: 12 }}>Connect GitHub once — AISO opens a pull request you review and merge in a click. Nothing to copy-paste.</div>
                      {window.GitHubConnector && <window.GitHubConnector site={site} onPickRepo={onConnectRepo} onChange={() => setGhState(window.ghLoad ? window.ghLoad() : {})} />}
                    </div>
                  )
                ) : deployMethod === 'wordpress' ? (
                  <DeploySteps platform="wordpress" code={fix.payload.code} />
                ) : (
                  <AnyWebsitePanel platform={deployMethod} domain={brand?.domain || fix.brandSnapshot?.domain} schema={fix.payload?.code} />
                )}
              </div>
            ) : (
              <div>
                <div className="actions">
                  <button className="approve" onClick={handleApprove}><Sparkle /> Open Wikidata →</button>
                  <button className="later">Later</button>
                </div>
                <div style={{ marginTop: 12, padding: '12px 14px', borderRadius: 8, border: '1px solid var(--line)', background: 'var(--bg-2)', fontSize: 13.5, color: 'var(--fg-2)', lineHeight: 1.5 }}>
                  This one lives on <strong style={{ color: 'var(--fg-1)' }}>Wikidata</strong>, not your website — copy the details below and submit them at wikidata.org.
                </div>
              </div>
            )}

            {deployError && (
              <div className="approve-error">
                <strong>Couldn't open the PR.</strong>
                <div style={{ marginTop: 6, fontFamily: 'var(--mono)', fontSize: 12 }}>{deployError}</div>
              </div>
            )}

            {shippedPr && (
              <div style={{ marginTop: 14, border: '1px solid var(--line)', borderRadius: 12, padding: '14px 16px', background: 'var(--bg-1)' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10, gap: 10 }}>
                  <span style={{ fontWeight: 600 }}>
                    {prClosed ? '✕ PR discarded' : reverted ? '↩ Reverted' : merged ? '✓ Merged & deployed' : `Pull request #${shippedPr.number}`}
                  </span>
                  <a href={shippedPr.url} target="_blank" rel="noopener noreferrer" className="link-btn" style={{ fontSize: 12.5 }}>View on GitHub →</a>
                </div>

                {!merged && !reverted && prFiles && prFiles.length > 0 && (
                  <div style={{ marginBottom: 10 }}>{prFiles.map((f) => <DiffView key={f.filename} file={f} />)}</div>
                )}
                {!merged && !reverted && prFiles === null && (
                  <div style={{ fontSize: 12.5, color: 'var(--fg-3)', marginBottom: 10 }}>Loading changes…</div>
                )}

                {!merged && !reverted && !prClosed && review && (
                  <div style={{ marginBottom: 12, padding: '12px 14px', borderRadius: 8, border: '1px solid var(--line)', background: 'rgba(0,0,0,0.18)', fontSize: 13 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 7, fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--fg-3)', marginBottom: 8 }}>
                      <span className="pulse" /> Pre-merge review
                    </div>
                    {review === 'checking' && <span style={{ color: 'var(--fg-2)' }}>Reviewing changes…</span>}
                    {review.unavailable && <span style={{ color: 'var(--fg-2)' }}>AI review unavailable — review the diff manually above.</span>}
                    {review !== 'checking' && !review.unavailable && (
                      (review.issues || []).length === 0 ? (
                        <div style={{ color: 'var(--good, #5ad08a)' }}>✓ {review.summary || 'Clean — safe to merge.'}</div>
                      ) : (
                        <>
                          <div style={{ color: (review.issues || []).some((i) => i.severity === 'error') ? '#f0746e' : 'var(--warn, #e0a050)', fontWeight: 600, marginBottom: 7 }}>{review.summary || `${review.issues.length} issue${review.issues.length === 1 ? '' : 's'} found`}</div>
                          <div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
                            {review.issues.map((it, i) => (
                              <div key={i}>
                                <div style={{ color: it.severity === 'error' ? '#f0746e' : 'var(--warn, #e0a050)' }}>{it.severity === 'error' ? '✕' : '⚠'} {it.what}</div>
                                {it.line && <div style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--fg-3)', marginLeft: 16, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{it.line}</div>}
                              </div>
                            ))}
                          </div>
                          <button onClick={handleImprove} disabled={improving} style={{ marginTop: 12, width: '100%', padding: '9px', borderRadius: 8, border: '1px solid var(--accent)', background: 'transparent', color: 'var(--accent)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
                            {improving ? 'Regenerating a corrected fix…' : '✨ Improve this fix — fix the issues automatically'}
                          </button>
                        </>
                      )
                    )}
                  </div>
                )}

                {!merged && !reverted && !prClosed && (() => {
                  const hasErrors = review && review !== 'checking' && !review.unavailable && (review.issues || []).some((i) => i.severity === 'error');
                  return (
                    <div style={{ display: 'flex', gap: 8 }}>
                      {appliesToCodebase && (
                        <button className={hasErrors ? 'btn' : 'btn btn-primary'} onClick={handleMerge} disabled={merging || review === 'checking'} style={{ flex: 1 }}>
                          {merging ? 'Merging…' : review === 'checking' ? 'Reviewing…' : hasErrors ? 'Merge anyway — issues found' : 'Merge now → deploy'}
                        </button>
                      )}
                      <button className="btn" onClick={handleClosePr} disabled={merging} style={{ color: '#f0746e', flex: appliesToCodebase ? '0 0 auto' : 1 }}>{merging ? '…' : 'Discard PR'}</button>
                    </div>
                  );
                })()}

                {prClosed && <div style={{ fontSize: 13.5, color: 'var(--fg-2)' }}>✕ PR closed and branch deleted. Re-open the fix to ship a corrected version.</div>}

                {merged && (
                  <div>
                    <div style={{ fontSize: 13.5, color: 'var(--fg-2)', marginBottom: 10 }}>
                      {verify === 'checking' && '● Verifying it’s live in your production HTML…'}
                      {verify === 'ok' && <span style={{ color: 'var(--good, #5ad08a)' }}>✓ Verified live in your homepage.</span>}
                      {verify === 'pending' && 'Deployed — your site may still be propagating; it’ll appear shortly.'}
                    </div>
                    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, fontSize: 13, color: 'var(--fg-2)', background: 'rgba(0,0,0,0.18)', border: '1px solid var(--line)', borderRadius: 8, padding: '10px 12px', marginBottom: 12, lineHeight: 1.5 }}>
                      <span className="pulse" style={{ marginTop: 5, flexShrink: 0 }} />
                      <span>AISO re-scans this site in <strong style={{ color: 'var(--fg-0)' }}>24 hours</strong>, compares to today's baseline, and notifies you if your AI visibility moved. <button className="link-btn" onClick={() => onSettings && onSettings('notifications')}>Notification settings →</button></span>
                    </div>
                    <button className="btn" onClick={handleRevert} disabled={reverting}>
                      {reverting ? 'Reverting…' : '↩ Revert this fix'}
                    </button>
                  </div>
                )}

                {reverted && (
                  <div style={{ fontSize: 13.5, color: 'var(--fg-2)' }}>Original file restored. Re-open the fix to deploy again.</div>
                )}

                {mergeError && (
                  <div className="approve-error" style={{ marginTop: 10 }}>
                    <strong>Couldn't complete that.</strong>
                    <div style={{ marginTop: 6, fontFamily: 'var(--mono)', fontSize: 12 }}>{mergeError} — <a href={shippedPr.url} target="_blank" rel="noopener noreferrer" className="link-btn">finish on GitHub →</a></div>
                  </div>
                )}
              </div>
            )}
          </aside>
        </div>

        {/* timeline */}
        <div className="timeline">
          <h3>The deploy</h3>
          {fixSteps.map((s, i) => (
            <div key={i} className={`tl-step ${s.state}`}>
              <div className="bullet" />
              <div>
                <div className="tl-label">{s.label}</div>
                <div className="tl-sub">{s.sub}</div>
              </div>
              <div className="tl-time">{s.time}</div>
            </div>
          ))}
        </div>

        <div className="daily" style={{ marginTop: 0 }}>
          <div>
            <h3>And then?</h3>
            <p>We re-scan in 24 hours, compare to today's baseline, and learn which fixes actually moved the needle for {industry || 'your category'}{location && location !== 'Online' && location !== 'Global' ? ` in ${location}` : ''}. Your next nine fixes get re-ranked on the way back up.</p>
          </div>
          <button className="btn btn-primary" onClick={onBack}>
            Back to report <Arrow />
          </button>
        </div>
      </div>
    </section>
  );
}

function CopyButton({ text }) {
  const [copied, setCopied] = useState(false);
  return (
    <button
      className="more"
      onClick={() => { navigator.clipboard?.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1500); }}
    >
      {copied ? '✓ Copied' : 'Copy code'}
    </button>
  );
}

// ---------- tiny inline icons ----------
function Arrow() { return <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7h10M8 3l4 4-4 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>; }
function Tick() { return <svg className="tick" width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2.5 7.5l3 3 6-6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>; }
function Plus() { return <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M6 1.5v9M1.5 6h9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>; }
function Sparkle() { return <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1.5l1.2 3.3L11.5 6 8.2 7.2 7 10.5 5.8 7.2 2.5 6l3.3-1.2L7 1.5z" fill="currentColor"/></svg>; }

// ---------- root ----------
// Lightweight toast system — replaces jarring browser alert() popups.
function toast(text, kind) { try { window.dispatchEvent(new CustomEvent('aiso:toast', { detail: { text, kind } })); } catch {} }
window.aisoToast = toast;
function Toasts() {
  const [items, setItems] = useState([]);
  useEffect(() => {
    const onT = (e) => {
      const id = Math.random().toString(36).slice(2);
      setItems((x) => [...x, { id, text: e.detail.text, kind: e.detail.kind || 'info' }]);
      setTimeout(() => setItems((x) => x.filter((t) => t.id !== id)), 4500);
    };
    window.addEventListener('aiso:toast', onT);
    return () => window.removeEventListener('aiso:toast', onT);
  }, []);
  if (!items.length) return null;
  return (
    <div style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 2000, display: 'flex', flexDirection: 'column', gap: 8, maxWidth: 'min(360px, calc(100vw - 40px))' }}>
      {items.map((t) => (
        <div key={t.id} className="fade-in" style={{ display: 'flex', alignItems: 'flex-start', gap: 10, padding: '12px 14px', borderRadius: 10, border: `1px solid ${t.kind === 'error' ? 'rgba(240,116,110,0.45)' : t.kind === 'success' ? 'rgba(90,208,138,0.45)' : 'var(--line)'}`, background: 'var(--bg-1, #14161b)', boxShadow: '0 12px 34px rgba(0,0,0,0.45)', fontSize: 13.5, color: 'var(--fg-0)' }}>
          <span style={{ color: t.kind === 'error' ? '#f0746e' : t.kind === 'success' ? 'var(--good, #5ad08a)' : 'var(--accent)', marginTop: 1, flexShrink: 0 }}>{t.kind === 'error' ? '✕' : t.kind === 'success' ? '✓' : '●'}</span>
          <span style={{ lineHeight: 1.45 }}>{t.text}</span>
        </div>
      ))}
    </div>
  );
}

function App() {
  // Persist ONLY the report across reloads (your actual work, so a refresh doesn't make
  // you re-scan). Marketing/settings/transient scenes are NOT restored — otherwise the
  // last page you happened to view (Pricing, Docs) became the "default" on reload.
  const [scene, setScene] = useState(() => {
    try {
      let saved = localStorage.getItem('aiso:scene');
      if (saved === 'fix') saved = 'report';
      if (saved !== 'report') return 'landing';
      const prof = JSON.parse(localStorage.getItem('aiso:profile:v1') || '{}');
      const site = (prof.sites || []).find((s) => s.id === prof.activeSiteId);
      if (!site || !(site.discovered || site.lastScan)) return 'landing';
      return 'report';
    } catch { return 'landing'; }
  }); // landing | scanning | report | fix | settings | how | pricing | docs
  const [openFix, setOpenFix] = useState(1);
  const [settingsTab, setSettingsTab] = useState('sites');

  const account = useProfile();
  const { profile, activeSite, update, updateActiveSite, updateSiteById, addSite, removeSite, setActiveSite, updateNotifications, saveSiteScan, signOut } = account;

  // ---- account (central Supabase): bootstrap config, restore session, sync profile ----
  const [authOpen, setAuthOpen] = useState(false);
  const [authed, setAuthed] = useState(() => (window.supaIsAuthed ? window.supaIsAuthed() : false));
  useEffect(() => {
    if (!window.supaBootstrap) return;
    (async () => {
      await window.supaBootstrap();
      if (window.supaIsAuthed && window.supaIsAuthed()) {
        setAuthed(true);
        try { const cloud = await window.supaPullProfile(); if (cloud && Array.isArray(cloud.sites) && cloud.sites.length) update(() => cloud); } catch {}
      }
    })();
  }, []);
  useEffect(() => { if (authed && window.supaMaybeSync) window.supaMaybeSync(profile); }, [profile, authed]);
  const onAuthed = async () => {
    setAuthed(true);
    window.aisoToast && window.aisoToast('Signed in — your data is synced to your account.', 'success');
    try { const cloud = await window.supaPullProfile(); if (cloud && Array.isArray(cloud.sites) && cloud.sites.length) update(() => cloud); } catch {}
  };
  const onSignOut = () => { try { window.supaSignOut && window.supaSignOut(); } catch {} setAuthed(false); signOut(); };
  useEffect(() => { window.aisoOpenAuth = () => setAuthOpen(true); }, []);

  const [domain, setDomain] = useState(activeSite?.domain || 'bekon.ai');
  const [showOnboarding, setShowOnboarding] = useState(() => {
    const had = (() => { try { return !!localStorage.getItem('aiso:profile:v1'); } catch { return false; } })();
    return !had;
  });

  // real scan state: { running, done, total, result }
  const [scanState, setScanState] = useState({ running: false, done: 0, total: 0, result: activeSite?.lastScan || null });
  const scanAbortRef = useRef(null);

  // real fixes from homepage analysis (verified-live)
  const [realFixes, setRealFixes] = useState(null);

  // discovered brand data (homepage-scraped + Claude-enriched)
  const [discovered, setDiscovered] = useState(activeSite?.discovered || null);

  const tweakDefaults = (typeof window !== 'undefined' && window.AISO_TWEAKS) || { theme: 'dark', accentHue: 232, density: 'airy' };
  // The design host normally owns theme, but the deployed site has no host — so
  // persist the user's choice in localStorage and restore it on load.
  let _savedTheme = null;
  try { _savedTheme = localStorage.getItem('aiso:theme'); } catch {}
  const [tweaks, setTweak] = useTweaks(_savedTheme ? { ...tweakDefaults, theme: _savedTheme } : tweakDefaults);
  const toggleTheme = () => {
    const next = tweaks.theme === 'light' ? 'dark' : 'light';
    setTweak('theme', next);
    try { localStorage.setItem('aiso:theme', next); } catch {}
  };

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', tweaks.theme);
    document.documentElement.style.setProperty('--accent-h', tweaks.accentHue);
    document.documentElement.style.setProperty('--accent-h2', (Number(tweaks.accentHue) + 60) % 360);
  }, [tweaks.theme, tweaks.accentHue]);

  useEffect(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }, [scene]);

  // Remember the current page so a browser refresh restores it (transient scenes excluded).
  useEffect(() => { try { if (scene !== 'scanning') localStorage.setItem('aiso:scene', scene); } catch {} }, [scene]);

  // GitHub OAuth callback: /api/github-callback redirects back with the token in the URL
  // hash (#gh_token=…). Pick it up, connect, clean the hash, open Settings → Integrations.
  useEffect(() => {
    const hash = window.location.hash || '';
    const tokenM = hash.match(/[#&]gh_token=([^&]+)/);
    const errM = hash.match(/[#&]gh_error=([^&]+)/);
    if (!tokenM && !errM) return;
    const clean = () => window.history.replaceState({}, '', window.location.pathname + window.location.search);
    if (errM) { console.warn('GitHub OAuth error:', decodeURIComponent(errM[1])); clean(); return; }
    (async () => {
      try {
        const token = decodeURIComponent(tokenM[1]);
        const user = await window.ghTestToken(token);
        window.ghSave({ pat: token, user });
        clean();
        setSettingsTab('integrations');
        transitionTo('settings');
      } catch (e) { console.warn('GitHub OAuth failed', e); clean(); }
    })();
  }, []);

  // Supabase magic-link / email-confirmation redirect: lands at /#access_token=…&refresh_token=…
  // Pick up the session so clicking the link in the email signs you straight in.
  useEffect(() => {
    const hash = window.location.hash || '';
    const at = hash.match(/[#&]access_token=([^&]+)/);
    if (!at) return;
    const rt = hash.match(/[#&]refresh_token=([^&]+)/);
    (async () => {
      try {
        if (window.supaBootstrap) await window.supaBootstrap();
        const s = window.supaLoad ? window.supaLoad() : {};
        const access_token = decodeURIComponent(at[1]);
        const refresh_token = rt ? decodeURIComponent(rt[1]) : null;
        let user = null;
        if (s.url) { try { const r = await fetch(`${s.url.replace(/\/$/, '')}/auth/v1/user`, { headers: { apikey: s.anonKey, authorization: `Bearer ${access_token}` } }); if (r.ok) user = await r.json(); } catch {} }
        window.supaSave({ session: { access_token, refresh_token, token_type: 'bearer', user }, user });
        window.history.replaceState({}, '', window.location.pathname + window.location.search);
        setAuthed(true);
        onAuthed();
      } catch (e) { console.warn('Supabase session pickup failed', e); }
    })();
  }, []);

  // When active site changes OR its domain updates (e.g. via onboarding edit),
  // sync the landing input + reset transient scan state.
  useEffect(() => {
    if (!activeSite) return;
    setDomain(activeSite.domain || 'bekon.ai');
    setScanState((s) => ({ ...s, result: activeSite.lastScan || null }));
    setRealFixes(null);
    setDiscovered(activeSite.discovered || null);
  }, [activeSite?.id, activeSite?.domain]);

  // When entering Report, kick off real fixes analysis for the active domain.
  // Then enrich the top "shipping" fix with Claude-generated content (FAQ pairs + before/after).
  useEffect(() => {
    if (scene !== 'report' || !domain) return;
    let cancelled = false;
    const brandForFixes = discovered || activeSite;
    if (!brandForFixes?.domain) return;
    (async () => {
      try {
        const fx = await window.analyzeFixes(brandForFixes);
        if (cancelled || !fx || fx.length === 0) return;
        setRealFixes(fx);

        // Find the shipping fix and enrich with Claude
        const ship = fx.find(f => f.shipping);
        if (ship) {
          const aiContent = await window.generateFixContentWithClaude(brandForFixes, ship, {
            anthropicKey: profile.keys?.anthropic,
          });
          if (!cancelled && aiContent) {
            const enriched = window.rebuildShipFixWithClaude(ship, brandForFixes, aiContent);
            setRealFixes(prev => prev.map(f => f.id === ship.id ? enriched : f));
          }
        }
      } catch (e) {
        console.warn('Fix analysis failed', e);
      }
    })();
    return () => { cancelled = true; };
  }, [scene, domain, discovered?.discoveredAt, profile.keys?.anthropic]);

  /* Auto-run a real scan when we arrive at the report in managed mode and
   * haven't yet measured this site. No sample-data fallback when AISO has
   * its own keys server-side.
   */
  useEffect(() => {
    if (scene !== 'report') return;
    if (!window.aisoEnabled || !window.aisoEnabled()) return;
    if (scanState.running || scanState.result) return;
    const brand = discovered || activeSite;
    if (!brand?.name) return;
    // Wait until /api/health has resolved so we know which providers are wired
    const ready = window.__aisoManaged?.ready;
    const goOrSubscribe = () => {
      const wired = (window.getManagedProviders?.() || new Set());
      if (wired.size === 0) return;
      runRealScan();
    };
    if (ready) {
      goOrSubscribe();
    } else {
      const on = () => { window.removeEventListener('aiso:managed-ready', on); goOrSubscribe(); };
      window.addEventListener('aiso:managed-ready', on);
      return () => window.removeEventListener('aiso:managed-ready', on);
    }
  }, [scene, discovered?.discoveredAt, activeSite?.id]);

  async function runRealScan() {
    if (scanState.running) return;
    const brand = discovered || activeSite;
    if (!brand?.name) { window.aisoToast('Run discovery first.', 'error'); return; }
    const ctrl = new AbortController();
    scanAbortRef.current = ctrl;
    setScanState({ running: true, done: 0, total: 0, result: scanState.result, brandName: brand.name, industry: brand.industry, location: brand.location });
    try {
      const result = await window.runRealScan({
        profile,
        brand,
        signal: ctrl.signal,
        onProgress: (p) => {
          if (p.phase === 'start') setScanState((s) => ({ ...s, total: p.total }));
          if (p.phase === 'tick') setScanState((s) => ({ ...s, done: p.done }));
        },
      });
      setScanState({ running: false, done: 0, total: 0, result, brandName: brand.name, industry: brand.industry, location: brand.location });
      if (activeSite) {
        const prevScore = activeSite.lastScan?.score;
        saveSiteScan(activeSite.id, result);
        // Append to time-series history
        window.recordScanIntoHistory(activeSite.id, result);
        window.dispatchEvent(new CustomEvent('aiso:scan-recorded'));
        // Real notification: if the score crossed your threshold, deliver it to Slack.
        const N = profile.notifications || {};
        if (N.slack && N.slackWebhook && prevScore != null && Math.abs((result.score || 0) - prevScore) >= (N.threshold || 5)) {
          const delta = (result.score || 0) - prevScore;
          fetch('/api/notify', {
            method: 'POST', headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ webhook: N.slackWebhook, text: `*AISO · ${activeSite.name}* visibility ${delta >= 0 ? 'rose' : 'dropped'} ${prevScore}% → ${result.score}% (${delta >= 0 ? '+' : ''}${delta}). ${location.origin}` }),
          }).catch(() => {});
        }
      }
    } catch (e) {
      setScanState((s) => ({ ...s, running: false }));
      window.aisoToast(e.message || String(e), 'error');
    }
  }

  function cancelRealScan() {
    scanAbortRef.current?.abort();
    setScanState((s) => ({ ...s, running: false }));
  }

  // Wrap scene transitions in startViewTransition when supported — gives a
  // browser-native cross-fade plus smooth morph for any element tagged with
  // view-transition-name (the orb especially).
  function transitionTo(nextScene) {
    if (typeof document.startViewTransition === 'function') {
      document.startViewTransition(() => {
        setScene(nextScene);
      });
    } else {
      setScene(nextScene);
    }
  }

  const onLogo = () => transitionTo('landing');
  const onSettings = (tab = 'sites') => {
    setSettingsTab(typeof tab === 'string' ? tab : 'sites');
    transitionTo('settings');
  };

  /* Add a site + immediately kick off discovery/scan. Used by both
   * Settings → "Add a site" form and the topbar SiteSwitcher inline form. */
  function onAddAndScan(rawDomain) {
    const cleaned = String(rawDomain || '')
      .replace(/^https?:\/\//, '')
      .replace(/^www\./, '')
      .replace(/\/.*$/, '')
      .trim();
    if (!cleaned) return null;
    const stem = cleaned.split('.')[0];
    const name = stem.charAt(0).toUpperCase() + stem.slice(1);
    const site = addSite({ name, domain: cleaned, industry: '', location: '' });
    setActiveSite(site.id);
    setDomain(cleaned);
    setDiscovered(null);
    transitionTo('scanning');
    return site;
  }

  // Switch to a site: VIEW its report if it's already been scanned, else start a scan.
  function onSelectSite(site) {
    if (!site) return;
    setActiveSite(site.id);
    setDomain(site.domain || '');
    if (site.discovered || site.lastScan) {
      setDiscovered(site.discovered || null);
      transitionTo('report');
    } else {
      setDiscovered(null);
      transitionTo('scanning');
    }
  }

  return (
    <div className="app">
      <Topbar
        scene={scene}
        onLogo={onLogo}
        onSettings={onSettings}
        onNav={transitionTo}
        theme={tweaks.theme}
        onToggleTheme={toggleTheme}
        withBorder={scene !== 'landing'}
        profile={profile}
        activeSite={activeSite}
        setActiveSite={setActiveSite}
        addSite={addSite}
        onAddAndScan={onAddAndScan}
        onSelectSite={onSelectSite}
        authed={authed}
        onSignIn={() => setAuthOpen(true)}
      />

      {authOpen && window.AuthModal && <window.AuthModal onClose={() => setAuthOpen(false)} onAuthed={onAuthed} />}
      <Toasts />

      <main className="stage">
        {scene === 'landing' && (
          <Landing
            initialDomain={domain}
            onScan={(d) => {
              setDomain(d);
              setDiscovered(null);
              transitionTo('scanning');
            }}
          />
        )}
        {scene === 'scanning' && (
          <Scanning
            domain={domain}
            anthropicKey={profile.keys?.anthropic}
            onDone={(result) => {
              setDiscovered(result);
              // persist into the active site (or create one if none)
              const patch = {
                domain: result.domain,
                name: result.name,
                industry: result.industry,
                location: result.location,
                description: result.description,
                tagline: result.tagline,
                logo: result.logo,
                favicon: result.favicon,
                address: result.address,
                competitors: (result.competitors || []).map((c) => c.name || c),
                discovered: result,
                lastDiscoveredAt: Date.now(),
              };
              if (activeSite) {
                updateActiveSite(patch);
              } else {
                addSite({ ...patch });
              }
              transitionTo('report');
            }}
          />
        )}
        {scene === 'report' && (
          <Report
            brand={discovered || activeSite || { name: 'Unknown', domain }}
            domain={domain}
            discovered={discovered}
            profile={profile}
            activeSite={activeSite}
            scanState={scanState}
            realFixes={realFixes}
            onRunRealScan={runRealScan}
            onCancelRealScan={cancelRealScan}
            onOpenFix={(id) => { setOpenFix(id); transitionTo('fix'); }}
            onReset={() => transitionTo('landing')}
            onSettings={onSettings}
            updateNotifications={updateNotifications}
          />
        )}
        {scene === 'fix' && (
          <FixDetail
            fixId={openFix}
            fixes={realFixes || []}
            brand={discovered || activeSite}
            site={activeSite}
            siteRepo={activeSite?.repo}
            sitePrs={activeSite?.prs || []}
            onPrOpened={(pr) => activeSite && updateSiteById(activeSite.id, { prs: [...(activeSite.prs || []).filter((p) => p.number !== pr.number), pr] })}
            onConnectRepo={(repo) => activeSite && updateSiteById(activeSite.id, { repo })}
            onBack={() => transitionTo('report')}
            onSettings={onSettings}
          />
        )}
        {scene === 'settings' && (
          <Settings
            profile={profile}
            activeSite={activeSite}
            update={update}
            updateActiveSite={updateActiveSite}
            updateSiteById={updateSiteById}
            addSite={addSite}
            onAddAndScan={onAddAndScan}
            removeSite={removeSite}
            setActiveSite={setActiveSite}
            updateNotifications={updateNotifications}
            signOut={signOut}
            onBack={() => transitionTo(activeSite ? 'report' : 'landing')}
            initialTab={settingsTab}
          />
        )}
        {scene === 'how' && <HowItWorks onStart={() => transitionTo('landing')} onNav={transitionTo} />}
        {scene === 'pricing' && <Pricing onStart={() => transitionTo('landing')} onNav={transitionTo} onSettings={onSettings} />}
        {scene === 'docs' && <Docs onNav={transitionTo} />}
        {scene === 'reports' && (
          <Reports
            profile={profile}
            sites={profile.sites || []}
            activeSite={activeSite}
            scanState={scanState}
            realFixes={realFixes}
            onOpenSettings={onSettings}
            onBack={() => transitionTo(activeSite ? 'report' : 'landing')}
          />
        )}
        {scene === 'workflows' && (
          <Workflows
            profile={profile}
            sites={profile.sites || []}
            activeSite={activeSite}
            setActiveSite={setActiveSite}
            onBack={() => transitionTo(activeSite ? 'report' : 'landing')}
            onOpenSettings={onSettings}
          />
        )}
      </main>

      <Onboarding
        open={showOnboarding}
        onDone={(updated) => {
          // If onboarding returned a domain, apply it immediately so the landing
          // input reflects what the user just typed (covers same-tick state batching).
          if (updated?.domain) setDomain(updated.domain);
          setShowOnboarding(false);
        }}
        onSkip={() => setShowOnboarding(false)}
        profile={profile}
        update={update}
        updateActiveSite={updateActiveSite}
      />

      <footer className="footer-strip">
        <div>AISO — Observatory release · v2026.05</div>
        <div style={{ display: 'flex', gap: 24 }}>
          <span>made for {activeSite?.location || '—'}</span>
          <span>built quietly</span>
        </div>
      </footer>

      <TweaksPanel title="Tweaks">
        <TweakSection title="Theme">
          <TweakRadio
            label="Mode"
            value={tweaks.theme}
            options={[{ value: 'dark', label: 'Dark' }, { value: 'light', label: 'Light' }]}
            onChange={(v) => setTweak('theme', v)}
          />
        </TweakSection>
        <TweakSection title="Accent">
          <TweakSlider label="Hue" min={180} max={340} step={2} value={Number(tweaks.accentHue)} onChange={(v) => setTweak('accentHue', v)} />
          <div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
            {[
              { h: 232, name: 'Spectral blue' },
              { h: 280, name: 'Iridium' },
              { h: 320, name: 'Magenta' },
              { h: 200, name: 'Glacier' },
            ].map(p => (
              <button
                key={p.h}
                onClick={() => setTweak('accentHue', p.h)}
                style={{
                  width: 28, height: 28, borderRadius: 8,
                  border: '1px solid var(--line)',
                  background: `linear-gradient(120deg, oklch(78% 0.16 ${p.h}), oklch(76% 0.16 ${(p.h + 60) % 360}))`,
                  cursor: 'pointer',
                  boxShadow: Number(tweaks.accentHue) === p.h ? '0 0 0 2px var(--fg-0)' : 'none',
                }}
                title={p.name}
              />
            ))}
          </div>
        </TweakSection>
        <TweakSection title="Jump to">
          <div style={{ display: 'grid', gap: 6 }}>
            <TweakButton onClick={() => setScene('landing')}>Landing</TweakButton>
            <TweakButton onClick={() => setScene('scanning')}>Scanning</TweakButton>
            <TweakButton onClick={() => setScene('report')}>Report</TweakButton>
            <TweakButton onClick={() => setScene('fix')}>Auto-fix detail</TweakButton>
            <TweakButton onClick={() => setScene('settings')}>Settings</TweakButton>
          </div>
        </TweakSection>
      </TweaksPanel>
    </div>
  );
}

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