// Brand Research — queue on the left, profile on the right.
// Profile shows all research fields, citations, sponsorship history, competitors.
// Status badges, low-confidence callouts, rejected/KIV stopped states.

// Assignee + country helpers come from ui.jsx (window.displayAssignee,
// window.extractCountry, window.ASSIGNEE_NAMES).

function BrandResearch({ brands, selectedBrandId, onSelectBrand, onNavigate, onUpdateBrand, onMoveToResearch, onToast, onResearchBrand, onRefreshBrandDetail, currentUser }) {
  // Active + stopped buckets. Split on the pipeline's held/closed test rather
  // than on `status` alone: rows can be status 'relevant' AND research 'rejected'
  // at the same time (6 of them are), and those were showing up in the active
  // queue as "rejected" — the reason this list looked like it was full of
  // brands that had already been dealt with.
  const activeBrands  = brands.filter((b) => !pbIsHeldOrClosed(b));
  const stoppedBrands = brands.filter((b) => pbIsHeldOrClosed(b));

  // ─── Filters ────────────────────────────────────────────────────────────
  // One shared filter set (see useBrandFilters in ui.jsx). Tier / category /
  // assignee / country are the SAME state here, on Match, on Contacts and on
  // Draft — set "my Tier 1 brands" once and it holds everywhere.
  const f = useBrandFilters(activeBrands);
  // This page exists to answer one question per brand: which property do we
  // pitch? So it DEFAULTS to only the brands still owing that answer. A brand
  // drops off the moment a property (or "No match") is confirmed — the filters
  // are there for when you want to look back at ones already decided.
  const [reviewFilter, setReviewFilter] = usePersistentState('filters.review', 'needs'); // needs | decided | all
  const filtersActive = f.active || reviewFilter !== 'all';

  // Decided is an explicit test (isDecided), not "everything that is not on the
  // review list". The old subtraction counted brands nobody had researched yet
  // as decided, so the Decided tab held 247 rows of which 17 had had no decision
  // made about them at all.
  const filteredActive = React.useMemo(
    () => activeBrands.filter((b) => {
      if (!f.matches(b)) return false;
      if (reviewFilter === 'needs' && !needsPropertyDecision(b)) return false;
      if (reviewFilter === 'decided' && !isDecided(b)) return false;
      return true;
    }),
    [activeBrands, f.matches, reviewFilter]
  );

  const needsReviewCount = React.useMemo(() => activeBrands.filter(needsPropertyDecision).length, [activeBrands]);
  const decidedCount     = React.useMemo(() => activeBrands.filter(isDecided).length, [activeBrands]);
  // The remainder: active, but not at this gate yet (never researched, or still
  // researching). Named in the footer so All can exceed the other two tabs
  // without the numbers looking broken.
  const earlierCount     = React.useMemo(() => activeBrands.filter(awaitingEarlierStage).length, [activeBrands]);

  // Newest first. reviewDate alone is not enough: it is a business date (day
  // granularity), so an entire scan batch shares one value and the order inside
  // today's block came down to whatever order Postgres returned — a brand you
  // just scanned could land anywhere in it. createdAt is a real timestamp, so it
  // breaks those ties precisely and puts the newest arrival at the top.
  const sortedActive = React.useMemo(
    () => [...filteredActive].sort((a, b) =>
      (b.reviewDate || '').localeCompare(a.reviewDate || '')
      || (b.createdAt || '').localeCompare(a.createdAt || '')
      // Last resort so rows never swap places between refreshes when both
      // dates match (imported brands share a createdAt).
      || String(a.id || '').localeCompare(String(b.id || ''))
    ),
    [filteredActive]
  );

  const allBrands = [...sortedActive, ...stoppedBrands];
  const selected = brands.find((b) => b.id === selectedBrandId) || sortedActive[0];

  React.useEffect(() => {
    if (!selectedBrandId && sortedActive[0]) onSelectBrand(sortedActive[0].id);
  }, [selectedBrandId, sortedActive]);

  const [showStopped, setShowStopped] = React.useState(false);

  // ─── Bulk actions ───────────────────────────────────────────────────────
  // Ticking rows and acting on the lot used to exist only on News Intake, so
  // re-assigning or parking ten researched brands meant ten separate trips.
  const bulk = useBulkSelect(sortedActive);
  const applyBulk = (fn, label) => {
    const ids = bulk.ids;
    ids.forEach(fn);
    bulk.clear();
    onToast && onToast(`${ids.length} brand${ids.length === 1 ? '' : 's'} ${label}`);
  };
  const bulkActions = [
    {
      label: 'Mark reviewed', icon: 'check',
      title: 'Sign these off as reviewed — they leave the review list without recording a property decision',
      onClick: () => {
        const by = displayNameFor(currentUser);
        applyBulk((id) => onUpdateBrand(id, reviewPatch(true, by)), 'marked reviewed');
      },
    },
    {
      label: 'Re-research', icon: 'sparkle',
      title: 'Regenerate the profile for each selected brand',
      onClick: () => applyBulk((id) => {
        onUpdateBrand(id, { researchStatus: 'researching', needsManualReview: false });
        onResearchBrand && onResearchBrand(id);
      }, 'sent back for research'),
    },
    {
      label: 'KIV', icon: 'eye',
      title: 'Keep in view — held out of the active flow',
      onClick: () => applyBulk((id) => onUpdateBrand(id, {
        status: 'maybe', priority: 'kiv', researchStatus: 'kiv',
        needsManualReview: false, userOverride: true,
      }), 'kept in view'),
    },
    {
      label: 'Reject', icon: 'x', className: 'btn btn-sm btn-ghost', danger: true,
      title: 'Stop these brands',
      onClick: () => applyBulk((id) => onUpdateBrand(id, {
        status: 'not-relevant', priority: 'no-action', researchStatus: 'rejected',
        needsManualReview: false, userOverride: true,
      }), 'rejected'),
    },
  ];

  return (
    <div className="page page-cap" style={{ paddingTop: 22 }}>
      <SectionHeader
        eyebrow="Step 02 · Researched by the system"
        title="Brand research"
        sub="Highly relevant brands are researched automatically. Maybe-relevant brands wait for your approval before research begins. Sources are cited wherever the system found supporting evidence."
      />

      <BrandBulkBar
        bulk={bulk}
        actions={bulkActions}
        onAssign={(name) => { if (name) applyBulk((id) => onUpdateBrand(id, { assignee: name }), `assigned to ${name}`); }}
      />

      <div className="responsive-split">
        <Card pad={false} style={{ position: 'sticky', top: 80, maxHeight: 'calc(100vh - 120px)', overflowY: 'auto' }}>
          <div style={{ padding: '12px 14px', borderBottom: '1px solid var(--hairline)' }}>
            <div className="row" style={{ justifyContent: 'space-between', alignItems: 'flex-start' }}>
              <div>
                <div className="eyebrow">Research queue</div>
                <div className="text-xs text-3" style={{ marginTop: 4 }}>
                  {reviewFilter === 'needs'
                    ? `${filteredActive.length} to review · ${decidedCount} decided`
                    : `${filteredActive.length} of ${activeBrands.length} active · ${stoppedBrands.length} stopped or parked`}
                  {earlierCount ? ` · ${earlierCount} still waiting on News Intake` : ''}
                </div>
              </div>
              <div className="row gap-1">
                {sortedActive.length > 0 ? (
                  <label
                    className="btn btn-icon btn-ghost"
                    title={bulk.allSelected ? 'Unselect all shown' : 'Select all shown'}
                    style={{ cursor: 'pointer' }}
                  >
                    <input type="checkbox" checked={bulk.allSelected} onChange={bulk.toggleAll} />
                  </label>
                ) : null}
                <button
                  className="btn btn-icon btn-ghost"
                  onClick={() => f.setOpen((x) => !x)}
                  title="Filter queue"
                  style={{ color: filtersActive ? 'var(--accent)' : 'var(--ink-3)' }}
                >
                  <window.I.filter size={14} />
                </button>
              </div>
            </div>
          </div>

          {/* Review axis, always visible — it is the first cut the meeting makes. */}
          <StageFilterBar
            value={reviewFilter}
            onChange={setReviewFilter}
            options={[
              { id: 'needs',   label: 'To review', count: needsReviewCount, title: 'Researched, but no property chosen yet — this is the working list' },
              { id: 'decided', label: 'Decided',   count: decidedCount,     title: 'A property is on record, or a person marked the brand reviewed' },
              { id: 'all',     label: 'All',       count: activeBrands.length,
                title: earlierCount
                  ? `Every active brand. ${needsReviewCount} to review, ${decidedCount} decided, ${earlierCount} not yet researched — those are decided in News Intake.`
                  : 'Every active brand' },
            ]}
          />

          {f.open ? <BrandFilterPanel f={f} /> : null}
          <div>
            {sortedActive.map((b) => {
              const isSel = selected && selected.id === b.id;
              return (
                <div
                  key={b.id}
                  onClick={() => onSelectBrand(b.id)}
                  style={{
                    padding: '12px 14px',
                    borderBottom: '1px solid var(--hairline)',
                    cursor: 'pointer',
                    background: isSel ? 'var(--accent-soft-bg)' : 'transparent',
                    borderLeft: isSel ? '3px solid var(--accent)' : '3px solid transparent',
                    transition: 'background 100ms ease',
                  }}
                >
                  <div className="row gap-2" style={{ marginBottom: 4, alignItems: 'center' }}>
                    <input
                      type="checkbox"
                      checked={bulk.has(b.id)}
                      onClick={(e) => e.stopPropagation()}
                      onChange={() => bulk.toggle(b.id)}
                      title="Select for a bulk action"
                    />
                    <BrandLogo brand={b} size={22} />
                    <span style={{ fontWeight: 600, fontSize: 13.5 }}>{b.brand}</span>
                    <span className="right">
                      <PriorityPill priority={b.priority} />
                    </span>
                  </div>
                  <div className="text-xs text-3 truncate" style={{ maxWidth: 280, paddingLeft: 30 }}>{b.category}</div>
                  <div className="row gap-2" style={{ marginTop: 6, flexWrap: 'wrap', alignItems: 'center' }}>
                    <ResearchPill research={b.researchStatus} />
                    {/* Has a person signed it off, and what the stage wants. */}
                    <BrandStatusChip brand={b} onNavigate={onNavigate} />
                    {/* Confirmed first; failing that, what the system suggested.
                        The meeting works down this list, and a suggestion that
                        only appeared inside the profile — below every panel of
                        evidence — was being missed on almost every brand. Shown
                        here it can be triaged without opening anything, and the
                        dimmer, arrow-less styling keeps "we decided this" and
                        "the system thinks this" from reading alike. */}
                    {b.confirmedProperties && b.confirmedProperties.length ? (
                      <span className="text-xs text-3 mono">
                        → {b.confirmedProperties.map((p) => p === 'generic' ? 'Generic' : propLabel(p)).join(', ')}
                      </span>
                    ) : (b.aiRecommendation && (b.aiRecommendation.properties || []).length) ? (
                      <span className="text-xs text-4 mono" title="Suggested by research — not yet confirmed">
                        {b.aiRecommendation.properties.slice(0, 2).map(propLabel).join(', ')}
                        {b.aiRecommendation.properties.length > 2
                          ? ` +${b.aiRecommendation.properties.length - 2}` : ''}
                      </span>
                    ) : null}
                    {/* Sign-off on the row itself. The meeting works down this
                        list, so the tick has to be here — not only inside the
                        profile, which meant opening every brand to clear it. */}
                    <span className="right" onClick={(e) => e.stopPropagation()}>
                      <MarkReviewedControl
                        brand={b}
                        currentUser={currentUser}
                        onUpdateBrand={onUpdateBrand}
                        onToast={onToast}
                      />
                    </span>
                  </div>
                </div>
              );
            })}

            {sortedActive.length === 0 ? (
              <div className="empty" style={{ padding: '32px 18px' }}>
                {reviewFilter === 'needs' && !f.active ? (
                  <>
                    <window.I.check size={24} stroke="var(--positive)" />
                    <div style={{ marginTop: 8, fontWeight: 600, color: 'var(--ink)' }}>You've reviewed this section.</div>
                    <div className="text-xs text-3" style={{ marginTop: 4, lineHeight: 1.55 }}>
                      Every researched brand has a property decision. Use <strong>Decided</strong> to look back over them.
                    </div>
                    <button className="btn btn-sm" style={{ marginTop: 10 }} onClick={() => setReviewFilter('decided')}>
                      Show decided brands
                    </button>
                  </>
                ) : filtersActive ? (
                  <>
                    <div>No brands match these filters.</div>
                    <button
                      className="btn btn-sm"
                      style={{ marginTop: 10 }}
                      onClick={() => { f.clear(); setReviewFilter('all'); }}
                    >
                      Clear filters
                    </button>
                  </>
                ) : activeBrands.length === 0 ? (
                  <>
                    <div>Nothing in research yet.</div>
                    <div className="text-xs text-3" style={{ marginTop: 4 }}>
                      Send a brand here from News Intake, or run a scan to pull in new ones.
                    </div>
                    <button className="btn btn-sm btn-primary" style={{ marginTop: 10 }} onClick={() => onNavigate('intake')}>
                      Open News Intake
                      <window.I.arrowRight size={12} />
                    </button>
                  </>
                ) : (
                  <div>Nothing active — everything here has been stopped or parked.</div>
                )}
              </div>
            ) : null}

            {stoppedBrands.length > 0 ? (
              <>
                <div
                  className="row gap-2"
                  onClick={() => setShowStopped((x) => !x)}
                  style={{
                    padding: '10px 14px',
                    background: 'var(--card-alt)',
                    cursor: 'pointer',
                    borderTop: '1px solid var(--hairline)',
                    borderBottom: showStopped ? '1px solid var(--hairline)' : 'none',
                  }}
                >
                  <window.I.ban size={12} stroke="var(--ink-3)" />
                  <span className="eyebrow">Stopped — {stoppedBrands.length}</span>
                  <span className="right">
                    <window.I.chevronDown
                      size={13}
                      stroke="var(--ink-3)"
                      style={{ transform: showStopped ? 'rotate(180deg)' : 'none', transition: 'transform 100ms' }}
                    />
                  </span>
                </div>
                {showStopped ? stoppedBrands.map((b) => {
                  const isSel = selected && selected.id === b.id;
                  return (
                    <div
                      key={b.id}
                      onClick={() => onSelectBrand(b.id)}
                      style={{
                        padding: '10px 14px',
                        borderBottom: '1px solid var(--hairline)',
                        cursor: 'pointer',
                        background: isSel ? 'var(--accent-soft-bg)' : 'transparent',
                        borderLeft: isSel ? '3px solid var(--accent)' : '3px solid transparent',
                        opacity: 0.75,
                      }}
                    >
                      <div className="row gap-2" style={{ alignItems: 'center' }}>
                        <BrandLogo brand={b} size={20} />
                        <span style={{ fontWeight: 500, fontSize: 13 }}>{b.brand}</span>
                        <span className="right">
                          <StatusPill status={b.status} />
                        </span>
                      </div>
                      <div className="text-xs text-3 truncate" style={{ maxWidth: 280, marginTop: 3, paddingLeft: 28 }}>{b.category}</div>
                    </div>
                  );
                }) : null}
              </>
            ) : null}
          </div>
        </Card>

        <div>
          {selected ? (
            <BrandProfile brand={selected} onNavigate={onNavigate} onUpdateBrand={onUpdateBrand} onMoveToResearch={onMoveToResearch} onToast={onToast} onResearchBrand={onResearchBrand}
            onRefreshBrandDetail={onRefreshBrandDetail} currentUser={currentUser} />
          ) : (
            <Card>
              <div className="empty">
                {/* An empty "To review" list is the good outcome, not a filter
                    problem — say so rather than telling the user to clear
                    filters they never set. */}
                {reviewFilter === 'needs' && !f.active ? (
                  <>
                    <window.I.check size={26} stroke="var(--positive)" />
                    <div style={{ marginTop: 10, fontWeight: 600, color: 'var(--ink)', fontSize: 15 }}>
                      You've reviewed this section.
                    </div>
                    <div className="text-sm" style={{ marginTop: 6, maxWidth: 440, marginInline: 'auto', lineHeight: 1.6 }}>
                      Every researched brand has a property decision. Contacts and drafts are generated
                      when a property is confirmed, so the next step is the Draft page.
                    </div>
                    <div className="row gap-2" style={{ marginTop: 14, justifyContent: 'center', flexWrap: 'wrap' }}>
                      <button className="btn btn-sm btn-primary" onClick={() => onNavigate('draft')}>
                        Go to drafts
                        <window.I.arrowRight size={12} />
                      </button>
                      <button className="btn btn-sm" onClick={() => setReviewFilter('decided')}>Show decided brands</button>
                    </div>
                  </>
                ) : (
                  <>
                    <div style={{ fontWeight: 600, color: 'var(--ink)' }}>Nothing to read yet.</div>
                    <div className="text-sm" style={{ marginTop: 6, maxWidth: 420, marginInline: 'auto' }}>
                      {f.active
                        ? 'Your filters are hiding every brand in the queue. Clear them to pick one.'
                        : 'Send a brand into research from News Intake — tick it and choose Research, or run a scan to pull in new ones.'}
                    </div>
                    <div className="row gap-2" style={{ marginTop: 12, justifyContent: 'center' }}>
                      {filtersActive ? (
                        <button className="btn btn-sm" onClick={() => { f.clear(); setReviewFilter('needs'); }}>Clear filters</button>
                      ) : null}
                      <button className="btn btn-sm btn-primary" onClick={() => onNavigate('intake')}>
                        Open News Intake
                        <window.I.arrowRight size={12} />
                      </button>
                    </div>
                  </>
                )}
              </div>
            </Card>
          )}
        </div>
      </div>
    </div>
  );
}

