// Main composite views for the ezscreenshots ASO prototype.

const { useState: useStateV, useEffect: useEffectV, useMemo: useMemoV } = React;

/* ── Keyword discovery pane (left column) ────────────────────── */
function KeywordDiscovery({ keyword, suggestions, savedSet, onPick, onToggleSave }) {
  // Split suggestions into "exact match family" and "related".
  const family = suggestions;
  // Heuristic: sort by priority desc
  const sorted = [...family].sort((a, b) => b.priority - a.priority);

  return (
    <div>
      <Card
        title="Keyword discovery"
        meta={<span>{sorted.length} terms</span>}
      >
        <div className="kw-section-title">Top by priority</div>
        <div className="kw-list">
          {sorted.map(s => (
            <KeywordRow
              key={s.term}
              term={s.term}
              priority={s.priority}
              active={s.term === keyword}
              saved={savedSet.has(s.term)}
              onPick={() => onPick(s.term)}
              onToggleSave={() => onToggleSave(s.term)}
            />
          ))}
          {sorted.length === 0 && (
            <div style={{ padding: 16, color: "var(--ui-ink-muted)", fontSize: 12, lineHeight: 1.5 }}>
              No suggestions yet. Try a broader seed term.
            </div>
          )}
        </div>
      </Card>
    </div>
  );
}

function KeywordRow({ term, priority, active, saved, onPick, onToggleSave }) {
  const pct = Math.max(3, Math.min(100, (priority / 10000) * 100));
  return (
    <div className={"kw-row" + (active ? " active" : "")} onClick={onPick}>
      <span className="kw-term" title={term}>{term}</span>
      <div className="kw-bar-wrap">
        <div className="kw-bar" style={{ width: pct + "%" }} />
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 6, justifyContent: "flex-end" }}>
        <button
          className={"kw-star" + (saved ? " starred" : "")}
          onClick={(e) => { e.stopPropagation(); onToggleSave(); }}
          title={saved ? "Remove from saved" : "Save keyword"}
        >
          <Icon name={saved ? "star-fill" : "star"} size={14} />
        </button>
      </div>
    </div>
  );
}

/* ── Saved keywords pane ─────────────────────────────────────── */
function SavedKeywords({ saved, onPick, onUnsave }) {
  return (
    <div className="saved-list-card">
      <Card
        title="Saved keywords"
        meta={<span>{saved.length}</span>}
      >
        {saved.length === 0 ? (
          <div className="saved-empty">
            Star any keyword to <em>track it here</em>. Saved across reloads.
          </div>
        ) : (
          <div className="kw-list">
            {saved.map(s => (
              <KeywordRow
                key={s.term}
                term={s.term}
                priority={s.priority}
                active={false}
                saved={true}
                onPick={() => onPick(s.term)}
                onToggleSave={() => onUnsave(s.term)}
              />
            ))}
          </div>
        )}
      </Card>
    </div>
  );
}

/* ── Apps list (right column) ────────────────────────────────── */
function AppsList({ keyword, apps, selectedAppId, pinned, onSelect, onTogglePin }) {
  return (
    <Card
      title={<>Top apps for <span style={{ textTransform: "none", letterSpacing: 0, fontFamily: "var(--display)", fontStyle: "italic", color: "var(--accent)", fontSize: 13, fontWeight: 500 }}>"{keyword}"</span></>}
      meta={<span>{apps.length} results</span>}
    >
      {apps.length === 0 ? (
        <div style={{ padding: 24, textAlign: "center", color: "var(--ui-ink-muted)", fontSize: 13 }}>
          No apps in our index for this term yet. <br />
          <span style={{ fontFamily: "var(--mono)", fontSize: 10, letterSpacing: 1.2, textTransform: "uppercase" }}>
            try a curated seed like "habit tracker"
          </span>
        </div>
      ) : (
        <div className="apps-list">
          {apps.map((app, idx) => (
            <AppRow
              key={app.appId}
              rank={idx + 1}
              app={app}
              selected={selectedAppId === app.appId}
              pinned={pinned.includes(app.appId)}
              onSelect={() => onSelect(app.appId)}
              onTogglePin={(e) => { e.stopPropagation(); onTogglePin(app.appId); }}
            />
          ))}
        </div>
      )}
    </Card>
  );
}

function AppRow({ rank, app, selected, pinned, onSelect, onTogglePin }) {
  return (
    <div className={"app-row" + (selected ? " selected" : "")} onClick={onSelect}>
      <span className={"app-rank" + (rank <= 3 ? " top" : "")}>{String(rank).padStart(2, "0")}</span>
      <AppIcon app={app} />
      <div className="app-meta">
        <div className="app-title">{app.title}</div>
        {app.subtitle && <div className="app-subtitle">{app.subtitle}</div>}
        <div className="app-sub">
          <span className="dev">{app.developer}</span>
          {" · "}
          {app.free
            ? <span className="price-free">FREE</span>
            : <span className="price-paid">${app.price.toFixed(2)}</span>}
          {" · "}
          <span>{app.primaryGenre}</span>
        </div>
      </div>
      <div className="app-numbers">
        <StarBadge score={app.score} />
        <div className="app-reviews">{fmt.num(app.reviews)} REVIEWS</div>
      </div>
      <button
        className={"pin-btn" + (pinned ? " pinned" : "")}
        onClick={onTogglePin}
        title={pinned ? "Unpin from compare" : "Pin to compare"}
      >
        <Icon name={pinned ? "pin-fill" : "pin"} size={14} />
      </button>
    </div>
  );
}

