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

function App() {
  const [tab, setTab] = useState('research');

  // search rows (manual keyword / competitor search)
  const [rows, setRows] = useState([
    { id: 1, query: '', competitorId: null, count: 20,
      filters: { minLikes: '', minDailyLikes: '', orderBy: '', bsPlatform: '', bsMinViews: '', bsMinLikes: '', bsMinEngagement: '', bsSort: '', foreplayUrl: '', brandsearchUrl: '' },
      platforms: { foreplay: true, adspy: false, brandsearch: true, metaads: false } },
  ]);
  const [competitors, setCompetitors] = useState([]);
  const [directions, setDirections] = useState([]);
  const directionsRef = useRef([]);

  // pipeline stages
  const [researchAds, setResearchAds] = useState([]);
  const [filteredAds, setFilteredAds] = useState([]);
  const [remodeledAds, setRemodeledAds] = useState([]);
  const [autoRemodelAds, setAutoRemodelAds] = useState([]);
  const [remodelScripts, setRemodelScripts] = useState({}); // { [adId]: [{tag, body}] | 'pending' | 'error' }
  const remodelPollRef = useRef(null);
  const [convertedAds, setConvertedAds] = useState([]);
  const [autoresearchActive, setAutoresearchActive] = useState(false);  // true if last Research view should show winners-only badge

  const [selResearch, setSelResearch] = useState(new Set());
  const [selFilter, setSelFilter] = useState(new Set());
  const [selRemodel, setSelRemodel] = useState(new Set());

  // autoresearch controls
  const [arSelected, setArSelected] = useState(new Set());
  const [arTarget, setArTarget] = useState('1');
  const [arNumDirections, setArNumDirections] = useState('5'); // cap on directions when no explicit picks
  const [arPickedDirections, setArPickedDirections] = useState(new Set()); // empty = take first arNumDirections
  const [arRunning, setArRunning] = useState(false);
  const [arProgress, setArProgress] = useState(null); // { totalScored, winners, directionsSatisfied, directionsTotal, crawlState }
  const [arSources, setArSources] = useState({ foreplay: true, adspy: false, brandsearch: true, metaads: false });
  // Platform-level credit balances surfaced in the top bar. Foreplay is live;
  // the other three are visual placeholders until their backends land.
  const [platformCredits, setPlatformCredits] = useState({
    foreplay: null, adspy: null, brandsearch: null, metaads: null,
  });
  const arPollRef = useRef(null);

  // ---- Auto-Remodel state -----------------------------------------------
  // The orchestrator's directions picker is shared with manual autoresearch
  // — both pull from the same non-competitor pool. We keep it in App so the
  // panel + the autoresearch panel can stay independent components without
  // losing user picks when one re-renders.
  const [arOrchPickedDirections, setArOrchPickedDirections] = useState(new Set());
  const toggleArOrchPickedDirection = (id) =>
    setArOrchPickedDirections(s => {
      const n = new Set(s);
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
  const clearArOrchPickedDirections = () => setArOrchPickedDirections(new Set());

  const [arRunId, setArRunId] = useState(null);
  const [arStage, setArStage] = useState(null);
  const [arLastEvent, setArLastEvent] = useState(null);
  const [arPickedIds, setArPickedIds] = useState([]);
  // Refs that mirror the orchestrator state — the SSE callback below is
  // installed once via useEffect([]), so any state value referenced inside
  // its closure is captured at mount-time and goes stale. Refs let the
  // handler read the *current* value when an event arrives.
  const arRunIdRef = useRef(null);
  const arStageRef = useRef(null);
  const arPickedIdsRef = useRef([]);
  const arTargetAdsRef = useRef(0);
  const arAcceptedAdIdsRef = useRef(new Set());
  useEffect(() => { arRunIdRef.current = arRunId; }, [arRunId]);
  useEffect(() => { arStageRef.current = arStage; }, [arStage]);
  useEffect(() => { arPickedIdsRef.current = arPickedIds; }, [arPickedIds]);
  useEffect(() => { arTargetAdsRef.current = arTargetAds; }, [arTargetAds]);
  // Live progress counters — fed by SSE so the panel meta line + progress
  // bar tick exactly like the autoresearch panel does. ``arNVariants`` is
  // captured at Run time so we know the variants_total denominator.
  const [arDirectionsDone, setArDirectionsDone] = useState(0);
  const [arDirectionsTotal, setArDirectionsTotal] = useState(0);
  const [arAdsFound, setArAdsFound] = useState(0);
  const [arTargetAds, setArTargetAds] = useState(0);
  const [arVariantsDone, setArVariantsDone] = useState(0);
  const [arNVariants, setArNVariants] = useState(1);
  const [arVariantsTotalOverride, setArVariantsTotalOverride] = useState(null);
  const arRunningOrch = !!arRunId && arStage !== 'done' && arStage !== 'failed';

  // Platform toggles for the Auto-Remodel panel. Kept separate from
  // ``arSources`` (which the manual autoresearch panel owns) so the user
  // can configure each panel independently.
  const [arOrchSources, setArOrchSources] = useState({
    foreplay: true, adspy: false, brandsearch: true, metaads: false,
  });
  const toggleArOrchSource = (key) => setArOrchSources(s => ({ ...s, [key]: !s[key] }));
  const selectAllArOrchSources = () => {
    const LIVE = new Set(['foreplay', 'adspy', 'brandsearch']);
    setArOrchSources(s => {
      const next = { ...s };
      const anyLiveOff = [...LIVE].some(k => !s[k]);
      [...LIVE].forEach(k => { next[k] = anyLiveOff ? true : false; });
      return next;
    });
  };

  const toggleArPickedDirection = (id) =>
    setArPickedDirections(s => {
      const n = new Set(s);
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
  const clearArPickedDirections = () => setArPickedDirections(new Set());

  const toggleArSource = (key) => setArSources(s => ({ ...s, [key]: !s[key] }));
  const selectAllArSources = () => {
    const LIVE = new Set(['foreplay', 'adspy', 'brandsearch']);
    setArSources(s => {
      const next = { ...s };
      const anyLiveOff = [...LIVE].some(k => !s[k]);
      [...LIVE].forEach(k => { next[k] = anyLiveOff ? true : false; });
      return next;
    });
  };

  const [tweaks, setTweaks] = useState({ theme: 'light', density: 'comfortable', cardStyle: 'standard', accent: 'teal' });
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [toast, setToast] = useState(null);
  const [searching, setSearching] = useState(false);
  const [filterBusy, setFilterBusy] = useState(false);
  const [remodelBusy, setRemodelBusy] = useState(false);
  const [expandedAd, setExpandedAd] = useState(null);
  const AUTO_REMODEL_RESTORE_LIMIT_KEY = 'resilia.autoRemodel.lastTargetAds';

  // edit-mode host protocol
  useEffect(() => {
    const onMsg = (e) => {
      if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
      if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  useEffect(() => { document.documentElement.setAttribute('data-theme', tweaks.theme); }, [tweaks.theme]);
  useEffect(() => { document.documentElement.setAttribute('data-accent', tweaks.accent); }, [tweaks.accent]);
  useEffect(() => { directionsRef.current = directions; }, [directions]);

  // Load competitors + directions + any cached session on mount
  useEffect(() => {
    (async () => {
      let loadedDirections = [];
      try {
        const [comps, dirs] = await Promise.all([
          RESILIA_API.listCompetitors(),
          RESILIA_API.listDirections(),
        ]);
        loadedDirections = dirs || [];
        directionsRef.current = loadedDirections;
        setCompetitors(comps);
        setDirections(loadedDirections);
      } catch (e) {
        showToast('Failed to load directions: ' + e.message, 'Error');
      }
      // Prime the Foreplay credit badge in the top bar. Other platforms stay
      // null until their backends land and wire a matching summary field.
      try {
        const s = await RESILIA_API.summary();
        if (s && typeof s.credits_remaining === 'number') {
          setPlatformCredits(pc => ({ ...pc, foreplay: s.credits_remaining }));
        }
      } catch (e) {
        console.warn('initial summary fetch failed', e);
      }
      // Restore last search from Postgres cache
      try {
        const cached = await RESILIA_API.getCache('last_search_results');
        const cachedAds = cached && cached.payload && Array.isArray(cached.payload.ads) ? cached.payload.ads : [];
        if (cachedAds.length) {
          const ads = cachedAds.map(a => ({ ...RESILIA_ADAPT.adaptSearchAd(a, 'foreplay'), source: 'search' }));
          setResearchAds(prev => [...prev.filter(a => a.source === 'autoresearch'), ...ads]);
        }
      } catch (e) {
        console.warn('restore search cache failed', e);
      }
      // Restore last autoresearch winners
      try {
        const perDir = parseInt(arTarget, 10) || 1;
        const winners = await RESILIA_API.autoresearchWinners(200, perDir);
        if (Array.isArray(winners) && winners.length) {
          const dirMap = directionMetaMap(loadedDirections);
          const ads = [];
          const seen = new Set();
          for (const row of winners) {
            if (seen.has(row.ad_id)) continue;
            seen.add(row.ad_id);
            const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
            attachDirectionMeta(adapted, dirMap);
            ads.push({ ...adapted, source: 'autoresearch' });
          }
          setResearchAds(prev => [...prev.filter(a => a.source !== 'autoresearch'), ...ads]);
          setAutoresearchActive(true);
        }
      } catch (e) {
        console.warn('restore autoresearch cache failed', e);
      }
      try {
        const auto = await RESILIA_API.getAutoRemodelState();
        let restoredAutoRemodel = false;
        if (auto && auto.status === 'ok' && auto.state) {
          syncAutoRemodelState(auto.state);
          if (Array.isArray(auto.state.picked_ad_ids) && auto.state.picked_ad_ids.length) {
            const liveLimit = Math.max(
              1,
              Math.min(20, parseInt(auto.state.target_ads ?? auto.state.count ?? auto.state.picked_ad_ids.length, 10) || 5),
            );
            const pickedIds = auto.state.picked_ad_ids.slice(0, liveLimit);
            const ads = await hydratePickedAds(pickedIds, loadedDirections);
            if (ads.length) {
              setAutoRemodelAds(ads);
              setRemodelScripts(prev => {
                const next = { ...prev };
                ads.forEach(a => { if (!next[a.id]) next[a.id] = { status: 'pending', variants: [] }; });
                return next;
              });
              pollRemodelJobs(pickedIds);
              setTab('autoRemodel');
              restoredAutoRemodel = true;
            }
          }
        }
        if (!restoredAutoRemodel) {
          const jobs = await RESILIA_API.listRemodelJobs({});
          const autoJobs = Array.isArray(jobs)
            ? jobs.filter(j => j.source === 'auto_remodel' && j.ad_id)
            : [];
          const restoredIds = [];
          const seenAutoAds = new Set();
          const readyAutoJobs = autoJobs.filter(j => j.status === 'done' && j.response_text);
          const pendingAutoJobs = autoJobs.filter(j => !(j.status === 'done' && j.response_text));
          [
            ...readyAutoJobs.slice().sort((a, b) => Number(b.id || 0) - Number(a.id || 0)),
            ...pendingAutoJobs.slice().sort((a, b) => Number(b.id || 0) - Number(a.id || 0)),
          ]
            .forEach(j => {
              if (seenAutoAds.has(j.ad_id)) return;
              seenAutoAds.add(j.ad_id);
              restoredIds.push(j.ad_id);
            });
          if (restoredIds.length) {
            const restoreLimit = Math.max(
              1,
              Math.min(20, parseInt(localStorage.getItem(AUTO_REMODEL_RESTORE_LIMIT_KEY), 10) || 5),
            );
            const ids = restoredIds.slice(0, restoreLimit);
            let ads = await hydratePickedAds(ids, loadedDirections);
            const hydratedIds = new Set(ads.map(a => a.id));
            const fallbackAds = ids
              .filter(id => !hydratedIds.has(id))
              .map(id => {
                const j = autoJobs.find(job => job.ad_id === id) || {};
                return {
                  id,
                  brand: j.ad_id || id,
                  platform: 'foreplay',
                  copy: '',
                  score: null,
                  reasoning: null,
                  daysRunning: 0,
                  directionLabel: 'Auto-Remodel',
                  directionSearchLabel: 'Auto-Remodel',
                  transcript: [],
                  transcriptRaw: null,
                  source: 'auto_remodel',
                };
              });
            ads = [...ads, ...fallbackAds];
            setAutoRemodelAds(ads);
            setArPickedIds(ids);
            arPickedIdsRef.current = ids;
            setArStage('done');
            setArTargetAds(restoreLimit);
            const idSet = new Set(ids);
            const restoredJobsForIds = autoJobs.filter(j => idSet.has(j.ad_id));
            const restoredDone = restoredJobsForIds.filter(j => j.status === 'done' || j.status === 'failed').length;
            setArVariantsDone(restoredDone);
            setArVariantsTotalOverride(restoredJobsForIds.length || null);
            setArLastEvent({ stage: 'done', picked_ad_ids: ids, picked_count: ids.length, target_ads: restoreLimit });
            setRemodelScripts(prev => {
              const next = { ...prev };
              ads.forEach(a => { if (!next[a.id]) next[a.id] = { status: 'pending', variants: [] }; });
              return next;
            });
            pollRemodelJobs(ids);
            setTab('autoRemodel');
          }
        }
      } catch (e) {
        console.warn('restore auto-remodel state failed', e);
      }
    })();
    return () => {
      if (arPollRef.current) clearInterval(arPollRef.current);
      if (remodelPollRef.current) clearInterval(remodelPollRef.current);
    };
  }, []);

  const showToast = (msg, badge) => {
    setToast({ msg, badge });
    setTimeout(() => setToast(null), 3200);
  };

  const directionMetaMap = (items) => Object.fromEntries((items || []).map(d => {
    const keywords = Array.isArray(d.keywords) ? d.keywords.filter(Boolean) : [];
    const label = d.description || d.display_name || d.brand_name || d.id;
    const searchLabel = keywords.length
      ? keywords.slice(0, 4).join(', ')
      : (d.brand_name || d.display_name || label || d.id);
    return [d.id, { label, searchLabel }];
  }));

  const attachDirectionMeta = (ad, metaMap) => {
    const meta = metaMap[ad.directionId];
    ad.directionLabel = meta?.label || ad.directionId || null;
    ad.directionSearchLabel = meta?.searchLabel || ad.directionLabel || null;
    return ad;
  };

  const syncAutoRemodelState = (state) => {
    if (!state) return;
    setArRunId(state.run_id || null);
    setArStage(state.stage || null);
    setArLastEvent({ ...state, run_id: state.run_id, stage: state.stage });
    if (typeof state.directions_done === 'number') setArDirectionsDone(state.directions_done);
    if (typeof state.directions_total === 'number') setArDirectionsTotal(state.directions_total);
    if (typeof state.ads_found === 'number') setArAdsFound(state.ads_found);
    if (typeof state.target_ads === 'number') setArTargetAds(state.target_ads);
    if (typeof state.n_variants === 'number') setArNVariants(state.n_variants);
    if (Array.isArray(state.picked_ad_ids)) setArPickedIds(state.picked_ad_ids);
  };

  const searchFiltersPayload = (filters = {}) => {
    const out = {};
    const minLikes = parseInt(filters.minLikes, 10);
    const minDailyLikes = parseInt(filters.minDailyLikes, 10);
    const bsMinViews = parseInt(filters.bsMinViews, 10);
    const bsMinLikes = parseInt(filters.bsMinLikes, 10);
    const bsMinEngagement = parseFloat(filters.bsMinEngagement);
    if (Number.isFinite(minLikes) && minLikes > 0) out.adspy_min_likes = minLikes;
    if (Number.isFinite(minDailyLikes) && minDailyLikes > 0) out.adspy_min_daily_likes = minDailyLikes;
    if (filters.orderBy) out.adspy_order = filters.orderBy;
    if (filters.bsPlatform) out.brandsearch_platforms = [filters.bsPlatform];
    if (Number.isFinite(bsMinViews) && bsMinViews > 0) out.brandsearch_min_views = bsMinViews;
    if (Number.isFinite(bsMinLikes) && bsMinLikes > 0) out.brandsearch_min_likes = bsMinLikes;
    if (Number.isFinite(bsMinEngagement) && bsMinEngagement > 0) out.brandsearch_min_engagement_rate = bsMinEngagement;
    if (filters.bsSort) out.brandsearch_sort = filters.bsSort;
    return Object.keys(out).length ? out : null;
  };

  const searchSourceUrlsPayload = (filters = {}) => {
    const out = {};
    const foreplayUrl = (filters.foreplayUrl || '').trim();
    const brandsearchUrl = (filters.brandsearchUrl || '').trim();
    if (foreplayUrl) out.foreplay = foreplayUrl;
    if (brandsearchUrl) out.brandsearch = brandsearchUrl;
    return Object.keys(out).length ? out : null;
  };

  const expandAd = (ad) => {
    setExpandedAd(ad);
    if (!ad || ad.transcriptRaw || ad.platform !== 'brandsearch') return;
    RESILIA_API.fetchTranscript(ad.id).then(detail => {
      if (!detail || !detail.transcript) return;
      const patch = (item) => {
        if (item.id !== ad.id) return item;
        const adapted = RESILIA_ADAPT.adaptSearchAd({
          ...(item.raw || {}),
          ad_id: item.id,
          brand: item.brand,
          platform: item.publisherPlatform,
          source_platform: item.platform,
          score: item.score,
          reasoning: item.reasoning,
          running_days: item.daysRunning,
          creative_url: item.creativeUrl,
          thumbnail_url: item.thumbnailUrl,
          transcript: detail.transcript,
          raw: { ...(item.raw || {}), ...(detail.raw || {}) },
          metrics: item.raw?.metrics || detail.raw?.metrics || {},
        }, item.platform);
        adapted.directionId = item.directionId;
        adapted.directionLabel = item.directionLabel;
        adapted.directionSearchLabel = item.directionSearchLabel;
        return { ...item, ...adapted, source: item.source };
      };
      setResearchAds(prev => prev.map(patch));
      setFilteredAds(prev => prev.map(patch));
      setRemodeledAds(prev => prev.map(patch));
      setAutoRemodelAds(prev => prev.map(patch));
      setExpandedAd(prev => prev && prev.id === ad.id ? patch(prev) : prev);
    }).catch(e => console.warn('brandsearch transcript hydration failed', e));
  };

  // ---- Manual search ------------------------------------------------------
  const runSearch = async () => {
    setSearching(true);
    try {
      const results = [];
      for (const row of rows) {
        const sourceUrls = searchSourceUrlsPayload(row.filters);
        if (!row.query && !row.competitorId && !sourceUrls) continue;
        const platforms = Object.keys(row.platforms).filter(k => row.platforms[k]);
        const payload = {
          query: row.query,
          competitorId: row.competitorId,
          count: Math.min(row.count, 100),
          platforms,
          filters: searchFiltersPayload(row.filters),
          sourceUrls,
        };
        try {
          const resp = await RESILIA_API.search(payload);
          const ads = (resp.ads || []).map(a => ({ ...RESILIA_ADAPT.adaptSearchAd(a, 'foreplay'), source: 'search' }));
          results.push(...ads);
          if (resp && typeof resp.credits_remaining === 'number') {
            setPlatformCredits(pc => ({ ...pc, foreplay: resp.credits_remaining }));
          }
          if (resp && Array.isArray(resp.warnings) && resp.warnings.length) {
            const labels = resp.warnings.map(w => w.source).filter(Boolean).join(', ');
            showToast(`Search completed with ${labels || resp.warnings.length} warning(s).`, 'Warn');
          }
        } catch (e) {
          showToast(`Search failed for ${row.query || row.competitorId}: ${e.message}`, 'Error');
        }
      }
      // dedupe by ad id across rows
      const seen = new Set();
      const deduped = [];
      for (const a of results) {
        if (seen.has(a.id)) continue;
        seen.add(a.id);
        deduped.push(a);
      }
      // Keep existing autoresearch ads; only replace the search slice.
      setResearchAds(prev => [...prev.filter(a => a.source === 'autoresearch'), ...deduped]);
      if (deduped.length === 0) {
        showToast('No ads returned. Check your query or competitor.', 'Search');
      } else {
        showToast(`${deduped.length} ads fetched (scored).`, 'Search');
      }
    } finally {
      setSearching(false);
    }
  };

  // ---- Autoresearch -------------------------------------------------------
  const pollWinners = async () => {
    try {
      // Checkpoint-aware: only returns winners evaluated since the last
      // /api/autoresearch/checkpoint call, so previous runs never leak in.
      const perDir = parseInt(arTarget, 10) || 1;
      const rows = await RESILIA_API.autoresearchWinners(500, perDir);
      const dirMap = directionMetaMap(directions);
      const seen = new Set();
      const ads = [];
      for (const row of rows) {
        if (seen.has(row.ad_id)) continue;
        seen.add(row.ad_id);
        const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
        attachDirectionMeta(adapted, dirMap);
        ads.push({ ...adapted, source: 'autoresearch' });
      }
      // Replace only the autoresearch slice; search-source ads stay put.
      setResearchAds(prev => [...prev.filter(a => a.source !== 'autoresearch'), ...ads]);

      const summary = await RESILIA_API.summary();
      if (summary) {
        const runTotal = summary.run_directions_total || 0;
        setArProgress({
          totalScored: summary.total_scored,
          winners: ads.length,
          totalWinners: summary.winners,
          // Prefer the per-run counter while a crawl is active so the bar
          // tracks "directions finished this run", not lifetime DB state.
          directionsSatisfied: runTotal > 0 ? summary.run_directions_done : summary.directions_satisfied,
          directionsTotal: runTotal > 0 ? runTotal : summary.directions_total,
          crawlState: summary.crawl_state,
          creditsRemaining: summary.credits_remaining,
        });
        if (typeof summary.credits_remaining === 'number') {
          setPlatformCredits(pc => ({ ...pc, foreplay: summary.credits_remaining }));
        }
      }
      if (summary && summary.crawl_state === 'idle') {
        setArRunning(false);
        if (arPollRef.current) { clearInterval(arPollRef.current); arPollRef.current = null; }
        showToast(`Autoresearch complete. ${ads.length} winners.`, 'Autoresearch');
      }
    } catch (e) {
      console.warn('poll winners failed', e);
    }
  };

  const runAutoresearch = async () => {
    const randomPool = arSources.brandsearch
      ? directions
      : directions.filter(d => d.type !== 'competitor');
    if (randomPool.length === 0) {
      showToast('No directions configured for the selected sources.', 'Autoresearch');
      return;
    }
    // Direction selection: explicit checkbox picks win; otherwise randomly
    // sample arNumDirections (default 5) from the non-competitor pool.
    // Random instead of slice-from-top so repeat runs cover different ground.
    let ids;
    if (arPickedDirections.size > 0) {
      ids = directions.filter(d => arPickedDirections.has(d.id)).map(d => d.id);
    } else {
      const cap = Math.max(1, Math.min(randomPool.length, parseInt(arNumDirections, 10) || 5));
      const shuffled = randomPool.slice();
      for (let i = shuffled.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
      }
      ids = shuffled.slice(0, cap).map(d => d.id);
    }
    if (ids.length === 0) {
      showToast('No directions resolved — check the picker or the cap.', 'Autoresearch');
      return;
    }
    const sources = Object.keys(arSources).filter(k => arSources[k]);
    if (sources.length === 0) {
      showToast('Pick at least one source (Foreplay, AdSpy, …).', 'Autoresearch');
      return;
    }
    setArSelected(new Set(ids));
    setAutoresearchActive(true);
    setArProgress({ crawlState: 'starting', winners: 0, totalScored: 0, directionsSatisfied: 0, directionsTotal: ids.length * sources.length });
    // Drop previous autoresearch results from the UI so stale cards don't
    // linger before the first poll returns.
    setResearchAds(prev => prev.filter(a => a.source !== 'autoresearch'));
    try {
      // Mark a fresh checkpoint on the backend — winners from prior runs
      // won't show up in /api/autoresearch/winners anymore.
      await RESILIA_API.setAutoresearchCheckpoint();
      await RESILIA_API.startAutoresearch({
        directionIds: ids,
        perDirectionTarget: arTarget ? parseInt(arTarget, 10) : null,
        platforms: sources,
      });
      setArRunning(true);
      showToast(`Autoresearch started for ${ids.length} direction(s) across ${sources.length} source(s).`, 'Autoresearch');
      if (arPollRef.current) clearInterval(arPollRef.current);
      arPollRef.current = setInterval(pollWinners, 4000);
      pollWinners();
    } catch (e) {
      showToast('Autoresearch failed to start: ' + e.message, 'Error');
    }
  };

  const stopAutoresearch = async () => {
    try {
      await RESILIA_API.stopAutoresearch();
      setArRunning(false);
      if (arPollRef.current) { clearInterval(arPollRef.current); arPollRef.current = null; }
      showToast('Autoresearch stopping…', 'Autoresearch');
    } catch (e) {
      showToast('Failed to stop: ' + e.message, 'Error');
    }
  };

  // ---- Auto-Remodel orchestrator ----------------------------------------
  // Once the orchestrator picks ads, hydrate the swipe rows for them and
  // push them through the existing Research → Filter → Remodel views as the
  // pipeline transitions stages. This reuses the same panels the manual
  // flow uses (no duplicate UI to maintain).
  const hydratePickedAds = async (adIds, directionSource = directionsRef.current) => {
    if (!adIds || adIds.length === 0) return [];
    const dirMap = directionMetaMap(directionSource);
    const getSwipeWithRetry = async (id) => {
      for (let attempt = 0; attempt < 6; attempt += 1) {
        const swipe = await RESILIA_API.getSwipe(id);
        if (swipe) return swipe;
        await new Promise(resolve => setTimeout(resolve, 500));
      }
      return null;
    };
    const rows = await Promise.all(adIds.map(async (id) => {
      try {
        const swipe = await getSwipeWithRetry(id);
        if (!swipe) return null;
        let row = swipe;
        if (!row.transcript) {
          const detail = await RESILIA_API.fetchTranscript(id);
          if (detail && detail.transcript) {
            row = { ...row, transcript: detail.transcript, raw: { ...(row.raw || {}), ...(detail.raw || {}) } };
          }
        }
        // ``adaptEvaluatedAd`` expects evaluated_ads-shaped rows; swipe rows
        // have the same column names for everything we need (ad_id, brand,
        // platform, transcript, score, reasoning, running_days, raw, …).
        const adapted = RESILIA_ADAPT.adaptEvaluatedAd(row);
        attachDirectionMeta(adapted, dirMap);
        return { ...adapted, source: 'auto_remodel' };
      } catch (e) {
        console.warn('hydratePickedAds failed for', id, e);
        return null;
      }
    }));
    return rows.filter(Boolean);
  };

  useEffect(() => {
    const ids = arPickedIds || [];
    if (!ids.length || !['picked', 'filtering', 'filter_done', 'remodeling', 'done'].includes(arStage)) return;
    let cancelled = false;
    (async () => {
      const ads = await hydratePickedAds(ids);
      if (cancelled || ads.length === 0) return;
      setAutoRemodelAds(prev => {
        const seen = new Set(prev.map(a => a.id));
        const missing = ads.filter(a => !seen.has(a.id));
        return missing.length ? [...prev, ...missing] : prev;
      });
      setRemodelScripts(prev => {
        const next = { ...prev };
        ads.forEach(a => { if (!next[a.id]) next[a.id] = { status: 'pending', variants: [] }; });
        return next;
      });
      pollRemodelJobs(ids);
      setTab('autoRemodel');
    })();
    return () => { cancelled = true; };
    // arPickedIds is an array from SSE/state restore; use its contents as the trigger.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [arStage, arPickedIds.join('|')]);

  // SSE subscription. The same channel powers the existing autoresearch
  // panel's live progress; we layer Auto-Remodel on top with no new
  // transport. Returned cleanup closes the EventSource on unmount / HMR.
  useEffect(() => {
    const close = RESILIA_API.subscribe((ev) => {
      if (!ev || !ev.type) return;

      if (ev.type === 'auto_remodel_event') {
        // Late subscribers latch onto whichever run is broadcasting — keep
        // the first run_id we see for this session unless dismissed.
        setArRunId(prev => prev || ev.run_id);
        if (arRunIdRef.current && ev.run_id && ev.run_id !== arRunIdRef.current) return;
        setArStage(ev.stage || null);
        setArLastEvent(ev);

        // Crawl-side progress counters from the orchestrator.
        if (typeof ev.directions_done === 'number') setArDirectionsDone(ev.directions_done);
        if (typeof ev.directions_total === 'number') setArDirectionsTotal(ev.directions_total);
        if (typeof ev.ads_found === 'number') {
          const target = arTargetAdsRef.current || ev.target_ads || 0;
          setArAdsFound(target > 0 ? Math.min(ev.ads_found, target) : ev.ads_found);
        }
        if (typeof ev.target_ads === 'number') setArTargetAds(ev.target_ads);

        if (Array.isArray(ev.picked_ad_ids) && ev.picked_ad_ids.length) {
          const target = ev.target_ads || arTargetAdsRef.current || ev.picked_ad_ids.length;
          const cappedIds = ev.picked_ad_ids.slice(0, Math.max(1, Math.min(20, target)));
          arPickedIdsRef.current = cappedIds;
          setArPickedIds(cappedIds);
        }

        // Pipeline now skips the Filter step entirely (Sonnet pre-approves
        // every pick at score ≥ 7.5). Stages we react to:
        //   * picked (with non-empty picks)
        //                → hydrate ads, switch to Remodel tab, push to
        //                   remodeledAds with pending variant slots, start
        //                   polling.
        //   * picked (empty picks)
        //                → orchestrator's fresh-run filter found nothing
        //                   new. Stay on the current tab; toast explains.
        //   * remodeling → no-op (just a status update).
        //   * done/failed → final poll + toast.
        (async () => {
          if (ev.stage === 'picked' && Array.isArray(ev.picked_ad_ids)) {
            if (ev.picked_ad_ids.length === 0) {
              // Critical: never switch tabs on empty picks. The user clicked
              // Run expecting ads — switching to a blank Remodel view feels
              // like the app broke. Stay put; the orchestrator will fire
              // ``done`` next anyway.
              showToast(
                'Auto-Remodel: no fresh ads found this run. Try different directions or wait for new winners to surface.',
                'Auto-Remodel',
              );
              return;
            }
            const ads = await hydratePickedAds(ev.picked_ad_ids);
            setAutoRemodelAds(ads);
            setRemodelScripts(prev => {
              const next = { ...prev };
              ads.forEach(a => { next[a.id] = 'pending'; });
              return next;
            });
            if (remodelPollRef.current) clearInterval(remodelPollRef.current);
            const adIds = ads.map(a => a.id);
            remodelPollRef.current = setInterval(() => pollRemodelJobs(adIds), 3000);
            pollRemodelJobs(adIds);
            // Now that the picks for THIS run are known, jump to the
            // Remodel tab so the user lands on the variants as they stream
            // in. Doing it here (instead of on Run click) means the user
            // sees their starting tab during the crawl wait.
            setTab('autoRemodel');
            showToast(
              `Auto-Remodel: ${ads.length} ad${ads.length === 1 ? '' : 's'} picked — generating variants.`,
              'Auto-Remodel',
            );
          } else if (ev.stage === 'done') {
            // Suppress the redundant "done" toast when we already toasted
            // about an empty pick — same run, same news.
            if ((ev.picked ?? 0) > 0) {
              showToast(
                `Auto-Remodel done — ${ev.completed ?? 0}/${ev.jobs_created ?? 0} jobs ok`,
                ev.failed ? 'Warn' : 'Auto-Remodel',
              );
            }
            if (arPickedIdsRef.current.length) pollRemodelJobs(arPickedIdsRef.current);
          } else if (ev.stage === 'failed') {
            showToast(`Auto-Remodel failed: ${ev.error || 'unknown'}`, 'Error');
          }
        })();
      } else if (ev.type === 'remodel_completed' || ev.type === 'remodel_failed' || ev.type === 'remodel_edited') {
        // Variant-level updates: refresh just the affected ad's scripts via
        // the existing poll helper (handles the parsing + status logic).
        if (ev.ad_id && arPickedIdsRef.current.includes(ev.ad_id)) {
          pollRemodelJobs([ev.ad_id]);
          // Each remodel_completed/_failed represents one variant finishing
          // (in any state) — tick the panel's variant counter.
          if (ev.type === 'remodel_completed' || ev.type === 'remodel_failed') {
            setArVariantsDone(n => n + 1);
          }
        }
      } else if (ev.type === 'remodel_promoted') {
        if (ev.ad_id && arPickedIdsRef.current.includes(ev.ad_id)) {
          pollRemodelJobs([ev.ad_id]);
        }
        showToast(`Promoted to Video Queue (job ${ev.job_id})`, 'Promote');
      } else if (ev.type === 'evaluated_deleted') {
        showToast(`Bulk delete — ${ev.count} ad(s) removed`, 'Delete');
      } else if (ev.type === 'ad_scored' && ev.passes === true) {
        // The crawler fires ``ad_scored`` for every ad it evaluates — when
        // ``passes`` is true the ad just qualified. Stream qualifying ads
        // into the Research grid live so the user sees winners landing as
        // the orchestrator finds them, instead of waiting for the entire
        // crawl to finish before anything appears. Gated to active
        // Auto-Remodel runs so we don't conflict with the autoresearch
        // panel's own polling-based flow.
        const stageNow = arStageRef.current;
        const active = !!arRunIdRef.current && stageNow !== 'done' && stageNow !== 'failed';
        if (!active) return;
        const target = arTargetAdsRef.current || 5;
        if (arAcceptedAdIdsRef.current.has(ev.ad_id)) return;
        if (arAcceptedAdIdsRef.current.size >= target) return;
        arAcceptedAdIdsRef.current.add(ev.ad_id);
        setArAdsFound(Math.min(arAcceptedAdIdsRef.current.size, target));

        const dirMap = directionMetaMap(directionsRef.current);
        const synth = {
          ad_id: ev.ad_id,
          direction_id: ev.direction_id,
          score: ev.score,
          passes: true,
          reasoning: ev.reasoning,
          transcript: ev.transcript_preview,
          brand: ev.brand,
          platform: ev.platform,
          source_platform: ev.source_platform,
          running_days: ev.running_days,
          creative_url: ev.creative_url,
          thumbnail_url: ev.thumbnail_url,
          link_url: ev.link_url,
          metrics: ev.metrics || {},
          raw: { source_platform: ev.source_platform, metrics: ev.metrics || {} },
        };
        const adapted = RESILIA_ADAPT.adaptEvaluatedAd(synth);
        attachDirectionMeta(adapted, dirMap);
        const ad = { ...adapted, source: 'autoresearch' };
        RESILIA_API.fetchTranscript(ev.ad_id).then(detail => {
          if (!detail || !detail.transcript) return;
          const patchTranscript = (item) => {
            if (item.id !== ev.ad_id) return item;
            const patched = RESILIA_ADAPT.adaptEvaluatedAd({
              ...synth,
              transcript: detail.transcript,
              raw: { ...(synth.raw || {}), ...(detail.raw || {}) },
            });
            patched.directionLabel = item.directionLabel;
            patched.directionSearchLabel = item.directionSearchLabel;
            return { ...item, ...patched, source: item.source };
          };
          setResearchAds(prev => prev.map(patchTranscript));
        }).catch(e => console.warn('auto-remodel transcript hydration failed', e));
        // Push only to the Research grid. The Auto-Remodel panel shows
        // progress; rendering cards there as well duplicates the ads below.
        setResearchAds(prev => {
          if (prev.some(a => a.id === ad.id)) return prev;
          const current = prev.filter(a => a.source === 'autoresearch');
          if (current.length >= target) return prev;
          return [...prev, ad];
        });
        setAutoresearchActive(true);
      }
    });
    return close;
    // We intentionally don't include arRunId/arPickedIds in deps — re-creating
    // the SSE connection on every state change would thrash the server. The
    // closure reads the latest values via the setState callbacks.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const startAutoRemodel = async ({ mode, directionId, directionIds, count, nVariants, platforms }) => {
    try {
      const out = await RESILIA_API.runAutoRemodel({ mode, directionId, directionIds, count, nVariants, platforms });
      if (out.status === 'already_running') {
        showToast('Auto-Remodel is already running.', 'Auto-Remodel');
        return;
      }
      // Fresh run — clear stale state. Stay on whatever tab the user is on;
      // the SSE handler will switch to Remodel once the orchestrator emits
      // the ``picked`` event (i.e. after the crawl finishes and the picks
      // for THIS run are known). Stop the manual-flow remodel poller too,
      // otherwise it could overwrite the orchestrator's state.
      if (remodelPollRef.current) {
        clearInterval(remodelPollRef.current);
        remodelPollRef.current = null;
      }
      setArRunId(out.run_id);
      setArStage('queued');
      setArLastEvent(null);
      setArPickedIds([]);
      arPickedIdsRef.current = [];
      arAcceptedAdIdsRef.current = new Set();
      setArDirectionsDone(0);
      setArDirectionsTotal(0);
      setArAdsFound(0);
      setArTargetAds(Math.max(1, Math.min(20, parseInt(count, 10) || 5)));
      localStorage.setItem(
        AUTO_REMODEL_RESTORE_LIMIT_KEY,
        String(Math.max(1, Math.min(20, parseInt(count, 10) || 5))),
      );
      setArVariantsDone(0);
      setArNVariants(Math.max(1, parseInt(nVariants, 10) || 1));
      setArVariantsTotalOverride(null);
      setAutoRemodelAds([]);
      setRemodelScripts(prev => {
        const next = { ...prev };
        autoRemodelAds.forEach(ad => { delete next[ad.id]; });
        arPickedIds.forEach(id => { delete next[id]; });
        return next;
      });
      setTab('autoRemodel');
      // Clear the autoresearch slice so qualifying ads from a prior run
      // don't linger behind the new ones streaming in.
      setResearchAds(prev => prev.filter(a => a.source !== 'autoresearch'));
      const snapshot = await RESILIA_API.getAutoRemodelState(out.run_id);
      if (snapshot && snapshot.status === 'ok' && snapshot.state) {
        syncAutoRemodelState(snapshot.state);
      }
      showToast(`Auto-Remodel started (${out.run_id}) — staying here until picks land.`, 'Auto-Remodel');
    } catch (e) {
      showToast('Failed to start Auto-Remodel: ' + e.message, 'Error');
    }
  };

  const regenerateRemodelJob = async (jobId) => {
    try {
      const out = await RESILIA_API.retryRemodelJob(jobId);
      const adId = Object.keys(remodelScripts).find(id => {
        const e = remodelScripts[id];
        return e && Array.isArray(e.variants) && e.variants.some(v => v.jobId === jobId);
      });
      if (adId) {
        setRemodelScripts(prev => ({ ...prev, [adId]: { status: 'pending', variants: [] } }));
        if (remodelPollRef.current) clearInterval(remodelPollRef.current);
        remodelPollRef.current = setInterval(() => pollRemodelJobs([adId]), 3000);
        pollRemodelJobs([adId]);
      }
      showToast(`Regeneration started (${out.run_id || jobId}).`, 'Remodel');
    } catch (e) {
      showToast('Regenerate failed: ' + e.message, 'Error');
    }
  };

  const discardRemodelJob = async (jobId) => {
    if (!confirm('Discard this remodel variant?')) return;
    try {
      await RESILIA_API.deleteRemodelJob(jobId);
      setRemodelScripts(prev => {
        const next = { ...prev };
        for (const adId of Object.keys(next)) {
          const entry = next[adId];
          if (!entry || !Array.isArray(entry.variants)) continue;
          const variants = entry.variants.filter(v => v.jobId !== jobId);
          next[adId] = { ...entry, variants, status: variants.length ? entry.status : 'error' };
        }
        return next;
      });
      showToast('Variant discarded.', 'Remodel');
    } catch (e) {
      showToast('Discard failed: ' + e.message, 'Error');
    }
  };

  const promoteRemodelJob = async (jobId) => {
    try {
      const out = await RESILIA_API.promoteRemodelJob(jobId);
      const job = out.job || out;
      const ad = remodeledAds.find(a => a.id === job.ad_id) || autoRemodelAds.find(a => a.id === job.ad_id);
      if (ad) {
        setConvertedAds(prev => prev.some(a => a.id === ad.id) ? prev : [...prev, ad]);
      }
      if (job.ad_id) pollRemodelJobs([job.ad_id]);
      showToast(`Promoted to Video Queue (job ${job.id || jobId}).`, 'Promote');
      return job;
    } catch (e) {
      showToast('Promote failed: ' + e.message, 'Error');
      return null;
    }
  };

  const promoteAllAutoRemodelJobs = async () => {
    const jobIds = [];
    autoRemodelAds.forEach(ad => {
      const entry = remodelScripts[ad.id];
      if (!entry || !Array.isArray(entry.variants)) return;
      entry.variants.forEach(v => {
        if (v.jobId) jobIds.push(v.jobId);
      });
    });
    if (!jobIds.length) {
      showToast('No Auto-Remodel scripts are ready.', 'Promote');
      return;
    }
    try {
      const promoted = [];
      for (const jobId of jobIds) {
        const out = await RESILIA_API.promoteRemodelJob(jobId);
        const job = out.job || out;
        if (job) promoted.push(job);
      }
      const promotedAdIds = new Set(promoted.map(j => j.ad_id).filter(Boolean));
      setConvertedAds(prev => {
        const seen = new Set(prev.map(a => a.id));
        const next = [...prev];
        autoRemodelAds.forEach(ad => {
          if (promotedAdIds.has(ad.id) && !seen.has(ad.id)) next.push(ad);
        });
        return next;
      });
      if (promotedAdIds.size) pollRemodelJobs([...promotedAdIds]);
      showToast(`${promoted.length} Auto-Remodel script${promoted.length === 1 ? '' : 's'} promoted to Video Queue.`, 'Promote');
    } catch (e) {
      showToast('Promote all failed: ' + e.message, 'Error');
    }
  };

  // ---- Research-tab bulk delete -----------------------------------------
  const bulkDeleteResearch = async () => {
    const ids = [...selResearch];
    if (!ids.length) return;
    if (!confirm(`Delete ${ids.length} evaluated ad row(s)?\n\nQualified winners and swipe file are NOT touched.`)) return;
    try {
      const out = await RESILIA_API.bulkDeleteEvaluated(ids);
      setResearchAds(prev => prev.filter(a => !selResearch.has(a.id)));
      setSelResearch(new Set());
      showToast(`${out.removed} ad(s) removed.`, 'Delete');
    } catch (e) {
      showToast('Bulk delete failed: ' + e.message, 'Error');
    }
  };

  // ---- Pipeline transitions ----------------------------------------------
  const toggleSel = (setter) => (id) => {
    setter(prev => {
      const n = new Set(prev);
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
  };

  const ensureAdsInSwipe = async (picked) => {
    // Filter/Remodel backends read transcripts from swipe_file, so ads
    // coming straight from search need to be persisted first. Autoresearch
    // ads are already in swipe_file via the promote-winners flow.
    for (const ad of picked) {
      try {
        await RESILIA_API.addToSwipe({
          adId: ad.id,
          raw: ad.raw || {},
          directionId: ad.directionId || null,
        });
      } catch (e) {
        console.warn('addToSwipe failed for', ad.id, e);
      }
    }
  };

  const passToFilter = async () => {
    const picked = researchAds.filter(a => selResearch.has(a.id));
    if (picked.length === 0) return;
    setFilterBusy(true);
    try {
      await ensureAdsInSwipe(picked);
      await RESILIA_API.runFilter({ adIds: picked.map(a => a.id) });
      // Optimistically seed the Filter tab with the picked ads; real
      // verdicts arrive async — UI can refresh via SSE in a follow-up.
      setFilteredAds(picked);
      setSelResearch(new Set());
      setTab('filter');
      showToast(`Filter started on ${picked.length} ads.`, 'Filter');
    } catch (e) {
      showToast('Filter failed: ' + e.message, 'Error');
    } finally {
      setFilterBusy(false);
    }
  };

  const passWithoutFilter = async () => {
    const picked = researchAds.filter(a => selResearch.has(a.id));
    if (picked.length === 0) return;
    try {
      await ensureAdsInSwipe(picked);
      await startRemodelAndPoll(picked);
      setRemodeledAds(picked);
      setSelResearch(new Set());
      setTab('remodel');
      showToast(`${picked.length} ads passed without filtering. Remodeling…`, 'Bypass');
    } catch (e) {
      showToast('Pass failed: ' + e.message, 'Error');
    }
  };

  // Extract just the REMODELED SCRIPT prose from the remodeler output — skip
  // the analysis header and any surrounding metadata. Each paragraph becomes
  // one untagged line so the UI renders it as a readable transcript rather
  // than a HOOK/PROBLEM/SOLUTION breakdown.
  const parseRemodelScript = (text) => {
    if (!text) return [];
    const scriptMatch = text.match(/###\s*REMODELED\s+SCRIPT\s*\n([\s\S]*?)(?=\n###\s|$)/i);
    const body = scriptMatch ? scriptMatch[1].trim() : text.trim();
    const paras = body.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean);
    return paras.map(p => ({
      tag: '',
      body: p.replace(/^\*\*[^*]+\*\*\s*/, '').replace(/\n+/g, ' '),
    }));
  };

  // ``editRemodelScript`` now takes a job id directly (RemodelView passes
  // the jobId of the specific variant being edited). Each ad can have
  // multiple variants in flight, so an adId-only lookup would be ambiguous.
  const editRemodelScript = async (jobId, newText) => {
    if (!jobId) {
      showToast('No remodel job to edit yet — wait for it to finish.', 'Remodel');
      return;
    }
    try {
      await RESILIA_API.updateRemodelScript({ jobId, responseText: newText });
      // Refresh by re-polling the affected ad — keeps the variants array in
      // sync with the DB without a bespoke patch path.
      const adId = Object.keys(remodelScripts).find(id => {
        const e = remodelScripts[id];
        return e && Array.isArray(e.variants) && e.variants.some(v => v.jobId === jobId);
      });
      if (adId) pollRemodelJobs([adId]);
      showToast('Edit saved.', 'Remodel');
    } catch (e) {
      showToast('Save failed: ' + e.message, 'Error');
      throw e;
    }
  };

  // Each ad can spawn N variants (one Sonnet call per variant). We track
  // them as ``{ status, variants: [{ jobId, variantIdx, raw, lines }] }``
  // so the Remodel view can render every finished variant as its own
  // editable card. Polling stops only when every variant for every ad has
  // reached done/failed.
  const pollRemodelJobs = async (adIds) => {
    try {
      const perAd = await Promise.all(
        adIds.map(id => RESILIA_API.listRemodelJobs({ adId: id })),
      );
      setRemodelScripts(prev => {
        const next = { ...prev };
        let stillPending = 0;
        adIds.forEach((id, i) => {
          const jobs = perAd[i] || [];
          const done = jobs.filter(j => j.status === 'done' && j.response_text);
          const failed = jobs.filter(j => j.status === 'failed');
          const inFlight = jobs.length - done.length - failed.length;
          if (done.length > 0) {
            next[id] = {
              status: 'ready',
              variants: done
                .slice()
                .sort((a, b) => (a.variant_idx || 0) - (b.variant_idx || 0))
                .map(j => ({
                  jobId: j.id,
                  variantIdx: j.variant_idx || 0,
                  raw: j.response_text,
                  lines: parseRemodelScript(j.response_text),
                  promotedAt: j.promoted_at || null,
                })),
            };
            if (inFlight > 0) stillPending++;
          } else if (failed.length === jobs.length && jobs.length > 0) {
            next[id] = { status: 'error', variants: [] };
          } else {
            next[id] = { status: 'pending', variants: [] };
            stillPending++;
          }
        });
        if (stillPending === 0 && remodelPollRef.current) {
          clearInterval(remodelPollRef.current);
          remodelPollRef.current = null;
        }
        return next;
      });
    } catch (e) {
      console.warn('poll remodel jobs failed', e);
    }
  };

  const startRemodelAndPoll = async (picked) => {
    const adIds = picked.map(a => a.id);
    setRemodelScripts(prev => {
      const next = { ...prev };
      adIds.forEach(id => { next[id] = 'pending'; });
      return next;
    });
    await RESILIA_API.runRemodel({ adIds, nVariants: 1 });
    if (remodelPollRef.current) clearInterval(remodelPollRef.current);
    remodelPollRef.current = setInterval(() => pollRemodelJobs(adIds), 3000);
    pollRemodelJobs(adIds);
  };

  const runRemodel = async () => {
    const picked = filteredAds.filter(a => selFilter.has(a.id));
    if (picked.length === 0) return;
    setRemodelBusy(true);
    try {
      await ensureAdsInSwipe(picked);
      await startRemodelAndPoll(picked);
      setRemodeledAds(picked);
      setSelFilter(new Set());
      setTab('remodel');
      showToast(`Remodel started on ${picked.length} ads (1 variant each).`, 'Remodel');
    } catch (e) {
      showToast('Remodel failed: ' + e.message, 'Error');
    } finally {
      setRemodelBusy(false);
    }
  };

  const convert = () => {
    const picked = remodeledAds.filter(a => selRemodel.has(a.id));
    setConvertedAds(picked);
    setSelRemodel(new Set());
    setTab('video');
    showToast(`${picked.length} ads queued for video conversion.`, 'Video');
  };

  const counts = {
    research: researchAds.length,
    autoRemodel: autoRemodelAds.length || arPickedIds.length,
    filter: filteredAds.length,
    remodel: remodeledAds.length,
    video: convertedAds.length,
  };

  // group by platform
  const groupByPlatform = (ads) => {
    const g = { foreplay: [], adspy: [], brandsearch: [], metaads: [] };
    ads.forEach(a => g[a.platform]?.push(a));
    return g;
  };

  return (
    <div className="app">
      <TopBar credits={platformCredits} />
      <TabBar active={tab} counts={counts} onChange={setTab} />
      <main className="content">
        {tab === 'research' && (
          <ResearchView
            rows={rows} setRows={setRows}
            competitors={competitors}
            ads={researchAds} searching={searching}
            sel={selResearch} onToggle={toggleSel(setSelResearch)}
            onSearch={runSearch}
            onExpandAd={expandAd}
            density={tweaks.density}
            minimal={tweaks.cardStyle === 'minimal'}
            groupByPlatform={groupByPlatform}
            arRunning={arRunning}
            arProgress={arProgress}
            arSources={arSources}
            onToggleArSource={toggleArSource}
            onSelectAllArSources={selectAllArSources}
            arTarget={arTarget}
            setArTarget={setArTarget}
            arNumDirections={arNumDirections}
            setArNumDirections={setArNumDirections}
            directions={directions.filter(d => d.type !== 'competitor')}
            arPickedDirections={arPickedDirections}
            onToggleArPickedDirection={toggleArPickedDirection}
            onClearArPickedDirections={clearArPickedDirections}
            onRunAutoresearch={runAutoresearch}
            onStopAutoresearch={stopAutoresearch}
          />
        )}
        {tab === 'filter' && (
          <PipelineView
            title="Filter" subtitle="Ads running through the filtering layer. Select and pass to Remodel."
            ads={filteredAds} sel={selFilter} onToggle={toggleSel(setSelFilter)}
            onExpandAd={expandAd}
            density={tweaks.density} minimal={tweaks.cardStyle === 'minimal'}
            groupByPlatform={groupByPlatform}
            emptyTitle="Nothing filtered yet"
            emptyBody="Select ads in Research and press Pass to Filter."
          />
        )}
        {tab === 'autoRemodel' && (
          <AutoRemodelWorkspace
            ads={autoRemodelAds}
            scripts={remodelScripts}
            directionOptions={directions}
            running={arRunningOrch}
            stage={arStage}
            lastEvent={arLastEvent}
            adsFound={arAdsFound}
            targetAds={arTargetAds}
            variantsDone={arVariantsDone}
            variantsTotal={arVariantsTotalOverride ?? arPickedIds.length * arNVariants}
            sources={arOrchSources}
            onToggleSource={toggleArOrchSource}
            onSelectAllSources={selectAllArOrchSources}
            onRun={startAutoRemodel}
            onEditScript={editRemodelScript}
            onRegenerate={regenerateRemodelJob}
            onDiscard={discardRemodelJob}
            onPromote={promoteRemodelJob}
            onPromoteAll={promoteAllAutoRemodelJobs}
          />
        )}
        {tab === 'remodel' && (
          <RemodelView
            ads={remodeledAds} sel={selRemodel} onToggle={toggleSel(setSelRemodel)}
            scripts={remodelScripts}
            onEditScript={editRemodelScript}
            onRegenerate={regenerateRemodelJob}
            onDiscard={discardRemodelJob}
            onPromote={promoteRemodelJob}
          />
        )}
        {tab === 'video' && (
          <VideoView ads={convertedAds} />
        )}
      </main>

      {tab === 'research' && (
        <SelectionBar
          count={selResearch.size} total={researchAds.length}
          ctaLabel={filterBusy ? 'Filtering…' : 'Pass to Filter'} icon="filter"
          onCTA={passToFilter}
          secondaryLabel="Pass without Filtering"
          secondaryIcon="arrowRight"
          onSecondary={passWithoutFilter}
          dangerLabel="Delete selected"
          dangerIcon="x"
          onDanger={bulkDeleteResearch}
          onClear={() => setSelResearch(new Set())}
          onSelectAll={() => setSelResearch(new Set(researchAds.map(a => a.id)))}
        />
      )}
      {tab === 'filter' && (
        <SelectionBar
          count={selFilter.size} total={filteredAds.length}
          ctaLabel={remodelBusy ? 'Running…' : 'Run Remodel'} icon="scroll"
          onCTA={runRemodel}
          onClear={() => setSelFilter(new Set())}
          onSelectAll={() => setSelFilter(new Set(filteredAds.map(a => a.id)))}
        />
      )}
      {tab === 'remodel' && (
        <SelectionBar
          count={selRemodel.size} total={remodeledAds.length}
          ctaLabel="Convert to Video" icon="film"
          onCTA={convert}
          onClear={() => setSelRemodel(new Set())}
          onSelectAll={() => setSelRemodel(new Set(remodeledAds.map(a => a.id)))}
        />
      )}

      <AdDrawer ad={expandedAd} onClose={() => setExpandedAd(null)} />

      {toast && (
        <div className="toast">
          <span className="toast-badge">{toast.badge}</span>
          <span>{toast.msg}</span>
        </div>
      )}

      {!tweaksOpen && (
        <button className="tweaks-fab" onClick={() => setTweaksOpen(true)} title="Tweaks">
          <Icon name="sliders" size={18} />
        </button>
      )}
      {tweaksOpen && (
        <Tweaks
          state={tweaks}
          setState={(next) => {
            setTweaks(next);
            window.parent.postMessage({ type: '__edit_mode_set_keys', edits: next }, '*');
          }}
          onClose={() => setTweaksOpen(false)}
        />
      )}
    </div>
  );
}

function ResearchView({
  rows, setRows, competitors,
  ads, searching, sel, onToggle, onSearch, onExpandAd, density, minimal, groupByPlatform,
  arRunning, arProgress, arSources, onToggleArSource, onSelectAllArSources,
  arTarget, setArTarget, onRunAutoresearch, onStopAutoresearch,
  arNumDirections, setArNumDirections,
  directions, arPickedDirections, onToggleArPickedDirection, onClearArPickedDirections,
}) {
  const updateRow = (id, patch) => setRows(rows.map(r => r.id === id ? patch : r));
  const removeRow = (id) => setRows(rows.filter(r => r.id !== id));
  const addRow = () => setRows([...rows, {
    id: Date.now(), query: '', competitorId: null, count: 20,
    filters: { minLikes: '', minDailyLikes: '', orderBy: '', bsPlatform: '', bsMinViews: '', bsMinLikes: '', bsMinEngagement: '', bsSort: '', foreplayUrl: '', brandsearchUrl: '' },
    platforms: { foreplay: true, adspy: false, brandsearch: true, metaads: false },
  }]);

  const searchAds = ads.filter(a => a.source !== 'autoresearch');
  const autoAds = ads.filter(a => a.source === 'autoresearch');
  const searchGrouped = groupByPlatform(searchAds);
  const autoGrouped = groupByPlatform(autoAds);
  const searchPlatforms = Object.keys(searchGrouped).filter(p => searchGrouped[p].length > 0);
  const autoPlatforms = Object.keys(autoGrouped).filter(p => autoGrouped[p].length > 0);

  return (
    <>
      <div className="search-card">
        <div className="sc-hdr">
          <span className="sc-title">Search</span>
          <span className="sc-sub">Multi-row keyword or competitor-scoped search · Foreplay + AdSpy + BrandSearch live</span>
          <div className="sc-actions">
            <button className="btn-primary" onClick={onSearch} disabled={searching}>
              {searching ? <><Icon name="loader" size={14} />Searching…</> : <><Icon name="search" size={14} />Search</>}
            </button>
          </div>
        </div>
        <div className="search-rows">
          {rows.map(r => (
            <SearchRow
              key={r.id}
              row={r}
              competitors={competitors}
              onChange={(patch) => updateRow(r.id, patch)}
              onRemove={() => removeRow(r.id)}
              canRemove={rows.length > 1}
            />
          ))}
        </div>
        <button className="sr-add" onClick={addRow}>
          <Icon name="plus" size={14} /> Add row
        </button>
      </div>

      {searchPlatforms.length > 0 && searchPlatforms.map(p => (
        <PlatformPanel
          key={`s-${p}`}
          platform={p}
          ads={searchGrouped[p]}
          selected={sel}
          onToggleAd={onToggle}
          onExpandAd={onExpandAd}
          minimal={minimal}
          density={density}
          showWinnersBadge={false}
        />
      ))}

      <AutoresearchPanel
        sources={arSources}
        onToggleSource={onToggleArSource}
        onSelectAllSources={onSelectAllArSources}
        perDirectionTarget={arTarget}
        onPerDirectionTarget={setArTarget}
        numDirections={arNumDirections}
        onNumDirections={setArNumDirections}
        directions={directions}
        pickedDirections={arPickedDirections}
        onTogglePickedDirection={onToggleArPickedDirection}
        onClearPickedDirections={onClearArPickedDirections}
        onRun={onRunAutoresearch}
        onStop={onStopAutoresearch}
        running={arRunning}
        progress={arProgress}
        disabled={searching}
      />

      {autoPlatforms.length > 0 && autoPlatforms.map(p => (
        <PlatformPanel
          key={`a-${p}`}
          platform={p}
          ads={autoGrouped[p]}
          selected={sel}
          onToggleAd={onToggle}
          onExpandAd={onExpandAd}
          minimal={minimal}
          density={density}
          showWinnersBadge={true}
        />
      ))}

      {searchPlatforms.length === 0 && autoPlatforms.length === 0 && (
        <div className="empty">
          <span className="empty-display">No results yet.</span>
          <p className="empty-body">Run a search above or pick autoresearch directions and press Run Autoresearch.</p>
        </div>
      )}
    </>
  );
}

function PipelineView({ title, subtitle, ads, sel, onToggle, onExpandAd, density, minimal, groupByPlatform, emptyTitle, emptyBody }) {
  const grouped = groupByPlatform(ads);
  const activePlatforms = Object.keys(grouped).filter(p => grouped[p].length > 0);
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">{title}</span>
        <span className="content-sub">{subtitle}</span>
      </div>
      {activePlatforms.length === 0 ? (
        <div className="empty">
          <span className="empty-display">{emptyTitle}</span>
          <p className="empty-body">{emptyBody}</p>
        </div>
      ) : (
        activePlatforms.map(p => (
          <PlatformPanel
            key={p}
            platform={p}
            ads={grouped[p]}
            selected={sel}
            onToggleAd={onToggle}
            onExpandAd={onExpandAd}
            minimal={minimal}
            density={density}
          />
        ))
      )}
    </>
  );
}

function AutoRemodelWorkspace({
  ads,
  scripts,
  directionOptions,
  running,
  stage,
  lastEvent,
  adsFound,
  targetAds,
  variantsDone,
  variantsTotal,
  sources,
  onToggleSource,
  onSelectAllSources,
  onRun,
  onEditScript,
  onRegenerate,
  onDiscard,
  onPromote,
  onPromoteAll,
}) {
  const readyJobs = ads.flatMap(ad => {
    const entry = scripts[ad.id];
    if (!entry || !Array.isArray(entry.variants)) return [];
    return entry.variants.filter(v => v.jobId).map(v => v.jobId);
  });
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">Auto-Remodel</span>
        <span className="content-sub">Autonomous search, scoring, filter audit, and script generation.</span>
      </div>
      <AutoRemodelPanel
        open={true}
        directionOptions={directionOptions || []}
        running={running}
        stage={stage}
        lastEvent={lastEvent}
        adsFound={adsFound}
        targetAds={targetAds}
        variantsDone={variantsDone}
        variantsTotal={variantsTotal}
        sources={sources}
        onToggleSource={onToggleSource}
        onSelectAllSources={onSelectAllSources}
        onRun={onRun}
      />
      <AutoRemodelResultsView
        ads={ads}
        scripts={scripts}
        readyJobs={readyJobs}
        running={running}
        stage={stage}
        onEditScript={onEditScript}
        onRegenerate={onRegenerate}
        onDiscard={onDiscard}
        onPromote={onPromote}
        onPromoteAll={onPromoteAll}
      />
    </>
  );
}

function AutoRemodelResultsView({
  ads,
  scripts,
  readyJobs,
  running,
  stage,
  onEditScript,
  onRegenerate,
  onDiscard,
  onPromote,
  onPromoteAll,
}) {
  const canPromoteAll = readyJobs.length > 0;
  const sortedAds = ads.slice().sort((a, b) => {
    const entryA = scripts[a.id];
    const entryB = scripts[b.id];
    const readyA = entryA && Array.isArray(entryA.variants) && entryA.variants.length > 0;
    const readyB = entryB && Array.isArray(entryB.variants) && entryB.variants.length > 0;
    if (readyA !== readyB) return readyA ? -1 : 1;
    const hasTranscriptA = !!(a.transcriptRaw || (Array.isArray(a.transcript) && a.transcript.length));
    const hasTranscriptB = !!(b.transcriptRaw || (Array.isArray(b.transcript) && b.transcript.length));
    if (hasTranscriptA !== hasTranscriptB) return hasTranscriptA ? -1 : 1;
    return 0;
  });
  return (
    <div className="platform-panel">
      <header className="pp-hdr">
        <span className="pp-chip" style={{ background: '#e6fbf7', color: '#00796b' }}>Auto-Remodel Results</span>
        <span className="pp-title">{ads.length} ad{ads.length === 1 ? '' : 's'}</span>
        <div className="pp-spacer" />
        <button className="btn-primary" onClick={onPromoteAll} disabled={!canPromoteAll} title="Promote every ready Auto-Remodel script">
          <Icon name="film" size={14} /> Promote All to Video Queue
        </button>
      </header>
      {ads.length === 0 ? (
        <div className="empty" style={{ margin: 0 }}>
          <span className="empty-display">No Auto-Remodel results yet</span>
          <p className="empty-body">
            {running ? 'The run is working. Results will appear here as soon as top ads are picked.' : stage === 'failed' ? 'The last run failed.' : 'Configure Auto-Remodel above and run it.'}
          </p>
        </div>
      ) : (
        <div style={{ display: 'grid', gap: 14, padding: 14 }}>
          {sortedAds.map(ad => (
            <AutoRemodelResultCard
              key={ad.id}
              ad={ad}
              entry={scripts[ad.id]}
              onEditScript={onEditScript}
              onRegenerate={onRegenerate}
              onDiscard={onDiscard}
              onPromote={onPromote}
            />
          ))}
        </div>
      )}
    </div>
  );
}

function AutoRemodelResultCard({ ad, entry, onEditScript, onRegenerate, onDiscard, onPromote }) {
  const variants = entry && Array.isArray(entry.variants) ? entry.variants : [];
  const status = entry && entry.status ? entry.status : (entry === 'error' ? 'error' : 'pending');
  const transcriptText = ad.transcriptRaw
    || (Array.isArray(ad.transcript)
      ? ad.transcript.map(seg => seg.body || seg.t || '').filter(Boolean).join('\n')
      : (ad.transcript || ''));
  const directionWords = ad.directionSearchLabel || ad.directionLabel;
  const statusLabel = variants.length > 0
    ? `${variants.length} remodeled script${variants.length === 1 ? '' : 's'}`
    : status === 'error'
      ? 'Generation failed'
      : 'Generating scripts';
  return (
    <article className="transcript ar-result-card">
      <header className="t-hdr ar-result-hdr">
        <span className="t-brand">{ad.brand || ad.id}</span>
        <span className="t-meta">{ad.id}</span>
        <div className="t-hdr-spacer" />
        <span className={`ar-result-status ${status === 'error' ? 'error' : variants.length > 0 ? 'ready' : 'pending'}`}>{statusLabel}</span>
        {typeof ad.score === 'number' && <span className="ad-score high">{ad.score.toFixed(1)}</span>}
      </header>
      <div className="ar-result-meta">
        {ad.daysRunning ? <span>{ad.daysRunning}d running</span> : null}
        {directionWords ? (
          <span className="ad-direction" title={`Search direction: ${directionWords}`}>
            <Icon name="target" size={10} />
            <span>{directionWords}</span>
          </span>
        ) : null}
        {ad.platform ? <span>{ad.platform}</span> : null}
      </div>
      <AutoRemodelMediaPreview ad={ad} transcriptText={transcriptText} />
      {(transcriptText || ad.reasoning) && (
        <details style={{ margin: '10px 14px 0', border: '1px solid var(--border-subtle)', borderRadius: 8, padding: '8px 10px', background: 'var(--bg-sunken)' }}>
          <summary style={{ cursor: 'pointer', fontSize: 12, color: 'var(--fg-2)' }}>Original transcript, Sonnet score, and reasoning</summary>
          {typeof ad.score === 'number' && <p style={{ margin: '8px 0', fontSize: 12 }}>Sonnet score: {ad.score.toFixed(1)}</p>}
          {ad.reasoning && <p style={{ margin: '8px 0', fontSize: 12, color: 'var(--fg-2)' }}>{ad.reasoning}</p>}
          {transcriptText && <pre style={{ whiteSpace: 'pre-wrap', maxHeight: 220, overflow: 'auto', fontSize: 12, lineHeight: 1.45 }}>{transcriptText}</pre>}
        </details>
      )}
      <div style={{ padding: 14, display: 'grid', gap: 10 }}>
        {status === 'pending' && variants.length === 0 && (
          <div className="empty" style={{ margin: 0, padding: 18 }}>
            <span className="empty-display">Generating scripts</span>
            <p className="empty-body">Claude Sonnet is still working on this ad.</p>
          </div>
        )}
        {status === 'error' && variants.length === 0 && (
          <div className="empty" style={{ margin: 0, padding: 18 }}>
            <span className="empty-display">Generation failed</span>
            <p className="empty-body">Regenerate from the job action when a failed variant is available.</p>
          </div>
        )}
        {variants.map(v => (
          <AutoRemodelScriptEditor
            key={`${ad.id}-${v.variantIdx}-${v.jobId}`}
            variant={v}
            onSave={(text) => onEditScript(v.jobId, text)}
            onRegenerate={() => onRegenerate(v.jobId)}
            onDiscard={() => onDiscard(v.jobId)}
            onPromote={() => onPromote(v.jobId)}
          />
        ))}
      </div>
    </article>
  );
}

function AutoRemodelMediaPreview({ ad, transcriptText }) {
  const [playing, setPlaying] = React.useState(false);
  const p = PLATFORMS[ad.platform] || PLATFORMS.foreplay;
  const metricChips = RESILIA_ADAPT.metricChips(ad);
  const hasVideo = !!ad.creativeUrl;
  const hasThumbnail = !!ad.thumbnailUrl;
  const showVideoPreview = hasVideo && !hasThumbnail && !playing;
  const scoreText = typeof ad.score === 'number'
    ? (ad.score > 10 ? Math.round(ad.score) : ad.score.toFixed(1))
    : null;
  const sourceCopy = ad.copy || (transcriptText ? transcriptText.slice(0, 220) : '');
  const directionWords = ad.directionSearchLabel || ad.directionLabel;
  return (
    <div className="ar-source-preview">
      <div className="ar-source-media" style={{ background: ad.thumb }}>
        {playing && hasVideo ? (
          <video
            src={ad.creativeUrl}
            poster={ad.thumbnailUrl || undefined}
            controls
            autoPlay
            playsInline
            className="ar-source-video"
          />
        ) : (
          <>
            {showVideoPreview && (
              <video
                className="ar-source-video-preview"
                src={ad.creativeUrl}
                muted
                autoPlay
                loop
                playsInline
                preload="auto"
                onLoadedData={(e) => {
                  const video = e.currentTarget;
                  try { e.currentTarget.currentTime = 0.15; } catch { /* preview seek can fail on some streams */ }
                  try { video.play(); } catch { /* muted autoplay can still be blocked in rare cases */ }
                }}
                onError={(e) => { e.currentTarget.style.display = 'none'; }}
              />
            )}
            <span className="ad-plat" style={{ background: p.bg, color: p.color }}>
              <span className="ad-plat-dot" style={{ background: p.color }} />
              {p.label}
            </span>
            <button
              className="ad-play ar-source-play"
              onClick={() => hasVideo && setPlaying(true)}
              title={hasVideo ? 'Play original video' : 'No video creative available'}
              disabled={!hasVideo}
            >
              <Icon name="play" size={16} />
            </button>
            {ad.duration && <span className="ad-duration">{ad.duration}</span>}
            {scoreText !== null && (
              <span className="ar-source-score" title="Evaluator score">
                <Icon name="trend" size={10} />
                {scoreText}
              </span>
            )}
          </>
        )}
      </div>
      <div className="ar-source-body">
        <div className="ar-source-title-row">
          <span className="ar-source-title">Original ad</span>
          {hasVideo ? <span className="ar-source-pill">video</span> : <span className="ar-source-pill muted">thumbnail</span>}
        </div>
        {directionWords && (
          <div className="ad-direction" title={`Search direction: ${directionWords}`}>
            <Icon name="target" size={10} />
            <span>{directionWords}</span>
          </div>
        )}
        {sourceCopy && <p className="ar-source-copy">{sourceCopy}</p>}
        <div className="ar-source-meta">
          {scoreText !== null && <span><Icon name="trend" size={10} /> {scoreText}</span>}
          {metricChips.map(({ label, value, title, className }) => (
            <span key={label} className={`ad-metric-chip ${className}`.trim()} title={title}>
              <span className="ad-metric-label">{label}</span>{value}
            </span>
          ))}
          {ad.daysRunning ? <span>{ad.daysRunning}d running</span> : null}
          {ad.directionLabel && ad.directionLabel !== directionWords ? <span>{ad.directionLabel}</span> : null}
        </div>
      </div>
    </div>
  );
}

function AutoRemodelScriptEditor({ variant, onSave, onRegenerate, onDiscard, onPromote }) {
  const [draft, setDraft] = React.useState(variant.raw || '');
  const [expanded, setExpanded] = React.useState(false);
  const [editing, setEditing] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  React.useEffect(() => { setDraft(variant.raw || ''); }, [variant.raw, variant.jobId]);
  const startEdit = () => {
    setExpanded(true);
    setEditing(true);
  };
  const save = async () => {
    setSaving(true);
    try {
      await onSave(draft);
      setEditing(false);
    } finally {
      setSaving(false);
    }
  };
  const cancelEdit = () => {
    setDraft(variant.raw || '');
    setEditing(false);
  };
  return (
    <section style={{ border: '1px solid var(--border-subtle)', borderRadius: 8, overflow: 'hidden', background: 'var(--bg-sunken)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', borderBottom: '1px solid var(--border-subtle)' }}>
        <button
          className={`ar-script-toggle ${expanded ? 'open' : ''}`}
          onClick={() => setExpanded(v => !v)}
          title={expanded ? 'Hide remodeled script' : 'Show remodeled script'}
          aria-expanded={expanded}
        >
          <Icon name="chevronDown" size={14} />
        </button>
        <strong style={{ fontSize: 12 }}>Remodeled</strong>
        {variant.promotedAt && <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>Promoted {String(variant.promotedAt).slice(0, 16)}</span>}
        <div className="t-hdr-spacer" />
        {!editing && (
          <button className="t-edit-btn" onClick={startEdit} title="Edit generated script">
            <Icon name="edit" size={12} /> Edit
          </button>
        )}
        {!editing && (
          <button className="t-edit-btn" onClick={onRegenerate}>
            <Icon name="loader" size={12} /> Regenerate
          </button>
        )}
        {!editing && (
          <button className="t-edit-btn ghost" onClick={onDiscard}>
            <Icon name="trash" size={12} /> Discard
          </button>
        )}
        {!editing && (
          <button className="t-edit-btn primary" onClick={onPromote}>
            <Icon name="film" size={12} /> {variant.promotedAt ? 'Promoted' : 'Promote to Video Queue'}
          </button>
        )}
        {editing && (
          <>
            <button className="t-edit-btn ghost" onClick={cancelEdit} disabled={saving}>
              Cancel
            </button>
            <button className="t-edit-btn primary" onClick={save} disabled={saving || !draft.trim()}>
              <Icon name="check" size={12} /> {saving ? 'Saving…' : 'Save'}
            </button>
          </>
        )}
      </div>
      {expanded && (editing ? (
        <textarea
          className="t-edit-area"
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          spellCheck
          autoFocus
          style={{ minHeight: 180, border: 0, borderRadius: 0 }}
        />
      ) : (
        <pre className="ar-script-readonly">{variant.raw || ''}</pre>
      ))}
    </section>
  );
}

function RemodelView({ ads, sel, onToggle, scripts, onEditScript, onRegenerate, onDiscard, onPromote }) {
  // ``scripts[ad.id]`` is one of:
  //   { status: 'ready',   variants: [{ jobId, variantIdx, raw, lines }] }
  //   { status: 'pending', variants: [] }
  //   { status: 'error',   variants: [] }
  // Each variant renders as its own editable TranscriptCard so multi-variant
  // Auto-Remodel runs surface every Sonnet output. Backwards-compatible with
  // the old single-entry shape so manual flows that haven't re-polled yet
  // still render.
  const cardsForAd = (ad) => {
    const entry = scripts[ad.id];
    let status = 'pending';
    let variants = [];
    if (entry && Array.isArray(entry.variants)) {
      status = entry.status || 'pending';
      variants = entry.variants;
    } else if (entry && typeof entry === 'object' && entry.jobId) {
      status = 'ready';
      variants = [{ jobId: entry.jobId, variantIdx: 0, raw: entry.raw, lines: entry.lines }];
    } else if (entry === 'error') {
      status = 'error';
    } else if (Array.isArray(entry) && entry.length) {
      status = 'ready';
      variants = [{ jobId: null, variantIdx: 0, raw: '', lines: entry }];
    }
    if (status === 'pending' || (status === 'ready' && variants.length === 0)) {
      return [(
        <TranscriptCard
          key={`${ad.id}-pending`}
          ad={ad}
          script={[{ tag: 'REMODELING', body: 'Claude Sonnet is generating the remodel — this usually takes 20–60s. Polling…' }]}
          selected={sel.has(ad.id)}
          onToggle={() => onToggle(ad.id)}
        />
      )];
    }
    if (status === 'error') {
      return [(
        <TranscriptCard
          key={`${ad.id}-error`}
          ad={ad}
          script={[{ tag: 'ERROR', body: 'Remodel failed for this ad. Check server logs.' }]}
          selected={sel.has(ad.id)}
          onToggle={() => onToggle(ad.id)}
        />
      )];
    }
    const multi = variants.length > 1;
    return variants.map(v => {
      // Decorate the brand label so users can tell variants apart.
      const adForCard = multi ? { ...ad, brand: `${ad.brand} · variant ${v.variantIdx + 1}` } : ad;
      const editable = !!v.jobId;
      return (
        <TranscriptCard
          key={`${ad.id}-v${v.variantIdx}-${v.jobId || 'nojob'}`}
          ad={adForCard}
          script={v.lines}
          selected={sel.has(ad.id)}
          onToggle={() => onToggle(ad.id)}
          editable={editable}
          rawText={editable ? v.raw : ''}
          onSave={editable ? (newText) => onEditScript(v.jobId, newText) : null}
          onRegenerate={editable ? () => onRegenerate(v.jobId) : null}
          onDiscard={editable ? () => onDiscard(v.jobId) : null}
          onPromote={editable ? () => onPromote(v.jobId) : null}
          promotedAt={v.promotedAt}
        />
      );
    });
  };
  const isAutoRemodel = ads.some(a => a.source === 'auto_remodel');
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">{isAutoRemodel ? 'Auto-Remodel Results' : 'Remodel'}</span>
        <span className="content-sub">Review final scripts, edit inline, regenerate, discard, or promote to the video queue.</span>
      </div>
      {ads.length === 0 ? (
        <div className="empty">
          <span className="empty-display">Nothing remodeled yet</span>
          <p className="empty-body">Select ads in Filter and press Run Remodel, Pass without Filtering from Research, or kick off Auto-Remodel.</p>
        </div>
      ) : (
        ads.map(ad => (
          <React.Fragment key={ad.id}>{cardsForAd(ad)}</React.Fragment>
        ))
      )}
    </>
  );
}

function VideoView({ ads }) {
  return (
    <>
      <div className="content-hdr">
        <span className="content-title">Video-Conversion</span>
        <span className="content-sub">Queued for the video-editing pipeline (backend in development).</span>
      </div>
      {ads.length === 0 ? (
        <div className="empty">
          <span className="empty-display">Queue is empty</span>
          <p className="empty-body">Select remodeled scripts in Remodel and press Convert to Video.</p>
        </div>
      ) : (
        <div className="platform-panel">
          <header className="pp-hdr">
            <span className="pp-chip" style={{ background: '#fdf1da', color: '#a56c00' }}>
              <span className="pp-dot" style={{ background: '#d97706' }} />
              Queued
            </span>
            <span className="pp-count">{ads.length} jobs</span>
            <div className="pp-spacer" />
            <span className="pp-dedupe">ETA ~{ads.length * 90}s</span>
          </header>
          <div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 10 }}>
            {ads.map(ad => (
              <div key={ad.id} style={{ display: 'flex', gap: 12, alignItems: 'center', padding: 10, background: 'var(--bg-sunken)', borderRadius: 8, border: '1px solid var(--border-subtle)' }}>
                <div style={{ width: 40, height: 50, background: ad.thumb, borderRadius: 6, flexShrink: 0 }} />
                <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 3 }}>
                  <span style={{ fontSize: 13, fontWeight: 600 }}>{ad.brand}</span>
                  <span style={{ fontSize: 11, color: 'var(--fg-3)', fontFamily: 'var(--font-mono)' }}>{ad.id} · remodeled · awaiting editor</span>
                </div>
                <span className="pill">Pending</span>
              </div>
            ))}
          </div>
        </div>
      )}
    </>
  );
}

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