function BrandProfile({ brand, onNavigate, onUpdateBrand, onMoveToResearch, onToast, onResearchBrand, onRefreshBrandDetail, currentUser }) {
  const isKiv = brand.priority === 'kiv';
  const isRejected = brand.status === 'not-relevant' || brand.status === 'duplicate';

  const [editing, setEditing] = React.useState(false);
  // Local form state mirrors editable fields
  const [form, setForm] = React.useState(() => buildEditForm(brand));
  React.useEffect(() => { setForm(buildEditForm(brand)); setEditing(false); }, [brand.id]);

  // Research engine state
  const hasResearch = !!(brand.description || '').trim();
  const isResearchingNow = brand.researchStatus === 'researching' && !hasResearch;
  const [researching, setResearching] = React.useState(false);
  const handleResearch = async () => {
    if (!onResearchBrand) return;
    setResearching(true);
    try { await onResearchBrand(brand.id); }
    finally { setResearching(false); }
  };

  // Footprint-only refresh. Exposure and market focus come from their own
  // research pass, so a thin country list can be fixed without re-running (and
  // re-churning) the news, signals and property match.
  const [refreshingMarkets, setRefreshingMarkets] = React.useState(false);
  const handleRefreshMarkets = async () => {
    setRefreshingMarkets(true);
    try {
      await window.IPSEM_API.refreshMarkets(brand.id);
      onToast && onToast('Refreshing footprint — exposure and market focus update in about a minute.');
      // The job writes markets/exposure/market_focus, which the brands LIST does
      // not carry, and it never sets research_status='researching' — so no poll
      // and no list refresh can bring the result back. Without this the panel
      // showed the old footprint until a full page reload.
      if (onRefreshBrandDetail) {
        [20000, 45000, 90000].forEach((ms) => setTimeout(() => onRefreshBrandDetail(brand.id), ms));
      }
    } catch (err) {
      onToast && onToast(err.message || 'Could not refresh the footprint.');
    } finally {
      setRefreshingMarkets(false);
    }
  };

  // Prior research runs (for "Previous research" panel at bottom).
  const [priorRuns, setPriorRuns] = React.useState([]);
  React.useEffect(() => {
    let alive = true;
    window.IPSEM_API.getResearchRuns(brand.id)
      .then((runs) => { if (alive) setPriorRuns(Array.isArray(runs) ? runs.filter(r => !r.isCurrent) : []); })
      .catch(() => {});
    return () => { alive = false; };
  }, [brand.id, brand.researchStatus]);

  const set = (k, v) => setForm((c) => ({ ...c, [k]: v }));

  const handleSave = () => {
    const patch = {
      description: form.description,
      coreValues: form.coreValues.split(',').map((s) => s.trim()).filter(Boolean),
      hq: form.hq,
      exposure: form.exposure,
      marketFocus: form.marketFocus,
      spending: form.spending,
      competitors: form.competitors.split(',').map((s) => s.trim()).filter(Boolean),
    };
    // A hand-edited footprint has to win over the researched one, or saving an
    // edit looks like it did nothing: the structured panel would keep rendering
    // the country list and the edited prose would never appear. Flag the field
    // instead of dropping brand.markets, so a Refresh footprint can still
    // restore the researched version.
    const m = brand.markets;
    if (m && typeof m === 'object') {
      const manual = {};
      if (form.exposure !== (brand.exposure || '')) manual.manualExposure = true;
      if (form.marketFocus !== (brand.marketFocus || '')) manual.manualMarketFocus = true;
      if (Object.keys(manual).length) patch.markets = { ...m, ...manual };
    }
    onUpdateBrand(brand.id, patch);
    setEditing(false);
    onToast && onToast(`${brand.brand} updated`);
  };

  return (
    <div className="col gap-4">
      {/* Header card */}
      <Card lg>
        <div className="row gap-3" style={{ alignItems: 'flex-start' }}>
          <BrandLogo brand={brand} size={42} radius={9} />
          <div className="grow">
            <div className="row gap-2" style={{ marginBottom: 6 }}>
              <h2 className="h2" style={{ margin: 0 }}>{brand.brand}</h2>
              {brand.previousPartner ? (
                <Pill kind="muted">Existing relationship</Pill>
              ) : null}
            </div>
            <div className="text-2 text-sm row gap-2">
              <span>{brand.category}</span>
              {brand.hq ? (<><span className="text-4">·</span><span className="row gap-1"><window.I.pin size={12} />{brand.hq}</span></>) : null}
            </div>
          </div>
          <div className="col" style={{ alignItems: 'flex-end', gap: 6 }}>
            <div className="row gap-2">
              <ResearchPill research={brand.researchStatus} />
              <PriorityPill priority={brand.priority} />
            </div>
            <div className="row gap-2" style={{ marginTop: 2 }}>
              <span className="text-xs text-3">Score</span>
              <ScoreWithBreakdown brand={brand} alignRight />
            </div>
            <div className="row gap-2">
              <span className="text-xs text-3">Assignee</span>
              <span className="text-xs" style={{ fontWeight: 500 }}>
                {displayAssignee(brand.assignee) || <span className="text-3">Unassigned</span>}
              </span>
            </div>
            <span className="text-xs text-3 row gap-1 mono">
              <window.I.cal size={11} /> Reviewed {formatDate(brand.reviewDate)}
            </span>
            {/* The other half of this brand. Flow carries the tasks against it,
                who owns it, its history and the comment thread; the research
                itself stays here. No lookup is needed either way — the id is
                the same slug on both sides. Hidden unless flowUrl is set, so an
                installation without Flow shows nothing rather than a dead link. */}
            {window.IPSEM_CONFIG && window.IPSEM_CONFIG.flowUrl ? (
              <a
                className="text-xs row gap-1"
                href={`${window.IPSEM_CONFIG.flowUrl}/research?brand=${encodeURIComponent(brand.id)}`}
                target="_blank"
                rel="noreferrer"
                title="Open this brand in IPSEM Flow — tasks, owner, history and comments"
              >
                Open in IPSEM Flow
                <window.I.external size={11} />
              </a>
            ) : null}
            {!isRejected && !isKiv ? (
              <div className="row gap-2" style={{ marginTop: 6, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
                {onResearchBrand ? (
                  <button
                    className="btn btn-sm"
                    onClick={handleResearch}
                    disabled={researching || brand.researchStatus === 'researching'}
                    title={hasResearch ? 'Refresh research, snapshotting the previous run' : 'Run deep research on this brand'}
                  >
                    <window.I.refresh size={12} className={(researching || brand.researchStatus === 'researching') ? 'spin' : ''} />
                    {(researching || brand.researchStatus === 'researching') ? 'Researching…' : (hasResearch ? 'Re-research' : 'Research now')}
                  </button>
                ) : null}
                <button className="btn btn-sm" onClick={() => setEditing((x) => !x)}>
                  <window.I.edit size={12} />
                  {editing ? 'Cancel edit' : 'Edit details'}
                </button>
                {/* The step out of this page. It used to live only inside the
                    "Suggested IPSEM properties" card, which is hidden when the
                    AI suggested nothing — so for those brands there was no
                    visible way to get to Property Match. */}
                {needsPropertyDecision(brand) ? (
                  <button className="btn btn-sm btn-primary" onClick={() => onNavigate('match', brand.id)}>
                    Review match
                    <window.I.arrowRight size={12} />
                  </button>
                ) : null}
              </div>
            ) : null}
          </div>
        </div>

        {isKiv ? (
          <div className="banner-stopped kiv" style={{ marginTop: 16 }}>
            <window.I.eye size={16} />
            <div className="grow">
              <div style={{ fontWeight: 600 }}>Keep in View — monitor only.</div>
              <div className="text-sm">{brand.manualReviewNote || 'No outreach yet. The system will reassess if new signals appear.'}</div>
            </div>
            {onMoveToResearch ? (
              <button className="btn btn-sm" onClick={() => { onMoveToResearch(brand.id); onToast && onToast(`${brand.brand} → activated for research`); }}>
                <window.I.bolt size={12} />
                Activate now
              </button>
            ) : null}
          </div>
        ) : null}

        {!isKiv && !isRejected && brand.researchStatus === 'needs-review' ? (
          <div className="row gap-2" style={{ marginTop: 16, padding: '12px 14px', background: 'var(--attention-soft)', border: '1px solid var(--attention)', borderRadius: 8, alignItems: 'center' }}>
            <window.I.warn size={16} stroke="var(--attention)" />
            <div className="grow">
              <div style={{ fontWeight: 600 }}>Research needs your attention.</div>
              <div className="text-sm">{brand.manualReviewNote || 'The last research run could not complete.'} Use "{hasResearch ? 'Re-research' : 'Research now'}" above to retry.</div>
            </div>
          </div>
        ) : null}

        {isRejected ? (
          <div className="banner-stopped" style={{ marginTop: 16 }}>
            <window.I.ban size={16} />
            <div className="grow">
              <div style={{ fontWeight: 600 }}>Flow stopped — {brand.status === 'duplicate' ? 'duplicate' : 'not relevant'}.</div>
              <div className="text-sm">{brand.rejectReason || 'No contact research or draft will be generated.'}</div>
            </div>
            {onMoveToResearch ? (
              <button
                className="btn btn-sm"
                onClick={() => { onMoveToResearch(brand.id); onToast && onToast(`${brand.brand} → moved back to research`); }}
                style={{ color: 'var(--accent)', borderColor: 'var(--accent)' }}
              >
                <window.I.refresh size={12} />
                Research anyway
              </button>
            ) : null}
          </div>
        ) : null}
      </Card>

      {/* Tier / status / relationship / outreach / assignee / actions — the
          same strip on every stage, so nothing forces a trip back to Intake. */}
      <BrandControlBar
        brand={brand}
        onUpdateBrand={onUpdateBrand}
        onMoveToResearch={onMoveToResearch}
        onToast={onToast}
        currentUser={currentUser}
      />

      {/* Notes — kept directly under the header so nobody has to scroll to
          read or add them. */}
      <NotesPanel brand={brand} currentUser={currentUser} onUpdateBrand={onUpdateBrand} onToast={onToast} />

      {/* News card */}
      <Card lg>
        <div className="eyebrow" style={{ marginBottom: 12 }}>Trigger news</div>
        <h3 className="h2" style={{ fontSize: 20, margin: '4px 0 8px' }}>{brand.news?.title}</h3>
        <div className="row gap-2 text-3 text-xs" style={{ marginBottom: 14 }}>
          <span>{brand.news?.source}</span>
          <span>·</span>
          <span>{formatDate(brand.news?.date)}</span>
          {brand.news?.url ? (
            <>
              <span>·</span>
              <a href={brand.news.url} target="_blank" rel="noopener noreferrer" style={{ color: 'var(--accent)' }} className="row gap-1">
                <window.I.external size={11} /> View source
              </a>
            </>
          ) : null}
        </div>
        {(() => {
          const pts = (brand.news?.summaryPoints && brand.news.summaryPoints.length)
            ? brand.news.summaryPoints
            : (brand.news?.summary || '')
                .split(/(?<=[.!?])\s+/)
                .map((s) => s.trim())
                .filter((s) => s.length > 0);
          if (!pts.length) return <div className="text-3 text-sm">No summary available.</div>;
          return (
            <ul className="text-2 text-sm" style={{ lineHeight: 1.6, maxWidth: 720, margin: 0, paddingLeft: 20 }}>
              {pts.map((p, i) => <li key={i} style={{ marginBottom: 6 }}>{p}</li>)}
            </ul>
          );
        })()}
        {brand.signals && brand.signals.length ? (
          <div className="row gap-1" style={{ marginTop: 16, flexWrap: 'wrap' }}>
            {brand.signals.map((s) => <SignalChip key={s} signal={s} />)}
          </div>
        ) : null}

        {/* Why engage — decision support for the salesperson */}
        {(brand.news?.whyEngage || brand.aiRecommendation?.assessment?.rationale) ? (
          <div style={{ marginTop: 18, padding: '14px 16px', background: 'var(--accent-soft-bg)', borderRadius: 10, border: '1px solid var(--accent)' }}>
            <div className="eyebrow row gap-1" style={{ marginBottom: 8 }}>
              <window.I.sparkle size={11} stroke="var(--accent)" />
              Why engage
              {brand.aiRecommendation?.assessment?.worthEngaging ? (
                <Pill
                  kind={({ high: 'positive', medium: 'pending', low: 'muted' })[brand.aiRecommendation.assessment.worthEngaging] || 'muted'}
                  style={{ marginLeft: 6, fontSize: 10 }}
                >
                  {brand.aiRecommendation.assessment.worthEngaging} priority
                </Pill>
              ) : null}
            </div>
            <div className="text-2 text-sm" style={{ lineHeight: 1.6, maxWidth: 720, whiteSpace: 'pre-line' }}>
              {brand.news?.whyEngage || brand.aiRecommendation?.assessment?.rationale}
            </div>
          </div>
        ) : null}
      </Card>

      {/* AI suggested properties */}
      {brand.aiRecommendation && brand.aiRecommendation.properties && brand.aiRecommendation.properties.length > 0 && !isKiv && !isRejected ? (
        <Card lg style={{ background: 'linear-gradient(180deg, var(--card) 0%, var(--accent-soft-bg) 100%)' }}>
          <div className="row" style={{ marginBottom: 14, justifyContent: 'space-between', alignItems: 'flex-start' }}>
            <div>
              <div className="eyebrow row gap-1" style={{ marginBottom: 8 }}>
                <window.I.sparkle size={11} stroke="var(--accent)" />
                System recommendation
              </div>
              <div className="h2" style={{ fontSize: 18, margin: 0 }}>
                Suggested IPSEM properties
              </div>
            </div>
            <button className="btn btn-primary btn-sm" onClick={() => onNavigate('match', brand.id)}>
              Review match
              <window.I.arrowRight size={13} />
            </button>
          </div>
          <div className="row gap-2" style={{ flexWrap: 'wrap' }}>
            {brand.aiRecommendation.properties.map((p) => (
              <Pill key={p} kind="tier1">{propName(p)}</Pill>
            ))}
          </div>
        </Card>
      ) : null}

      {/* Research fields — wide main column, Sources as a narrower rail.
          See .research-split in index.html for why this is not auto-fit. */}
      <div className="research-split">
        <div className="col gap-4" style={{ minWidth: 0 }}>
        {editing ? (
          <Card lg>
            <div className="row" style={{ marginBottom: 14, justifyContent: 'space-between' }}>
              <div className="eyebrow">Edit brand intelligence</div>
              <span className="text-xs text-3">Changes save to this brand's research record.</span>
            </div>
            <div className="col gap-3">
              <BR_EditField label="Description" value={form.description} onChange={(v) => set('description', v)} multiline />
              <BR_EditField label="Core values (comma separated)" value={form.coreValues} onChange={(v) => set('coreValues', v)} />
              <BR_EditField label="HQ location" value={form.hq} onChange={(v) => set('hq', v)} />
              <BR_EditField label="Brand exposure" value={form.exposure} onChange={(v) => set('exposure', v)} multiline />
              <BR_EditField label="Market focus" value={form.marketFocus} onChange={(v) => set('marketFocus', v)} multiline />
              <BR_EditField label="Spending trends" value={form.spending} onChange={(v) => set('spending', v)} multiline />
              <BR_EditField label="Competitors (comma separated)" value={form.competitors} onChange={(v) => set('competitors', v)} />
            </div>
            <div className="row gap-2" style={{ marginTop: 18, justifyContent: 'flex-end' }}>
              <button className="btn" onClick={() => { setForm(buildEditForm(brand)); setEditing(false); }}>Cancel</button>
              <button className="btn btn-primary" onClick={handleSave}>
                <window.I.check size={13} />
                Save changes
              </button>
            </div>
          </Card>
        ) : (
          <Card lg>
            <div className="row" style={{ justifyContent: 'space-between', marginBottom: 12, gap: 8 }}>
              <div className="eyebrow">Brand intelligence</div>
              {!hasResearch ? (
                <span className="text-xs text-3">Run research to populate</span>
              ) : (
                <button
                  className="btn btn-sm"
                  onClick={handleRefreshMarkets}
                  disabled={refreshingMarkets}
                  title="Re-run only the footprint research: country list, exposure and market focus"
                >
                  <window.I.globe size={12} />
                  {refreshingMarkets ? 'Refreshing…' : 'Refresh footprint'}
                </button>
              )}
            </div>

            {(() => {
              const gap = <span className="text-3" style={{ fontStyle: 'italic' }}>Not found in research</span>;
              const cvals = Array.isArray(brand.coreValues) ? brand.coreValues : (brand.coreValues ? [brand.coreValues] : []);
              return (
                <>
                  <Definition label="Description">
                    {brand.description ? <span style={{ whiteSpace: 'pre-line' }}>{brand.description}</span> : gap}
                  </Definition>
                  {/* Growth & investment sits second, right under the description:
                      it is the field the team reads to decide whether this outreach
                      is worth the time. */}
                  {hasGrowthIntel(brand) ? (
                    <Definition label="Growth & investment">
                      <GrowthIntel brand={brand} />
                    </Definition>
                  ) : null}
                  <Definition label="Core values">
                    {cvals.length ? (
                      <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
                        {cvals.map((v, i) => <Pill key={i} kind="muted">{v}</Pill>)}
                      </div>
                    ) : gap}
                  </Definition>
                  {/* Market focus sits above exposure deliberately: it is what the
                      property match is argued on, so it should be the first thing
                      read. Exposure is the supporting detail underneath it. */}
                  <Definition label="Market focus">
                    {hasMarketData(brand) && !brand.markets.manualMarketFocus
                      && ((brand.markets.keyMarkets || []).length || (brand.markets.growthMarkets || []).length) ? (
                      <MarketFocus brand={brand} />
                    ) : brand.marketFocus ? (
                      <span style={{ whiteSpace: 'pre-line' }}>{brand.marketFocus}<CitationsFor brand={brand} anchor="market_focus" /></span>
                    ) : gap}
                  </Definition>
                  <Definition label="Brand exposure">
                    {hasMarketData(brand) && !brand.markets.manualExposure ? (
                      <MarketExposure brand={brand} />
                    ) : brand.exposure ? (
                      <span style={{ whiteSpace: 'pre-line' }}>{brand.exposure}<CitationsFor brand={brand} anchor="exposure" /></span>
                    ) : gap}
                  </Definition>
                  <Definition label="Spending trends">
                    {brand.spending ? (
                      <span style={{ whiteSpace: 'pre-line' }}>{brand.spending}<CitationsFor brand={brand} anchor="spending" /></span>
                    ) : gap}
                  </Definition>
                  <Definition label="Competitors">
                    {(brand.competitors && brand.competitors.length) ? (
                      <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
                        {brand.competitors.map((c, i) => (
                          <span key={i} className="pill" style={{ background: 'transparent', border: '1px solid var(--hairline)', color: 'var(--ink-2)' }}>{c}</span>
                        ))}
                      </div>
                    ) : gap}
                  </Definition>
                  {brand.previousProperties && brand.previousProperties.length ? (
                    <Definition label="Previous IPSEM partnership">
                      <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
                        {brand.previousProperties.map((p) => <Pill key={p} kind="accent">{propLabel(p)}</Pill>)}
                      </div>
                    </Definition>
                  ) : null}
                </>
              );
            })()}
          </Card>
        )}

          {/* Sponsorships — stacked under Brand Intelligence, same width */}
          <SponsorshipsPanel brand={brand} onUpdateBrand={onUpdateBrand} onToast={onToast} />
        </div>

        {/* Sources rail. Sticky so citations stay to hand while you read the
            profile, and its own scroll so a long citation list never stretches
            the row. Source titles are often raw URLs — hence overflow-wrap. */}
        <Card
          lg
          style={{
            background: 'var(--card-alt)',
            position: 'sticky',
            top: 80,
            maxHeight: 'calc(100vh - 110px)',
            overflowY: 'auto',
            minWidth: 0,
          }}
        >
          <div className="row" style={{ justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10 }}>
            <div className="eyebrow">Sources</div>
            {brand.citations && brand.citations.length ? (
              <span className="text-xs text-3 mono">{brand.citations.length}</span>
            ) : null}
          </div>
          {brand.citations && brand.citations.length ? (
            <div className="col gap-2">
              {brand.citations.map((c) => (
                <div key={c.id} className="row gap-2" style={{ alignItems: 'flex-start' }}>
                  <span className="cite" style={{ flexShrink: 0, marginTop: 2 }}>{c.id}</span>
                  <div className="grow" style={{ minWidth: 0, overflowWrap: 'anywhere' }}>
                    {c.url ? (
                      <a
                        href={c.url}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="text-sm"
                        style={{ color: 'var(--accent)', lineHeight: 1.5 }}
                        title={c.url}
                      >
                        {c.text}
                        <window.I.external size={11} style={{ verticalAlign: '-1px', marginLeft: 4, flexShrink: 0 }} />
                      </a>
                    ) : (
                      <span className="text-sm text-2" style={{ lineHeight: 1.5 }}>{c.text}</span>
                    )}
                  </div>
                </div>
              ))}
            </div>
          ) : (
            <div className="text-3 text-xs">No external sources cited for this brand yet.</div>
          )}

          {brand.aiRecommendation && brand.aiRecommendation.rationale ? (
            <>
              <hr className="hr" style={{ margin: '16px 0' }} />
              <div className="eyebrow" style={{ marginBottom: 10 }}>
                <window.I.sparkle size={11} stroke="var(--accent)" style={{ verticalAlign: '-1px', marginRight: 4 }} />
                AI rationale
              </div>
              <div className="text-sm text-2" style={{ lineHeight: 1.55, whiteSpace: 'pre-line' }}>
                {brand.aiRecommendation.rationale}
              </div>
            </>
          ) : null}
        </Card>
      </div>


      {/* If KIV with note */}
      {isKiv && brand.manualReviewNote ? (
        <Card lg>
          <div className="eyebrow" style={{ marginBottom: 8 }}>Reviewer note</div>
          <div className="text-2 text-sm">{brand.manualReviewNote}</div>
        </Card>
      ) : null}

      {/* Previous research — snapshots of earlier runs, oldest at the bottom */}
      {priorRuns.length > 0 ? (
        <Card lg>
          <div className="row" style={{ marginBottom: 12, justifyContent: 'space-between' }}>
            <div className="eyebrow">Previous research</div>
            <span className="text-xs text-3">{priorRuns.length} prior run{priorRuns.length === 1 ? '' : 's'}</span>
          </div>
          <div className="col gap-3">
            {priorRuns.map((run, i) => (
              <div
                key={run.id}
                className="col gap-2"
                style={{
                  padding: '12px 0',
                  borderBottom: i === priorRuns.length - 1 ? 'none' : '1px solid var(--hairline)',
                }}
              >
                <div className="row gap-2 text-xs text-3">
                  <span className="mono">Run #{run.runNumber}</span>
                  <span>·</span>
                  <span>
                    {run.createdAt
                      ? new Date(run.createdAt).toLocaleString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })
                      : ''}
                  </span>
                </div>
                {run.newsTitle ? (
                  <div>
                    <div className="text-sm" style={{ fontWeight: 500 }}>{run.newsTitle}</div>
                    <div className="row gap-2 text-xs text-3" style={{ marginTop: 4 }}>
                      {run.newsSource ? <span>{run.newsSource}</span> : null}
                      {run.newsDate ? (<><span>·</span><span>{formatDate(run.newsDate)}</span></>) : null}
                      {run.relevanceScore != null ? (<><span>·</span><span className="mono">Score {run.relevanceScore}</span></>) : null}
                    </div>
                    {run.newsSummary ? (
                      <div className="text-2 text-xs" style={{ lineHeight: 1.5, marginTop: 6 }}>{run.newsSummary}</div>
                    ) : null}
                    {run.newsUrl ? (
                      <a
                        href={run.newsUrl}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="text-xs row gap-1"
                        style={{ color: 'var(--accent)', marginTop: 6 }}
                      >
                        <window.I.external size={10} />
                        View source
                      </a>
                    ) : null}
                  </div>
                ) : null}
                {(run.signals || []).length > 0 ? (
                  <div className="row gap-1" style={{ flexWrap: 'wrap', marginTop: 6 }}>
                    {run.signals.map((s) => <SignalChip key={s} signal={s} />)}
                  </div>
                ) : null}
                {run.aiRecommendation && (run.aiRecommendation.properties || []).length > 0 ? (
                  <div className="row gap-1" style={{ flexWrap: 'wrap', marginTop: 4 }}>
                    <span className="text-xs text-3" style={{ marginRight: 4 }}>Recommended then:</span>
                    {run.aiRecommendation.properties.map((p) => (
                      <Pill key={p} kind="muted">{propLabel(p)}</Pill>
                    ))}
                  </div>
                ) : null}
              </div>
            ))}
          </div>
        </Card>
      ) : null}
    </div>
  );
}