/* ── Difficulty calc ─────────────────────────────────────────── */
function computeDifficulty(apps) {
  if (apps.length === 0) return { score: 0, label: "—", tier: "easy" };
  const top10 = apps.slice(0, 10);
  // Difficulty heuristic: avg log10(reviews) of top 10 + avg score weight.
  const avgReviews = top10.reduce((s, a) => s + a.reviews, 0) / top10.length;
  const avgScore = top10.reduce((s, a) => s + a.score, 0) / top10.length;
  // Map log reviews 0–7 → 0–100, then nudge by score.
  const rawDiff = Math.min(100, Math.max(0, (Math.log10(avgReviews + 1) / 7) * 90 + (avgScore - 4.0) * 10));
  const score = Math.round(rawDiff);
  let label, tier;
  if (score < 35) { label = "Easy"; tier = "easy"; }
  else if (score < 65) { label = "Medium"; tier = "medium"; }
  else { label = "Hard"; tier = "hard"; }
  return { score, label, tier, avgReviews, avgScore };
}

/* ── Results header with stats ───────────────────────────────── */
function ResultsHeader({ keyword, apps, suggestions, showDifficulty = true }) {
  const diff = useMemoV(() => computeDifficulty(apps), [apps]);
  const totalReviews = useMemoV(() => apps.slice(0, 10).reduce((s, a) => s + a.reviews, 0), [apps]);

  return (
    <div className="results-header">
      <h2 className="rh-title">
        Researching <em>"{keyword}"</em>
      </h2>
      <div className="rh-stats">
        {showDifficulty && (
          <div className={"stat-pill difficulty-" + diff.tier}>
            <div className="sp-cap">Difficulty</div>
            <div className="sp-value">{diff.label}</div>
          </div>
        )}
        <div className="stat-pill">
          <div className="sp-cap">Top 10 reviews</div>
          <div className="sp-value">{fmt.num(totalReviews)} combined</div>
        </div>
      </div>
    </div>
  );
}

/* ── Audit checks ────────────────────────────────────────────── */
function buildAudit(app, iphoneCount, ipadCount) {
  const checks = [];
  // Title length: ideal 25-30 chars (App Store cap is 30)
  const titleLen = app.title.length;
  let titleStatus = "green";
  let titleDesc = "Uses near the full 30-character cap — strong keyword surface.";
  if (titleLen < 18) { titleStatus = "amber"; titleDesc = "Short title — you have room to add keywords up to 30 chars."; }
  if (titleLen > 30) { titleStatus = "red"; titleDesc = "Over the 30-char App Store limit; will be truncated."; }
  checks.push({
    key: "title", title: "Title length", status: titleStatus, desc: titleDesc,
    value: titleLen, sub: "of 30 chars"
  });

  // Subtitle present (30 char cap) — omit if API didn't return it
  if (app.subtitle) {
    const subLen = app.subtitle.length;
    let subStatus = "green", subDesc = "Subtitle present, taking advantage of the 30-char ranking field.";
    if (subLen < 12) { subStatus = "amber"; subDesc = "Subtitle is short; you have room for more keywords."; }
    else if (subLen > 30) { subStatus = "red"; subDesc = "Subtitle exceeds 30 chars and will be truncated."; }
    checks.push({
      key: "sub", title: "Subtitle", status: subStatus, desc: subDesc,
      value: subLen, sub: "of 30 chars"
    });
  }

  // Screenshot count — uses D1/AppRanks real count when available
  if (iphoneCount > 0) {
    let shotStatus = "green", shotDesc = "Strong screenshot coverage — full visual story.";
    if (iphoneCount < 3) { shotStatus = "red"; shotDesc = "Below 3 iPhone screenshots. Apple shows up to 10 — fill them in."; }
    else if (iphoneCount < 6) { shotStatus = "amber"; shotDesc = "Could add more — Apple allows 10. Most top apps use 6+."; }
    checks.push({
      key: "shots", title: "Screenshots", status: shotStatus, desc: shotDesc,
      value: iphoneCount, sub: "of 10 slots"
    });
  }

  // iPad screenshots
  if (ipadCount > 0) {
    checks.push({
      key: "ipad", title: "iPad screenshots", status: "green",
      desc: "iPad screenshots present — eligible for iPad search.",
      value: ipadCount, sub: "of 10 slots"
    });
  }

  // Languages: more = more locale-specific search reach
  let langStatus = "green", langDesc = "Localized across many App Store regions.";
  if (app.languages.length < 3) { langStatus = "red"; langDesc = "Only English-shaped reach — localization unlocks new regions."; }
  else if (app.languages.length < 6) { langStatus = "amber"; langDesc = "Decent reach. Top apps localize to 8+ languages."; }
  checks.push({
    key: "lang", title: "Languages", status: langStatus, desc: langDesc,
    value: app.languages.length, sub: "locales"
  });

  // Updated recently? <6 months = green, <12 = amber, else red.
  const daysSince = (Date.now() - new Date(app.updated).getTime()) / (1000 * 60 * 60 * 24);
  let upStatus = "green", upDesc = "Recent update — Apple favors actively maintained apps.";
  if (daysSince > 180 && daysSince <= 365) { upStatus = "amber"; upDesc = "Last update is getting old; freshness boosts ranking."; }
  if (daysSince > 365) { upStatus = "red"; upDesc = "No update in over a year — ranking signal degrades."; }
  checks.push({
    key: "updated", title: "Last update", status: upStatus, desc: upDesc,
    value: fmt.daysAgo(app.updated), sub: "freshness"
  });

  return checks;
}