// ─── Sponsorships panel — single source of truth = sponsorship_deals table ──
// Reads/writes the brand's rows in sponsorship_deals (deal_source "Others").
// The Sponsorship Data page reads the same table, so anything added here shows
// up there automatically (tracking the brand's category × sport spend).
const DEAL_SEGMENTS = [
  'Football', 'Motorsport', 'Rugby', 'Tennis', 'Golf', 'Cricket', 'Basketball',
  'American Football', 'Baseball', 'Ice Hockey', 'Cycling', 'Athletics / Endurance Sports',
  'Marathon', 'Triathlon', 'Winter Sports', 'Combat Sports / MMA', 'Volleyball',
  'Padel', 'Netball', 'Badminton', 'Handball', 'Table Tennis', 'Field Hockey',
  'Equestrian', 'Water Sports', 'Esports', 'College Sports', 'Multi-sport',
  'Action Sports', 'Ambassador / Endorsements', 'Arts, Culture & Entertainment',
  'Community & Others', 'Entertainment Venue', 'Business / Corporate Events', 'Others',
];
const DEAL_SCOPES = ['Global', 'Regional', 'Country-Specific', 'N/A'];

function fmtDealYears(a, b, status) {
  if (!a && !b) return '—';
  if (a && b && a !== b) return `${a}–${b}`;
  if (a && !b) return `${a}–${status === 'current' ? 'present' : '?'}`;
  if (b && !a) return `?–${b}`;
  return String(a || b);
}

function SponsorshipsPanel({ brand, onToast }) {
  // The app's own confirm dialog, not the browser's — window.confirm blocks the
  // page, looks nothing like the rest of the app, and some browsers let a user
  // suppress it entirely, which would have deleted without asking.
  const [confirm, confirmEl] = useConfirm();
  const [deals, setDeals] = React.useState(null);   // null = loading
  const [error, setError] = React.useState('');
  const [editing, setEditing] = React.useState(null); // deal id, or 'new', or null
  const blank = {
    property_name: '', property_segment: '', geographic_scope: '', designation: '',
    start_year: '', end_year: '', est_value_usd: '', deal_status: 'current', notes: '',
  };
  const [form, setForm] = React.useState(blank);
  const [busy, setBusy] = React.useState(false);
  const set = (k, v) => setForm((c) => ({ ...c, [k]: v }));

  const load = React.useCallback(() => {
    window.IPSEM_API.getDeals(brand.id)
      .then((d) => { setDeals(d); setError(''); })
      .catch((e) => { setError(e.message || 'Could not load deals'); setDeals([]); });
  }, [brand.id]);

  React.useEffect(() => { load(); }, [load]);

  const openNew = () => { setForm(blank); setEditing('new'); };
  const openEdit = (d) => {
    setForm({
      property_name: d.propertyName || '', property_segment: d.propertySegment || '',
      geographic_scope: d.geographicScope || '', designation: d.designation || '',
      start_year: d.startYear || '', end_year: d.endYear || '',
      est_value_usd: d.estValueUsd || '', deal_status: d.dealStatus || 'current',
      notes: d.notes || '',
    });
    setEditing(d.id);
  };
  const cancel = () => { setEditing(null); setForm(blank); };

  const numOrNull = (v) => (String(v).trim() === '' ? null : Number(v));

  const submit = async () => {
    if (!form.property_name.trim() || busy) return;
    setBusy(true);
    const payload = {
      brandId: brand.id, brandName: brand.brand, category: brand.category || null,
      dealSource: 'Others',
      propertyName: form.property_name.trim(),
      propertySegment: form.property_segment || null,
      geographicScope: form.geographic_scope || null,
      designation: form.designation.trim() || null,
      startYear: numOrNull(form.start_year),
      endYear: numOrNull(form.end_year),
      estValueUsd: numOrNull(form.est_value_usd),
      dealStatus: form.deal_status,
      notes: form.notes.trim() || null,
    };
    try {
      if (editing === 'new') {
        await window.IPSEM_API.createDeal(payload);
        onToast && onToast('Sponsorship added');
      } else {
        await window.IPSEM_API.updateDeal(editing, payload);
        onToast && onToast('Sponsorship updated');
      }
      cancel();
      load();
    } catch (e) {
      onToast && onToast(e.message || 'Save failed');
    } finally {
      setBusy(false);
    }
  };

  const remove = async (d) => {
    const ok = await confirm({
      title: `Remove ${d.propertyName || 'this sponsorship'}?`,
      message: `This deletes the sponsorship from ${brand.brand}'s record and from the Sponsorship Data library.`,
      confirmLabel: 'Remove',
      danger: true,
    });
    if (!ok) return;
    try {
      await window.IPSEM_API.deleteDeal(d.id);
      onToast && onToast('Sponsorship removed');
      load();
    } catch (e) {
      onToast && onToast(e.message || 'Delete failed');
    }
  };

  const setStatus = async (d, status) => {
    setDeals((cur) => cur.map((x) => x.id === d.id ? { ...x, dealStatus: status } : x));
    try { await window.IPSEM_API.updateDeal(d.id, { dealStatus: status }); }
    catch (e) { onToast && onToast(e.message || 'Update failed'); load(); }
  };

  const fieldStyle = {
    width: '100%', height: 32, padding: '0 8px', fontSize: 12.5,
    border: '1px solid var(--hairline-strong)', borderRadius: 6,
    background: 'var(--card)', color: 'var(--ink)',
  };

  const list = deals || [];
  const current = list.filter((d) => (d.dealStatus || 'current') === 'current');
  const past = list.filter((d) => d.dealStatus === 'past');

  const EditForm = () => (
    <div className="card card-pad" style={{ background: 'var(--card-alt)', marginBottom: 14 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 8 }}>
        <input style={fieldStyle} placeholder="Property / partner *" value={form.property_name} onChange={(e) => set('property_name', e.target.value)} autoFocus />
        <input style={fieldStyle} list="deal-segments" placeholder="Sport / segment" value={form.property_segment} onChange={(e) => set('property_segment', e.target.value)} />
        <datalist id="deal-segments">{DEAL_SEGMENTS.map((s) => <option key={s} value={s} />)}</datalist>
        <select style={fieldStyle} value={form.geographic_scope} onChange={(e) => set('geographic_scope', e.target.value)}>
          <option value="">Scope…</option>
          {DEAL_SCOPES.map((s) => <option key={s} value={s}>{s}</option>)}
        </select>
        <input style={fieldStyle} placeholder="Designation (e.g. Official Kit Partner)" value={form.designation} onChange={(e) => set('designation', e.target.value)} />
        <input style={fieldStyle} type="number" placeholder="Start year" value={form.start_year} onChange={(e) => set('start_year', e.target.value)} />
        <input style={fieldStyle} type="number" placeholder="End year (leave blank if ongoing)" value={form.end_year} onChange={(e) => set('end_year', e.target.value)} />
        <input style={fieldStyle} type="number" placeholder="Est. value / season (USD only)" value={form.est_value_usd} onChange={(e) => set('est_value_usd', e.target.value)} />
        <select style={fieldStyle} value={form.deal_status} onChange={(e) => set('deal_status', e.target.value)}>
          <option value="current">Current</option>
          <option value="past">Past</option>
        </select>
        <input style={{ ...fieldStyle, gridColumn: '1 / -1' }} placeholder="Notes (e.g. original currency if not USD)" value={form.notes} onChange={(e) => set('notes', e.target.value)} />
      </div>
      <div className="row gap-2" style={{ marginTop: 10, justifyContent: 'flex-end' }}>
        <button className="btn btn-sm" onClick={cancel}>Cancel</button>
        <button className="btn btn-sm btn-primary" onClick={submit} disabled={!form.property_name.trim() || busy}>
          {editing === 'new' ? 'Add sponsorship' : 'Save changes'}
        </button>
      </div>
    </div>
  );

  const Rows = ({ items }) => (
    <tbody>
      {items.map((d) => (
        <tr key={d.id}>
          {/* data-label drives the stacked-card transform below 560px (see
              .tbl td[data-label]::before in index.html) — without it the
              columns rendered as unlabelled values on a phone. */}
          <td data-label="Property / partner">
            <span style={{ fontWeight: 500, overflowWrap: 'anywhere' }}>{d.propertyName}</span>
            {d.designation ? (
              <div className="text-xs text-3" style={{ marginTop: 2, overflowWrap: 'anywhere' }}>{d.designation}</div>
            ) : null}
          </td>
          <td className="text-2" data-label="Sport / segment">{d.propertySegment || <span className="text-4">—</span>}</td>
          <td className="text-2 text-xs" data-label="Scope">{d.geographicScope || <span className="text-4">—</span>}</td>
          <td className="text-2 mono text-xs" data-label="Years" style={{ whiteSpace: 'nowrap' }}>{fmtDealYears(d.startYear, d.endYear, d.dealStatus)}</td>
          <td className="text-2 mono text-xs" data-label="Est. value" style={{ whiteSpace: 'nowrap' }}>{d.estValueUsd ? `$${Number(d.estValueUsd).toLocaleString()}` : <span className="text-4">—</span>}</td>
          <td data-label="Status" onClick={(e) => e.stopPropagation()}>
            <select value={d.dealStatus || 'current'} onChange={(e) => setStatus(d, e.target.value)} className="field" style={{ height: 24, padding: '0 4px', fontSize: 11.5 }}>
              <option value="current">Current</option>
              <option value="past">Past</option>
            </select>
          </td>
          <td style={{ textAlign: 'right', whiteSpace: 'nowrap' }}>
            <button className="btn btn-icon btn-ghost" onClick={() => openEdit(d)} title="Edit">
              <window.I.edit size={12} stroke="var(--ink-4)" />
            </button>
            <button className="btn btn-icon btn-ghost" onClick={() => remove(d)} title="Remove">
              <window.I.x size={12} stroke="var(--ink-4)" />
            </button>
          </td>
        </tr>
      ))}
    </tbody>
  );

  return (
    <Card lg>
      <div className="row" style={{ marginBottom: 12, justifyContent: 'space-between' }}>
        <div className="eyebrow">Sponsorships & partnerships</div>
        <div className="row gap-2">
          <span className="text-xs text-3">{list.length} on record</span>
          <button className="btn btn-sm" onClick={editing ? cancel : openNew}>
            <window.I.plus size={12} />
            {editing ? 'Close' : 'Add'}
          </button>
        </div>
      </div>

      {error ? (
        <div className="text-xs" style={{ color: 'var(--negative)', marginBottom: 10 }}>
          {error}{/brand_id|column/.test(error) ? ' — run MIGRATION_sponsorship_deals_link.sql in Supabase.' : ''}
        </div>
      ) : null}

      {editing ? EditForm() : null}

      {deals === null ? (
        <div className="text-3 text-xs" style={{ paddingTop: 4 }}>Loading sponsorships…</div>
      ) : list.length === 0 ? (
        <div className="text-3 text-xs" style={{ paddingTop: 4 }}>No sponsorships on record yet. Click "Add" to enter one, or re-research the brand.</div>
      ) : (
        // Percentage widths so the columns share whatever the card gives them
        // instead of demanding a fixed 846px; .tbl-scroll is the safety net at
        // tablet widths, where scrolling the table beats bleeding out of it.
        <div className="tbl-scroll">
          <table className="tbl tbl-compact">
            <thead>
              <tr>
                <th style={{ width: '26%' }}>Property / partner</th>
                <th style={{ width: '18%' }}>Sport / segment</th>
                <th style={{ width: '14%' }}>Scope</th>
                <th style={{ width: '12%' }}>Years</th>
                <th style={{ width: '14%' }}>Est. value</th>
                <th style={{ width: 96 }}>Status</th>
                <th style={{ width: 68 }}></th>
              </tr>
            </thead>
            {current.length ? Rows({ items: current }) : null}
            {past.length ? Rows({ items: past }) : null}
          </table>
        </div>
      )}
      {confirmEl}
    </Card>
  );
}