/* ── Audit drawer ────────────────────────────────────────────── */
function AuditDrawer({ app, open, pinned, showCrossPromo = true, onClose, onTogglePin, myAppId, onSetMyApp }) {
  const [descOpen, setDescOpen] = useStateV(false);
  useEffectV(() => { if (open) setDescOpen(false); }, [open, app && app.appId]);

  // Use D1/AppRanks screenshot count when available, fall back to iTunes API count
  const iphoneCount = useMemoV(() => {
    if (app && app.screenshotImages && app.screenshotImages.length > 0) {
      return app.screenshotImages.filter(s => s.variant === 'iphone').length;
    }
    return app ? app.screenshots : 0;
  }, [app && app.appId, app && app.screenshotImages]);

  const ipadCount = useMemoV(() => {
    if (app && app.screenshotImages && app.screenshotImages.length > 0) {
      return app.screenshotImages.filter(s => s.variant === 'ipad').length;
    }
    return app ? app.ipadScreenshots : 0;
  }, [app && app.appId, app && app.screenshotImages]);

  const checks = useMemoV(() => app ? buildAudit(app, iphoneCount, ipadCount) : [], [app && app.appId, iphoneCount, ipadCount]);
  const histoMax = useMemoV(() => {
    if (!app || !app.histogram) return 1;
    return Math.max(...Object.values(app.histogram));
  }, [app && app.appId]);

  if (!app) return null;

  return (
    <React.Fragment>
      <div className={"audit-backdrop" + (open ? " open" : "")} onClick={onClose} />
      <aside className={"audit-drawer" + (open ? " open" : "")}>
        <div className="audit-header">
          <AppIcon app={app} size={64} />
          <div className="ah-text">
            <div className="ah-title">{app.title}</div>
            {app.subtitle && <div className="ah-subtitle">{app.subtitle}</div>}
            <div className="ah-dev">{app.developer}{app.primaryGenre ? <span className="ah-genre"> · {app.primaryGenre}</span> : null}</div>
            <div className="ah-id">id {app.id} · {app.appId}</div>
          </div>
          <button className="ah-close" onClick={onClose} title="Close (Esc)">
            <Icon name="x" size={16} />
          </button>
        </div>

        <div className="audit-body">
          <div className="audit-quick-stats">
            <div className="aqs">
              <div className="aqs-cap">Rating</div>
              <div className="aqs-val"><Icon name="star-fill" size={14} />{app.score.toFixed(1)}</div>
              <div className="aqs-sub">{fmt.num(app.reviews)} reviews</div>
            </div>
            <div className="aqs">
              <div className="aqs-cap">Current version</div>
              <div className="aqs-val">{app.currentVersionScore.toFixed(1)}</div>
              <div className="aqs-sub">{fmt.num(app.currentVersionReviews)} this version</div>
            </div>
            <div className="aqs">
              <div className="aqs-cap">Price</div>
              <div className="aqs-val" style={{ color: app.free ? "var(--green)" : "var(--ui-ink)" }}>
                {app.free ? "Free" : "$" + app.price.toFixed(2)}
              </div>
              <div className="aqs-sub">v{app.version} · {fmt.bytes(app.size)}</div>
            </div>
          </div>

          {/* Screenshots carousel */}
          {app.screenshotImages && app.screenshotImages.length > 0 && (
            <div className="audit-section">
              <div className="as-cap">
                Screenshots · {app.screenshotImages.filter(s => s.variant === 'iphone').length} iPhone
                {app.screenshotImages.some(s => s.variant === 'ipad')
                  ? ` + ${app.screenshotImages.filter(s => s.variant === 'ipad').length} iPad` : ''}
              </div>
              <ScreenshotCarousel
                shots={app.screenshotImages.filter(s => s.variant === 'iphone')}
                appName={app.title}
              />
            </div>
          )}

          {/* Description */}
          <div className="audit-section">
            <div className="as-cap">Description · first paragraph</div>
            <div className={"audit-desc" + (descOpen ? " expanded" : "")}>
              {app.description}
            </div>
            {app.description.length > 200 && (
              <button className="audit-desc-toggle" onClick={() => setDescOpen(o => !o)}>
                {descOpen ? "Show less ←" : "Read full →"}
              </button>
            )}
          </div>

          {/* Languages chips */}
          <div className="audit-section">
            <div className="as-cap">Localized in {app.languages.length} locales</div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
              {app.languages.map(l => (
                <span key={l} style={{
                  fontFamily: "var(--mono)", fontSize: 10, fontWeight: 700, letterSpacing: 0.6,
                  background: "var(--paper)", border: "1px solid var(--rule-deep)",
                  padding: "3px 7px", borderRadius: 4, color: "var(--ui-ink-soft)"
                }}>{l}</span>
              ))}
            </div>
          </div>

          {/* Audit checks */}
          <div className="audit-section">
            <div className="as-cap">Listing audit · what they're doing</div>
            <div className="check-list">
              {checks.map(c => (
                <div className="check-row" key={c.key}>
                  <div className={"check-dot " + c.status} />
                  <div className="check-body">
                    <div className="check-title">{c.title}</div>
                    <div className="check-desc">{c.desc}</div>
                  </div>
                  <div className="check-value">
                    {c.value}
                    <span className="sub">{c.sub}</span>
                  </div>
                </div>
              ))}
            </div>
          </div>

          {/* Ratings histogram */}
          {app.histogram && (
            <div className="audit-section">
              <div className="as-cap">Ratings distribution</div>
              <div className="histo">
                {["5", "4", "3", "2", "1"].map(star => {
                  const n = app.histogram[star] || 0;
                  const pct = histoMax ? (n / histoMax) * 100 : 0;
                  return (
                    <div className="histo-row" key={star}>
                      <span className="histo-star">{star}★</span>
                      <div className="histo-bar-wrap"><div className="histo-bar" style={{ width: pct + "%" }} /></div>
                      <span className="histo-count">{fmt.num(n)}</span>
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          {/* Actions */}
          <div style={{ display: "flex", gap: 8, marginTop: 4, flexWrap: "wrap" }}>
            <button
              className={"pin-btn" + (pinned ? " pinned" : "")}
              style={{ width: "auto", height: "auto", padding: "9px 14px", borderRadius: 8, fontFamily: "var(--sans)", fontSize: 12, fontWeight: 600, gap: 6 }}
              onClick={onTogglePin}
            >
              <Icon name={pinned ? "pin-fill" : "pin"} size={13} />
              <span style={{ marginLeft: 6 }}>{pinned ? "Pinned to compare" : "Pin to compare"}</span>
            </button>
            {(!myAppId || myAppId === app.appId) && (
              <button
                className={"mark-mine-btn" + (myAppId === app.appId ? " active" : "")}
                style={{ padding: "9px 14px", borderRadius: 8 }}
                onClick={() => onSetMyApp(myAppId === app.appId ? null : app.appId)}
              >
                {myAppId === app.appId ? "My app" : "Mark as mine"}
              </button>
            )}
            <a
              href={`https://apps.apple.com/us/app/id${app.id}`}
              target="_blank"
              rel="noopener noreferrer"
              style={{
                display: "inline-flex", alignItems: "center", gap: 6,
                padding: "9px 14px", borderRadius: 8,
                border: "1px solid var(--ui-rule)", background: "var(--ui-bg)",
                color: "var(--ui-ink)", fontSize: 12, fontWeight: 600, textDecoration: "none"
              }}
            >
              <Icon name="external" size={12} />
              View on App Store
            </a>
          </div>

          {/* Cross-promo */}
          {showCrossPromo && (
            <div className="cross-promo">
              <div className="cp-icon">e<span style={{ color: "rgba(255,255,255,0.7)" }}>.</span></div>
              <div className="cp-text">
                <strong>Like their screenshots?</strong> Recreate this layout in 60 seconds with ezscreenshots — drop your screen, theme it, export.
              </div>
              <a href="https://ezscreenshots.com" target="_blank" rel="noopener noreferrer">Open app →</a>
            </div>
          )}
        </div>
      </aside>
    </React.Fragment>
  );
}

/* ── Screenshot carousel + lightbox ─────────────────────────── */
function ScreenshotCarousel({ shots, appName }) {
  const [lightboxOpen, setLightboxOpen] = useStateV(false);
  const [lightboxIdx, setLightboxIdx] = useStateV(0);
  if (!shots || shots.length === 0) return null;
  return (
    <>
      <div className="shot-strip">
        {shots.map((s, idx) => (
          <img
            key={idx}
            className="shot-thumb"
            src={s.thumbUrl}
            alt={`${appName} screenshot ${idx + 1}`}
            loading="lazy"
            onClick={() => { setLightboxIdx(idx); setLightboxOpen(true); }}
          />
        ))}
      </div>
      {lightboxOpen && (
        <Lightbox
          shots={shots}
          startIndex={lightboxIdx}
          appName={appName}
          onClose={() => setLightboxOpen(false)}
        />
      )}
    </>
  );
}

function Lightbox({ shots, startIndex, appName, onClose }) {
  const [idx, setIdx] = useStateV(startIndex);

  useEffectV(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') { e.stopPropagation(); onClose(); }
      else if (e.key === 'ArrowLeft' && idx > 0) setIdx(i => i - 1);
      else if (e.key === 'ArrowRight' && idx < shots.length - 1) setIdx(i => i + 1);
    };
    document.addEventListener('keydown', onKey, true);
    return () => document.removeEventListener('keydown', onKey, true);
  }, [idx, shots.length, onClose]);

  return ReactDOM.createPortal(
    <div className="lightbox" onClick={onClose}>
      <div className="lightbox-stage" onClick={e => e.stopPropagation()}>
        <img className="lightbox-img" src={shots[idx].url} alt={`${appName} screenshot`} />
        <button className="lightbox-nav prev" disabled={idx === 0}
          onClick={() => setIdx(i => i - 1)}>‹</button>
        <button className="lightbox-nav next" disabled={idx === shots.length - 1}
          onClick={() => setIdx(i => i + 1)}>›</button>
        <button className="lightbox-close" onClick={onClose}>×</button>
      </div>
      <div className="lightbox-meta" onClick={e => e.stopPropagation()}>
        {appName} <span className="counter">{idx + 1} / {shots.length}</span>
      </div>
      <div className="lightbox-dots" onClick={e => e.stopPropagation()}>
        {shots.map((s, i) => (
          <button key={i} className={"lightbox-dot" + (i === idx ? " active" : "")}
            style={{ backgroundImage: `url(${s.thumbUrl})` }}
            onClick={() => setIdx(i)} />
        ))}
      </div>
    </div>,
    document.body
  );
}

/* ── Compare tray + modal ────────────────────────────────────── */
function CompareTray({ pinnedApps, onOpen, onOpenKeywords, onClose }) {
  return (
    <div className={"compare-tray" + (pinnedApps.length > 0 ? " open" : "")}>
      <span className="ct-cap">Pinned</span>
      <div className="ct-icons">
        {pinnedApps.map(a => (
          <AppIcon key={a.appId} app={a} size={32} />
        ))}
      </div>
      <span style={{ fontSize: 13, fontWeight: 500, opacity: 0.85, whiteSpace: "nowrap" }}>
        {pinnedApps.length} {pinnedApps.length === 1 ? "competitor" : "competitors"}
      </span>
      <div className="ct-actions">
        <button className="ct-btn ct-btn-accent" onClick={onOpen} disabled={pinnedApps.length < 2}>
          Compare
        </button>
        <button className="ct-btn ct-btn-outline" onClick={onOpenKeywords} disabled={pinnedApps.length < 1}>
          <Icon name="sparkle" size={12} />
          <span>Extract keywords</span>
        </button>
      </div>
      <button className="ct-close" onClick={onClose} title="Clear all"><Icon name="x" size={14} /></button>
    </div>
  );
}

function CompareModal({ pinnedApps, compareShots, onClose, onUnpin, myAppId, onSetMyApp }) {
  const [compareLightbox, setCompareLightbox] = useStateV(false);
  const [compareLightboxIdx, setCompareLightboxIdx] = useStateV(0);

  const myApp = myAppId ? pinnedApps.find(a => a.appId === myAppId) : null;
  const competitorLocales = useMemoV(() => {
    if (!myApp) return new Set();
    const all = new Set();
    pinnedApps.filter(a => a.appId !== myAppId).forEach(a => (a.languages || []).forEach(l => all.add(l)));
    return all;
  }, [pinnedApps, myAppId]);
  const missingLocales = useMemoV(() => {
    if (!myApp) return [];
    const mine = new Set(myApp.languages || []);
    return [...competitorLocales].filter(l => !mine.has(l)).sort();
  }, [myApp, competitorLocales]);

  if (pinnedApps.length === 0) return null;

  // Get real screenshot counts from compareShots (D1/AppRanks)
  const getIphoneShots = (app) => compareShots && compareShots[app.id]
    ? compareShots[app.id].filter(s => s.variant === 'iphone')
    : [];
  const getIpadShots = (app) => compareShots && compareShots[app.id]
    ? compareShots[app.id].filter(s => s.variant === 'ipad')
    : [];

  // Compute "winner" per metric using real screenshot data
  const winners = {
    score: maxBy(pinnedApps, a => a.score).appId,
    reviews: maxBy(pinnedApps, a => a.reviews).appId,
    screenshots: maxBy(pinnedApps, a => getIphoneShots(a).length || a.screenshots).appId,
    languages: maxBy(pinnedApps, a => a.languages.length).appId,
    titleLen: maxBy(pinnedApps, a => a.title.length).appId,
    subLen: maxBy(pinnedApps, a => (a.subtitle || "").length).appId,
    updated: maxBy(pinnedApps, a => new Date(a.updated).getTime()).appId,
  };

  const hasScreenshots = compareShots && Object.keys(compareShots).length > 0
    && pinnedApps.some(a => getIphoneShots(a).length > 0);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="compare-modal" onClick={e => e.stopPropagation()}>
        <div className="compare-modal-head">
          <h2>Side-by-side <em>compare</em></h2>
          <button className="ah-close" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="compare-modal-body">
          <table className="compare-table">
            <thead>
              <tr>
                <th className="col-row-label">App</th>
                {pinnedApps.map(a => (
                  <th key={a.appId} style={{ minWidth: 200 }} className={myAppId === a.appId ? "my-app-col" : ""}>
                    <div className="ct-app-head">
                      <AppIcon app={a} size={36} />
                      <div>
                        <div className="ct-app-name">{a.title}</div>
                        <div className="ct-app-dev">{a.developer}</div>
                      </div>
                    </div>
                    {(!myAppId || myAppId === a.appId) && (
                      <button
                        className={"mark-mine-btn" + (myAppId === a.appId ? " active" : "")}
                        onClick={() => onSetMyApp(myAppId === a.appId ? null : a.appId)}
                      >
                        {myAppId === a.appId ? "My app" : "Mark as mine"}
                      </button>
                    )}
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {/* Screenshots right below app name */}
              {hasScreenshots && (
                <tr>
                  <td className="col-row-label">
                    Screenshots
                    <button
                      className="compare-expand-btn"
                      onClick={() => { setCompareLightboxIdx(0); setCompareLightbox(true); }}
                      title="Compare screenshots fullscreen"
                    >
                      <Icon name="expand" size={12} />
                    </button>
                  </td>
                  {pinnedApps.map(a => {
                    const shots = getIphoneShots(a);
                    return (
                      <td key={a.appId}>
                        <div className="compare-shot-strip">
                          {shots.slice(0, 5).map((s, i) => (
                            <img
                              key={i}
                              className="compare-shot-thumb"
                              src={s.thumbUrl}
                              alt=""
                              loading="lazy"
                              onClick={() => { setCompareLightboxIdx(i); setCompareLightbox(true); }}
                              style={{ cursor: "pointer" }}
                            />
                          ))}
                          {shots.length > 5 && (
                            <span className="compare-shot-more">+{shots.length - 5}</span>
                          )}
                        </div>
                      </td>
                    );
                  })}
                </tr>
              )}
              <CompareRow label="Rating" apps={pinnedApps} render={a => `★ ${a.score.toFixed(1)}`} winner={winners.score} valueFn={a => a.score} />
              <CompareRow label="Reviews" apps={pinnedApps} render={a => fmt.num(a.reviews)} winner={winners.reviews} valueFn={a => a.reviews} />
              <CompareRow label="Price" apps={pinnedApps} render={a => a.free ? "Free" : "$" + a.price.toFixed(2)} />
              <CompareRow label="Title chars" apps={pinnedApps} render={a => `${a.title.length} / 30`} winner={winners.titleLen} valueFn={a => a.title.length} />
              {pinnedApps.some(a => a.subtitle) && (
                <CompareRow label="Subtitle chars" apps={pinnedApps} render={a => a.subtitle ? `${a.subtitle.length} / 30` : "—"} winner={winners.subLen} valueFn={a => (a.subtitle || "").length} />
              )}
              {hasScreenshots && (
                <CompareRow label="iPhone shots" apps={pinnedApps} render={a => {
                  const count = getIphoneShots(a).length || a.screenshots;
                  return count > 0 ? `${count} / 10` : "—";
                }} winner={winners.screenshots} valueFn={a => getIphoneShots(a).length || a.screenshots} />
              )}
              {pinnedApps.some(a => getIpadShots(a).length > 0 || a.ipadScreenshots > 0) && (
                <CompareRow label="iPad shots" apps={pinnedApps} render={a => {
                  const count = getIpadShots(a).length || a.ipadScreenshots;
                  return count > 0 ? `${count} / 10` : "—";
                }} />
              )}
              <CompareRow label="Languages" apps={pinnedApps} render={a => a.languages.length} winner={winners.languages} valueFn={a => a.languages.length} />
              {myApp && missingLocales.length > 0 && (
                <tr className="locale-gap-row">
                  <td className="col-row-label locale-gap-label">
                    <span className="locale-gap-trigger">
                      Locale gaps
                      <span className="locale-gap-count">{missingLocales.length}</span>
                      <span className="locale-gap-tooltip">Languages your competitors support that you don't yet. Localizing into these could help you rank in new markets.</span>
                    </span>
                  </td>
                  {pinnedApps.map(a => (
                    <td key={a.appId}>
                      {a.appId === myAppId ? (
                        <div className="locale-gap-chips">
                          {missingLocales.slice(0, 6).map(l => (
                            <span key={l} className="locale-gap-chip">{l}</span>
                          ))}
                          {missingLocales.length > 6 && (
                            <span className="locale-gap-chip locale-gap-more">+{missingLocales.length - 6}</span>
                          )}
                        </div>
                      ) : (
                        <span className="locale-gap-has">
                          {(a.languages || []).filter(l => missingLocales.includes(l)).length} of these
                        </span>
                      )}
                    </td>
                  ))}
                </tr>
              )}
              <CompareRow label="Genre" apps={pinnedApps} render={a => a.primaryGenre} />
              <CompareRow label="Version" apps={pinnedApps} render={a => `v${a.version}`} />
              <CompareRow label="Last update" apps={pinnedApps} render={a => fmt.daysAgo(a.updated)} winner={winners.updated} valueFn={a => new Date(a.updated).getTime()} />
              <CompareRow label="Size" apps={pinnedApps} render={a => fmt.bytes(a.size)} />
              <tr>
                <td className="col-row-label"></td>
                {pinnedApps.map(a => (
                  <td key={a.appId}>
                    <button
                      onClick={() => onUnpin(a.appId)}
                      style={{
                        background: "transparent", border: "1px solid var(--ui-rule)",
                        color: "var(--ui-ink-soft)", padding: "6px 10px", borderRadius: 6,
                        fontSize: 11, fontWeight: 600, cursor: "pointer", fontFamily: "var(--sans)"
                      }}
                    >Remove</button>
                  </td>
                ))}
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      {compareLightbox && (
        <CompareLightbox
          pinnedApps={pinnedApps}
          compareShots={compareShots}
          startIndex={compareLightboxIdx}
          onClose={() => setCompareLightbox(false)}
          myAppId={myAppId}
        />
      )}
    </div>
  );
}

/* ── Compare lightbox — syncs nth screenshot across all pinned apps ── */
function CompareLightbox({ pinnedApps, compareShots, startIndex, onClose, myAppId }) {
  const [idx, setIdx] = useStateV(startIndex);

  const allIphoneShots = useMemoV(() =>
    pinnedApps.map(a => (compareShots[a.id] || []).filter(s => s.variant === 'iphone')),
    [pinnedApps, compareShots]
  );

  const maxLen = useMemoV(() => Math.max(...allIphoneShots.map(s => s.length), 0), [allIphoneShots]);

  useEffectV(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') { e.stopPropagation(); onClose(); }
      else if (e.key === 'ArrowLeft' && idx > 0) setIdx(i => i - 1);
      else if (e.key === 'ArrowRight' && idx < maxLen - 1) setIdx(i => i + 1);
    };
    document.addEventListener('keydown', onKey, true);
    return () => document.removeEventListener('keydown', onKey, true);
  }, [idx, maxLen, onClose]);

  return ReactDOM.createPortal(
    <div className="lightbox compare-lightbox" onClick={onClose}>
      <div className="compare-lightbox-stage" onClick={e => e.stopPropagation()}>
        <div className="compare-lightbox-grid" style={{ gridTemplateColumns: `repeat(${pinnedApps.length}, 1fr)` }}>
          {pinnedApps.map((app, appIdx) => {
            const shot = allIphoneShots[appIdx][idx];
            return (
              <div key={app.appId} className={"compare-lightbox-col" + (app.appId === myAppId ? " is-mine" : "")}>
                <div className="compare-lightbox-label">
                  <AppIcon app={app} size={20} />
                  <span>{app.title}</span>
                </div>
                {shot ? (
                  <img className="compare-lightbox-img" src={shot.url} alt={`${app.title} #${idx + 1}`} />
                ) : (
                  <div className="compare-lightbox-empty">No screenshot #{idx + 1}</div>
                )}
              </div>
            );
          })}
        </div>
        <button className="lightbox-nav prev" disabled={idx === 0}
          onClick={() => setIdx(i => i - 1)}>‹</button>
        <button className="lightbox-nav next" disabled={idx >= maxLen - 1}
          onClick={() => setIdx(i => i + 1)}>›</button>
        <button className="lightbox-close" onClick={onClose}>×</button>
      </div>
      <div className="lightbox-meta" onClick={e => e.stopPropagation()}>
        Screenshot {idx + 1} of {maxLen}
      </div>
      <div className="lightbox-dots" onClick={e => e.stopPropagation()}>
        {Array.from({ length: maxLen }, (_, i) => (
          <button key={i} className={"lightbox-dot-num" + (i === idx ? " active" : "")}
            onClick={() => setIdx(i)}>{i + 1}</button>
        ))}
      </div>
    </div>,
    document.body
  );
}

function CompareRow({ label, apps, render, winner, valueFn }) {
  // Only show BEST if winner is strictly greater than at least one other app
  const showWinner = winner && valueFn && (() => {
    const winApp = apps.find(a => a.appId === winner);
    if (!winApp) return false;
    const winVal = valueFn(winApp);
    return apps.some(a => a.appId !== winner && valueFn(a) < winVal);
  })();
  return (
    <tr>
      <td className="col-row-label">{label}</td>
      {apps.map(a => (
        <td key={a.appId} className={showWinner && winner === a.appId ? "winner" : ""} style={{ paddingLeft: showWinner && winner === a.appId ? 22 : 16 }}>
          {render(a)}
          {showWinner && winner === a.appId && (
            <span style={{
              fontFamily: "var(--mono)", fontSize: 9, fontWeight: 700, color: "var(--accent)",
              marginLeft: 8, letterSpacing: 0.8
            }}>BEST</span>
          )}
        </td>
      ))}
    </tr>
  );
}

function maxBy(arr, fn) {
  let best = arr[0], bestV = fn(arr[0]);
  for (const a of arr.slice(1)) {
    const v = fn(a);
    if (v > bestV) { best = a; bestV = v; }
  }
  return best;
}

/* ── API Access Modal ───────────────────────────────────────── */
function ApiAccessModal({ onClose }) {
  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div className="pitch-modal" onClick={e => e.stopPropagation()}>
        <button className="ah-close" onClick={onClose}><Icon name="x" size={14} /></button>
        <div className="pitch-eyebrow">API + MCP Server</div>
        <h2 className="pitch-title">Build on our App Store data</h2>
        <div className="pitch-body">
          <div className="pitch-section">
            <div className="pitch-label">Who it's for</div>
            <ul className="pitch-list">
              <li>App agencies doing competitive research at scale</li>
              <li>Indie devs plugging Claude / Codex into keyword discovery</li>
              <li>Internal tools that need real-time App Store rankings</li>
            </ul>
          </div>
          <div className="pitch-section">
            <div className="pitch-label">What you get</div>
            <ul className="pitch-list">
              <li>REST API — search, suggest, app detail, screenshots</li>
              <li>MCP server for AI-native integrations</li>
              <li>Generous rate limits (adjustable)</li>
            </ul>
          </div>
          <div className="pitch-section">
            <div className="pitch-label">Pricing</div>
            <p className="pitch-text">Custom, based on volume. Starts at $29/mo.</p>
          </div>
        </div>
        {/* TODO: Replace with Typeform embed or custom form */}
        <a
          className="pitch-cta"
          href="mailto:api@ezscreenshots.com?subject=API%20Access%20Request&body=Hi%2C%20I%27m%20interested%20in%20API%20access.%0A%0AMy%20use%20case%3A%20%0AExpected%20volume%3A%20%0A"
        >
          Request access →
        </a>
      </div>
    </div>,
    document.body
  );
}

/* ── Sponsor Modal ─────────────────────────────────────────── */
function SponsorModal({ onClose }) {
  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div className="pitch-modal" onClick={e => e.stopPropagation()}>
        <button className="ah-close" onClick={onClose}><Icon name="x" size={14} /></button>
        <div className="pitch-eyebrow">Sponsorship</div>
        <h2 className="pitch-title">Get in front of indie developers</h2>
        <div className="pitch-body">
          <div className="pitch-section">
            <div className="pitch-label">The audience</div>
            <p className="pitch-text">
              Indie iOS/Android developers actively researching keywords and optimizing their App Store presence. High-intent, technical, decision-makers.
            </p>
          </div>
          <div className="pitch-section">
            <div className="pitch-label">Placement</div>
            <ul className="pitch-list">
              <li>"Sponsored by [You]" — visible on every search result page</li>
              <li>Your logo + one-line tagline + link</li>
              <li>Non-intrusive, no pop-ups, no interstitials</li>
            </ul>
          </div>
          <div className="pitch-section">
            <div className="pitch-label">Ideal sponsors</div>
            <ul className="pitch-list">
              <li>ASO consultancies & agencies</li>
              <li>App analytics / crash reporting tools</li>
              <li>Dev tools, hosting, CI/CD platforms</li>
            </ul>
          </div>
        </div>
        {/* TODO: Replace with Typeform or dedicated page */}
        <a
          className="pitch-cta"
          href="mailto:sponsors@ezscreenshots.com?subject=Sponsorship%20Inquiry&body=Hi%2C%20I%27m%20interested%20in%20sponsoring%20the%20ASO%20tool.%0A%0ACompany%3A%20%0AWebsite%3A%20%0A"
        >
          Get in touch →
        </a>
      </div>
    </div>,
    document.body
  );
}

// TODO: Re-enable GateModal when pricing/gating strategy is decided.
// Options: gate after N searches (email capture), gate by plan, or WTP survey.
// function GateModal({ onClose, onContinue }) { ... }

Object.assign(window, {
  KeywordDiscovery, KeywordRow, SavedKeywords, AppsList, AppRow,
  ResultsHeader, AuditDrawer, CompareTray, CompareModal, CompareLightbox,
  computeDifficulty, ScreenshotCarousel, Lightbox,
  ApiAccessModal, SponsorModal
});