// NotesPanel now lives in ui.jsx so every screen can mount it (and so it can
// carry @mentions + notifications). See window.NotesPanel.

// ─── Growth intel (brand.growthIntel) ─────────────────────────────────────
// The go/no-go field. "Raised $45m, indicating expansion" is what this replaces:
// each row carries what the money buys, which market it lands in, and when — plus
// an honest against-list, because a brand two years into a rival's deal is a pass
// and the team should learn that here rather than on the call.
function hasGrowthIntel(brand) {
  const g = brand.growthIntel;
  if (!g || typeof g !== 'object') return false;
  const sig = g.decisionSignals || {};
  return !!(
    (g.funding || []).length || (g.expansion || []).length || (g.investments || []).length
    || g.commercialMomentum
    || (sig.for || []).length || (sig.against || []).length || (sig.openQuestions || []).length
  );
}

function GrowthIntel({ brand }) {
  const g = brand.growthIntel || {};
  const funding = g.funding || [];
  const expansion = g.expansion || [];
  const investments = g.investments || [];
  const sig = g.decisionSignals || {};

  const meta = (parts) => {
    const kept = parts.filter(Boolean);
    return kept.length ? <span className="text-3"> · {kept.join(' · ')}</span> : null;
  };

  const bullets = (items, label, tone) => (items || []).length ? (
    <div className="col gap-1">
      <div className="eyebrow" style={tone ? { color: `var(--${tone})` } : undefined}>{label}</div>
      {items.map((t, i) => (
        <div key={i} className="text-2" style={{ lineHeight: 1.55 }}>• {t}</div>
      ))}
    </div>
  ) : null;

  return (
    <div className="col gap-3">
      {funding.length ? (
        <div className="col gap-2">
          <div className="eyebrow">Funding &amp; capital</div>
          {funding.map((f, i) => (
            <div key={i} className="col gap-1">
              <div className="text-2">
                <span style={{ fontWeight: 600 }}>{[f.round, f.amount].filter(Boolean).join(' — ') || 'Round'}</span>
                {meta([f.date, f.valuation && `valued ${f.valuation}`, f.lead_investor && `led by ${f.lead_investor}`])}
              </div>
              {f.use_of_proceeds ? (
                <div className="text-2" style={{ lineHeight: 1.55 }}>
                  <span className="text-3">Money goes to: </span>{f.use_of_proceeds}
                </div>
              ) : (
                <div className="text-xs text-3" style={{ fontStyle: 'italic' }}>
                  Use of proceeds not reported — worth asking on the call.
                </div>
              )}
              {(f.target_markets || []).length ? (
                <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
                  {f.target_markets.map((mk, j) => <Pill key={j} kind="accent">{mk}</Pill>)}
                </div>
              ) : null}
              {(f.investors || []).length ? (
                <div className="text-xs text-3">Investors: {f.investors.join(', ')}</div>
              ) : null}
            </div>
          ))}
        </div>
      ) : null}

      {expansion.length ? (
        <div className="col gap-1">
          <div className="eyebrow">Expansion</div>
          {expansion.map((x, i) => (
            <div key={i} className="text-2" style={{ lineHeight: 1.55 }}>
              <span style={{ fontWeight: 600 }}>{x.market || x.what}</span>
              {x.market && x.what ? <span> — {x.what}</span> : null}
              {meta([x.scale, x.timing])}
            </div>
          ))}
        </div>
      ) : null}

      {investments.length ? (
        <div className="col gap-1">
          <div className="eyebrow">Investments</div>
          {investments.map((x, i) => (
            <div key={i} className="text-2" style={{ lineHeight: 1.55 }}>
              <span style={{ fontWeight: 600 }}>{x.what}</span>
              {meta([x.amount, x.area, x.timing])}
            </div>
          ))}
        </div>
      ) : null}

      {g.commercialMomentum ? (
        <div className="col gap-1">
          <div className="eyebrow">Momentum</div>
          <div className="text-2" style={{ lineHeight: 1.6 }}>{g.commercialMomentum}</div>
        </div>
      ) : null}

      {bullets(sig.for, 'Reasons to pursue', 'positive')}
      {bullets(sig.against, 'Reasons to hold off', 'pending')}
      {bullets(sig.openQuestions, 'Ask on the call')}
    </div>
  );
}

// ─── Footprint (brand.markets) ────────────────────────────────────────────
// Written by the markets research pass. Exposure and market focus are still
// stored as prose too — these components render the structured version when it
// is there, and the prose is the fallback for brands researched before it
// existed (or hand-edited since).
//
// Every entry carries a `basis`: 'source' means a searched source supports it,
// 'knowledge' means it came from the model's own knowledge of the brand. The two
// are shown apart on purpose — a country list is only useful if the reader can
// see which half of it is checkable.
function hasMarketData(brand) {
  const m = brand.markets;
  return !!(m && typeof m === 'object' && ((m.countries || []).length || m.countryCount));
}

const COUNT_BASIS_LABEL = {
  reported: 'Stated in sources',
  counted: 'Counted from sources',
  knowledge: 'Known brand footprint',
  unknown: 'Not established',
};

// cap 60 matches ENUMERATE_COUNTRIES_UP_TO in researcher.py: below that the list
// is the answer and gets shown in full, above it the brand is near-global and the
// research pass deliberately returns a partial list of the markets that matter.
function MarketChips({ items, cap = 60 }) {
  const [showAll, setShowAll] = React.useState(false);
  const shown = showAll ? items : items.slice(0, cap);
  return (
    <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
      {shown.map((c, i) => (
        <span
          key={`${c.name}-${i}`}
          className="pill"
          title={[c.region, c.presence].filter(Boolean).join(' · ')}
          style={{ background: 'transparent', border: '1px solid var(--hairline)', color: 'var(--ink-2)' }}
        >
          {c.name}
        </span>
      ))}
      {items.length > shown.length ? (
        <button className="btn btn-sm btn-ghost" onClick={() => setShowAll(true)}>
          +{items.length - shown.length} more
        </button>
      ) : null}
    </div>
  );
}

function MarketExposure({ brand }) {
  const m = brand.markets || {};
  const countries = m.countries || [];
  const sourced = countries.filter((c) => c.basis === 'source');
  const known = countries.filter((c) => c.basis !== 'source');
  const planned = m.plannedMarkets || [];

  return (
    <div className="col gap-2">
      <div className="row gap-2" style={{ flexWrap: 'wrap', alignItems: 'baseline' }}>
        <span style={{ fontSize: 15, fontWeight: 600 }}>
          {m.countryCount
            ? `${m.countBasis === 'counted' ? 'At least ' : m.countBasis === 'knowledge' ? '~' : ''}${m.countryCount} ${m.countryCount === 1 ? 'country' : 'countries'}`
            : 'Country count not established'}
        </span>
        <Pill kind="muted">{COUNT_BASIS_LABEL[m.countBasis] || COUNT_BASIS_LABEL.unknown}</Pill>
        {m.confidence === 'low' ? <Pill kind="pending">Low confidence</Pill> : null}
        <CitationsFor brand={brand} anchor="exposure" />
      </div>

      {m.countNote ? <div className="text-xs text-3">{m.countNote}</div> : null}

      {sourced.length ? (
        <div className="col gap-1">
          <div className="text-xs text-3">Named in sources ({sourced.length})</div>
          <MarketChips items={sourced} />
        </div>
      ) : null}

      {known.length ? (
        <div className="col gap-1">
          <div className="text-xs text-3">From known brand footprint, not in the sources searched ({known.length})</div>
          <MarketChips items={known} />
        </div>
      ) : null}

      {!countries.length && (m.regions || []).length ? (
        <div className="text-2">Regions: {m.regions.join(', ')}</div>
      ) : null}

      {m.channels ? <div className="text-2">{m.channels}</div> : null}

      {planned.length ? (
        <div className="text-2">
          <span className="text-3">Announced, not yet open: </span>
          {planned.map((p) => p.name).join(', ')}
        </div>
      ) : null}

      {m.notes ? <div className="text-xs text-3" style={{ whiteSpace: 'pre-line' }}>{m.notes}</div> : null}
    </div>
  );
}

// The panel the team actually pitches against: which markets carry the brand's
// business, and which ones it has said it wants next. Core markets are numbered
// because rank carries meaning here — #1 is the market a property has to speak to.
function MarketFocus({ brand }) {
  const m = brand.markets || {};
  const key = m.keyMarkets || [];
  const growth = m.growthMarkets || [];
  const core = key.filter((k) => k.priority !== 'secondary');
  const also = key.filter((k) => k.priority === 'secondary');

  const row = (x, i, opts = {}) => (
    <div key={`${x.name}-${i}`} className="row gap-2" style={{ alignItems: 'baseline', lineHeight: 1.55 }}>
      {opts.rank ? (
        <span className="mono text-xs text-3" style={{ minWidth: 14, textAlign: 'right' }}>{x.rank || i + 1}</span>
      ) : null}
      <div className="text-2" style={{ minWidth: 0 }}>
        <span style={{ fontWeight: 600 }}>{x.name}</span>
        {x.timing ? <span className="text-3"> · {x.timing}</span> : null}
        {x.evidence ? <span className="text-3"> — {x.evidence}</span> : null}
        {x.basis !== 'source' ? <span className="text-3" style={{ fontStyle: 'italic' }}> (brand knowledge)</span> : null}
      </div>
    </div>
  );

  return (
    <div className="col gap-3">
      {m.marketFocusSummary ? (
        <div className="text-2" style={{ lineHeight: 1.6 }}>
          {m.marketFocusSummary}
          <CitationsFor brand={brand} anchor="market_focus" />
        </div>
      ) : null}

      {core.length ? (
        <div className="col gap-1">
          <div className="row gap-1" style={{ alignItems: 'baseline' }}>
            <div className="eyebrow">Core markets</div>
            {!m.marketFocusSummary ? <CitationsFor brand={brand} anchor="market_focus" /> : null}
          </div>
          {core.map((x, i) => row(x, i, { rank: true }))}
        </div>
      ) : null}

      {also.length ? (
        <div className="col gap-1">
          <div className="eyebrow">Also significant</div>
          {also.map((x, i) => row(x, i, { rank: true }))}
        </div>
      ) : null}

      {growth.length ? (
        <div className="col gap-1">
          <div className="eyebrow">Stated growth priorities</div>
          {growth.map((x, i) => row(x, i))}
        </div>
      ) : (key.length ? (
        <div className="text-xs text-3">No stated growth markets found in the sources searched.</div>
      ) : null)}

      {m.confidence === 'low' ? (
        <div className="text-xs text-3" style={{ fontStyle: 'italic' }}>
          Evidence for these markets is thin — verify before it goes in a proposal.
        </div>
      ) : null}
    </div>
  );
}

function CitationsFor({ brand, anchor }) {
  if (!brand.citations) return null;
  const matches = brand.citations.filter((c) => (c.anchors || []).includes(anchor));
  if (!matches.length) return null;
  return (
    <span>
      {matches.map((c) => <Cite key={c.id} n={c.id} title={c.text} />)}
    </span>
  );
}

function buildEditForm(brand) {
  return {
    description: brand.description || '',
    coreValues: Array.isArray(brand.coreValues) ? brand.coreValues.join(', ') : (brand.coreValues || ''),
    hq: brand.hq || '',
    exposure: brand.exposure || '',
    marketFocus: brand.marketFocus || '',
    spending: brand.spending || '',
    competitors: (brand.competitors || []).join(', '),
  };
}

function BR_EditField({ label, value, onChange, multiline }) {
  return (
    <div>
      <div className="eyebrow" style={{ marginBottom: 6 }}>{label}</div>
      {multiline ? (
        <textarea
          value={value}
          onChange={(e) => onChange(e.target.value)}
          rows={3}
          style={{
            width: '100%', padding: '10px 12px',
            border: '1px solid var(--hairline-strong)', borderRadius: 6,
            font: 'inherit', fontSize: 13.5, lineHeight: 1.55,
            background: 'var(--card)', color: 'var(--ink)', resize: 'vertical',
          }}
        />
      ) : (
        <input
          value={value}
          onChange={(e) => onChange(e.target.value)}
          style={{
            width: '100%', padding: '8px 12px',
            border: '1px solid var(--hairline-strong)', borderRadius: 6,
            font: 'inherit', fontSize: 13.5,
            background: 'var(--card)', color: 'var(--ink)',
          }}
        />
      )}
    </div>
  );
}

window.BrandResearch = BrandResearch;
