// Properties — library of IPSEM rightsholders/properties.
// Each property has rich detail: markets, audience, sponsors, exclusions, available rights, approval rules.
// User can add/edit (mock).

function PropertiesLibrary({ onToast, propVersion, onReloadProperties, brands, onNavigate, onApprovalsChanged }) {
  // PROPERTIES is the live DB-backed list (loaded into window.IPSEM_DATA by app.jsx).
  const allRaw = window.IPSEM_DATA.PROPERTIES;
  const [showArchived, setShowArchived] = React.useState(false);
  const activeProps   = allRaw.filter((p) => !p.archived);
  const archivedProps = allRaw.filter((p) => p.archived);
  const all = showArchived ? archivedProps : activeProps;  // currently displayed list

  const [selectedId, setSelectedId] = React.useState(all[0] ? all[0].id : null);
  const [editing, setEditing] = React.useState(false);
  const [showNew, setShowNew] = React.useState(false);
  const [showParse, setShowParse] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [researchingId, setResearchingId] = React.useState(null);  // property id being researched
  const [confirm, confirmEl] = useConfirm();

  // Keep a valid selection within the currently-displayed list.
  React.useEffect(() => {
    if (!all.find((p) => p.id === selectedId) && all[0]) setSelectedId(all[0].id);
  }, [propVersion, showArchived]);

  const selected = allRaw.find((p) => p.id === selectedId);

  // Brands contacted for a property — confirmed match + outreach past "not contacted".
  const contactedCountFor = (pid) =>
    (brands || []).filter((b) =>
      (b.confirmedProperties || []).includes(pid) &&
      b.outreachStatus && b.outreachStatus !== 'not_contacted'
    ).length;

  const handleSaveEdit = async (patch) => {
    setBusy(true);
    try {
      await window.IPSEM_API.updateProperty(selectedId, patch);
      await onReloadProperties();
      setEditing(false);
      onToast('Property updated');
    } catch (e) {
      console.error('updateProperty failed:', e);
      onToast('Could not save property');
    } finally {
      setBusy(false);
    }
  };

  const handleNew = async (prop) => {
    setBusy(true);
    try {
      await window.IPSEM_API.createProperty(prop);
      await onReloadProperties();
      setShowNew(false);
      setSelectedId(prop.id);
      onToast(`${prop.name} added`);
    } catch (e) {
      console.error('createProperty failed:', e);
      onToast('Could not add property');
    } finally {
      setBusy(false);
    }
  };

  const handleDelete = async () => {
    if (!selected) return;
    const ok = await confirm({
      title: `Delete ${selected.name}?`,
      message: 'This removes the property from the database and from AI matching. Consider archiving instead if you may need it again.',
      confirmLabel: 'Delete',
      danger: true,
    });
    if (!ok) return;
    setBusy(true);
    try {
      await window.IPSEM_API.deleteProperty(selected.id);
      await onReloadProperties();
      const remaining = window.IPSEM_DATA.PROPERTIES;
      setSelectedId(remaining[0] ? remaining[0].id : null);
      setEditing(false);
      onToast('Property deleted');
    } catch (e) {
      console.error('deleteProperty failed:', e);
      onToast('Could not delete property');
    } finally {
      setBusy(false);
    }
  };

  const handleArchive = async (id, archived) => {
    setBusy(true);
    try {
      await window.IPSEM_API.updateProperty(id, { archived });
      await onReloadProperties();
      onToast(archived ? 'Property archived' : 'Property restored');
    } catch (e) {
      console.error('archive failed:', e);
      onToast('Could not update property');
    } finally {
      setBusy(false);
    }
  };

  // Non-destructive top-up: adds standard properties the database is missing
  // (FEI, ISU after the July 2026 review) and fills in disciplines/engagement
  // type where they are still at their defaults. Never touches an edited field.
  const handleSync = async () => {
    setBusy(true);
    try {
      const res = await window.IPSEM_API.syncProperties();
      await onReloadProperties();
      const added = (res && res.added) || [];
      const updated = (res && res.updated) || [];
      if (!added.length && !updated.length) onToast('Portfolio already up to date');
      else onToast(`Portfolio synced — ${added.length} added, ${updated.length} updated`);
    } catch (e) {
      console.error('syncProperties failed:', e);
      onToast('Could not sync the portfolio');
    } finally {
      setBusy(false);
    }
  };

  // Re-apply the hand-written geographic market detail (backend/property_geography.py).
  // Not AI — this is the IPSEM desk's own text. Overwrites key markets,
  // geographic reach and the market-by-market briefing on every property that
  // has an entry on file; leaves every other field alone.
  const handleGeography = async () => {
    const ok = await confirm({
      title: 'Refresh market detail on every property?',
      message: 'This rewrites Key markets, Geographic reach and the Market-by-market briefing from the desk-written reference file. Every other field — rights, exclusions, sponsors, approval rules, email pitch — is untouched.',
      confirmLabel: 'Refresh market detail',
    });
    if (!ok) return;
    setBusy(true);
    try {
      const res = await window.IPSEM_API.applyGeography();
      await onReloadProperties();
      const n = ((res && res.updated) || []).length;
      const none = ((res && res.noGeographyOnFile) || []).length;
      onToast(`Market detail refreshed on ${n} propert${n === 1 ? 'y' : 'ies'}`
        + (none ? ` — ${none} user-added propert${none === 1 ? 'y has' : 'ies have'} no entry on file` : ''));
    } catch (e) {
      console.error('applyGeography failed:', e);
      onToast('Could not refresh market detail');
    } finally {
      setBusy(false);
    }
  };

  const handleResearch = async (id) => {
    setResearchingId(id);
    try {
      const updated = await window.IPSEM_API.researchProperty(id);
      await onReloadProperties();
      onToast(updated && updated.id ? `Researched ${updated.name}` : 'No sources found for this property');
    } catch (e) {
      console.error('researchProperty failed:', e);
      onToast('Property research failed — check backend console');
    } finally {
      setResearchingId(null);
    }
  };

  const handleParseSave = async (parsed, mode) => {
    setBusy(true);
    try {
      if (mode === 'update' && selected) {
        await window.IPSEM_API.updateProperty(selected.id, parsed);
        await onReloadProperties();
        onToast(`${parsed.name || selected.name} updated from text`);
      } else {
        const id = (parsed.name || 'property')
          .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 32)
          + '-' + Date.now().toString(36).slice(-4);
        const newProp = { ...parsed, id, archived: false, approvalRules: '', short: parsed.short || parsed.name };
        await window.IPSEM_API.createProperty(newProp);
        await onReloadProperties();
        setSelectedId(id);
        setShowArchived(false);
        onToast(`${parsed.name || 'Property'} added from text`);
      }
      setShowParse(false);
    } catch (e) {
      console.error('parse save failed:', e);
      onToast('Could not save parsed property');
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="page page-cap">
      <SectionHeader
        eyebrow="Library"
        title="Properties"
        sub="These are the IPSEM rightsholders the system uses when matching brands. Edit any field to reshape how matches are scored."
        right={
          <>
            <button
              className="btn"
              onClick={handleSync}
              disabled={busy || researchingId}
              title="Add any standard IPSEM properties missing from this list (e.g. FEI, ISU). Never overwrites your edits."
            >
              <window.I.refresh size={14} className={busy ? 'spin' : ''} />
              Sync portfolio
            </button>
            <button
              className="btn"
              onClick={handleGeography}
              disabled={busy || researchingId}
              title="Rewrite key markets, geographic reach and the market-by-market briefing from the desk-written reference (not AI)"
            >
              <window.I.globe size={14} />
              Refresh market detail
            </button>
            <button className="btn" onClick={() => setShowParse(true)} disabled={busy || researchingId}>
              <window.I.sparkle size={14} />
              Parse from text
            </button>
            <button className="btn" onClick={() => setShowNew(true)} disabled={busy || researchingId}>
              <window.I.plus size={14} />
              Add manually
            </button>
          </>
        }
      />

      {/* Active / Archived toggle */}
      <div className="row gap-2" style={{ marginBottom: 14 }}>
        <div className="toggle-group">
          <button className={!showArchived ? 'is-active' : ''} onClick={() => setShowArchived(false)}>
            Active ({activeProps.length})
          </button>
          <button className={showArchived ? 'is-active' : ''} onClick={() => setShowArchived(true)}>
            Archived ({archivedProps.length})
          </button>
        </div>
      </div>

      <div className="responsive-split">
        <Card pad={false} style={{ position: 'sticky', top: 80, maxHeight: 'calc(100vh - 120px)', overflowY: 'auto' }}>
          {all.length === 0 ? (
            <div className="empty">
              {showArchived ? 'No archived properties.' : (
                <div className="col gap-3" style={{ alignItems: 'center' }}>
                  <div>No active properties yet.</div>
                  <button className="btn btn-primary btn-sm" onClick={() => setShowNew(true)}>
                    <window.I.plus size={13} />
                    Add your first property
                  </button>
                </div>
              )}
            </div>
          ) : null}
          {all.map((p) => (
            <div
              key={p.id}
              onClick={() => { setSelectedId(p.id); setEditing(false); }}
              style={{
                padding: '12px 14px',
                borderBottom: '1px solid var(--hairline)',
                cursor: 'pointer',
                background: selectedId === p.id ? 'var(--accent-soft-bg)' : 'transparent',
                borderLeft: selectedId === p.id ? '3px solid var(--accent)' : '3px solid transparent',
              }}
            >
              <div className="row gap-2">
                <span style={{ fontWeight: 600, fontSize: 13.5 }}>{p.name}</span>
              </div>
              <div className="text-xs text-3" style={{ marginTop: 4 }}>{p.sport}</div>
              <div className="row gap-1" style={{ marginTop: 6, flexWrap: 'wrap' }}>
                {p.engagementType && p.engagementType !== 'ongoing' ? (
                  <Pill kind={engagementMeta(p.engagementType).kind} title={engagementMeta(p.engagementType).desc}>
                    {engagementMeta(p.engagementType).short}
                  </Pill>
                ) : null}
                {(p.disciplines || []).length ? (
                  <span className="pill" style={{ background: 'var(--card-alt)', color: 'var(--ink-3)', fontSize: 10.5 }}>
                    {(p.disciplines || []).length} disciplines
                  </span>
                ) : null}
                <span className="pill" style={{ background: 'var(--card-alt)', color: 'var(--ink-3)', fontSize: 10.5 }}>
                  {(p.keyMarkets || []).length} markets
                </span>
                <span className="pill" style={{ background: 'var(--card-alt)', color: 'var(--ink-3)', fontSize: 10.5 }}>
                  {(p.availableRights || []).length} assets
                </span>
                {contactedCountFor(p.id) ? (
                  <span className="pill" style={{ background: 'var(--accent-soft-bg)', color: 'var(--accent)', fontSize: 10.5 }}>
                    {contactedCountFor(p.id)} contacted
                  </span>
                ) : null}
              </div>
            </div>
          ))}
        </Card>

        {selected ? (
          <div className="col gap-3">
            <Card lg>
              <div className="row gap-3" style={{ alignItems: 'flex-start' }}>
                <div className="grow">
                  <div className="eyebrow" style={{ marginBottom: 4 }}>{selected.sport}</div>
                  <div className="row gap-2" style={{ alignItems: 'center', flexWrap: 'wrap' }}>
                    <h2 className="h2" style={{ margin: 0 }}>{selected.name}</h2>
                    {selected.archived ? <Pill kind="muted">Archived</Pill> : null}
                    <Pill kind={engagementMeta(selected.engagementType).kind} title={engagementMeta(selected.engagementType).desc}>
                      {engagementMeta(selected.engagementType).label}
                    </Pill>
                  </div>
                  <div className="text-2 text-sm" style={{ marginTop: 4 }}>{selected.rightsholder}</div>
                </div>
                <div className="row gap-2">
                  {!editing ? (
                    <button
                      className="btn btn-sm"
                      onClick={() => handleResearch(selected.id)}
                      disabled={busy || researchingId}
                      title="Fill factual fields from public sources"
                    >
                      <window.I.sparkle size={13} className={researchingId === selected.id ? 'spin' : ''} />
                      {researchingId === selected.id ? 'Researching…' : 'Research'}
                    </button>
                  ) : null}
                  <button className="btn btn-sm" onClick={() => setEditing((x) => !x)} disabled={busy || researchingId}>
                    <window.I.edit size={13} />
                    {editing ? 'Cancel' : 'Edit'}
                  </button>
                  {!editing ? (
                    selected.archived ? (
                      <button className="btn btn-sm" onClick={() => handleArchive(selected.id, false)} disabled={busy || researchingId} title="Restore to active">
                        <window.I.refresh size={13} />
                        Restore
                      </button>
                    ) : (
                      <button className="btn btn-sm" onClick={() => handleArchive(selected.id, true)} disabled={busy || researchingId} title="Archive — keep the record but exclude from matching">
                        <window.I.ban size={13} />
                        Archive
                      </button>
                    )
                  ) : null}
                  {!editing ? (
                    <button className="btn btn-sm btn-icon btn-ghost" onClick={handleDelete} disabled={busy || researchingId} title="Delete permanently">
                      <window.I.trash size={14} stroke="var(--negative)" />
                    </button>
                  ) : null}
                </div>
              </div>
            </Card>

            {editing ? (
              <PropertyEditor property={selected} onSave={handleSaveEdit} onCancel={() => setEditing(false)} />
            ) : (
              <PropertyDetail property={selected} brands={brands} onNavigate={onNavigate} onToast={onToast} onApprovalsChanged={onApprovalsChanged} />
            )}
          </div>
        ) : null}
      </div>

      {showNew ? <NewPropertyModal onSave={handleNew} onCancel={() => setShowNew(false)} /> : null}
      {showParse ? (
        <ParseModal
          selected={selected}
          onSave={handleParseSave}
          onCancel={() => setShowParse(false)}
          onToast={onToast}
        />
      ) : null}
      {confirmEl}
    </div>
  );
}

// Normalise a sponsor entry (object or legacy string) to a consistent shape.
function normSponsor(s) {
  if (typeof s === 'string') return { sponsor: s, category: '', deal: '', tenure: '', status: 'current' };
  if (s && typeof s === 'object') {
    return {
      sponsor: s.sponsor || s.name || '',
      category: s.category || '',
      deal: s.deal || s.type || '',
      tenure: s.tenure || '',
      status: ['current', 'expiring', 'past'].includes(s.status) ? s.status : 'current',
    };
  }
  return { sponsor: String(s || ''), category: '', deal: '', tenure: '', status: 'current' };
}

const SPONSOR_STATUS_META = {
  current:  { label: 'Current',  kind: 'positive' },
  expiring: { label: 'Expiring', kind: 'pending' },
  past:     { label: 'Past',     kind: 'muted' },
};

function PropertyDetail({ property, brands, onNavigate, onToast, onApprovalsChanged }) {
  const sponsors = (property.existingSponsors || []).map(normSponsor).filter((s) => s.sponsor);
  const takenCategories = [...new Set(sponsors.filter((s) => s.status !== 'past' && s.category).map((s) => s.category))];
  const detailed = sponsors.some((s) => s.deal || s.tenure || s.category);
  const linkedBrands = (brands || []).filter((b) => (b.confirmedProperties || []).includes(property.id));
  const contactedBrands = linkedBrands.filter((b) => b.outreachStatus && b.outreachStatus !== 'not_contacted');

  // Tabs, not one long column: a property can carry 40+ assigned brands, and
  // stacked they pushed the profile, the sponsor table and the exclusions so far
  // down the page that nobody scrolled to them.
  //
  // Sticky across properties and across a reload, because the weekly review
  // walks the portfolio one property at a time looking at the same thing on each
  // — re-picking the tab every time would be the whole meeting.
  const [tab, setTab] = usePersistentState('property-detail-tab', 'profile');
  const TABS = [
    { key: 'profile',  label: 'Profile' },
    { key: 'brands',   label: 'Brands assigned', count: linkedBrands.length },
    { key: 'outreach', label: 'Outreach', count: contactedBrands.length },
  ];
  // A tab written by an older build (or dropped since) must not blank the pane.
  const active = TABS.some((t) => t.key === tab) ? tab : 'profile';

  return (
    <div className="col gap-3">
      {/* Snapshot tiles — markets / audience / sponsors at a glance. Outside the
          tabs: they are the summary of the whole property, not of one view. */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12 }}>
        <MiniStat label="Key markets" value={(property.keyMarkets || []).length} icon="globe" />
        <MiniStat label="Available assets" value={(property.availableRights || []).length} icon="trophy" />
        <MiniStat label="Active sponsors" value={sponsors.filter((s) => s.status !== 'past').length} icon="briefcase" />
        <MiniStat label="Exclusions" value={(property.exclusions || []).length} icon="ban" />
        <MiniStat label="Brands contacted" value={contactedBrands.length} icon="mail" />
      </div>

      <div className="row" style={{ flexWrap: 'wrap' }}>
        <div className="toggle-group">
          {TABS.map((t) => (
            <button
              key={t.key}
              className={active === t.key ? 'is-active' : ''}
              onClick={() => setTab(t.key)}
            >
              {t.label}{t.count !== undefined ? ` (${t.count})` : ''}
            </button>
          ))}
        </div>
      </div>

      {/* Brands we picked for this property, and whether the rightsholder has
          agreed we may approach them. This is what gets exported and sent. */}
      {active === 'brands' ? (
        <PropertyBrandApprovals property={property} onToast={onToast} onNavigate={onNavigate} onApprovalsChanged={onApprovalsChanged} />
      ) : null}

      {/* Outreach for this property — which brands we approached and where they stand */}
      {active === 'outreach' ? (
        <PropertyOutreach linkedBrands={linkedBrands} contactedBrands={contactedBrands} onNavigate={onNavigate} />
      ) : null}

      {active === 'profile' ? (
      <>
      {/* Profile */}
      <Card lg>
        <div className="eyebrow" style={{ marginBottom: 4 }}>Profile</div>
        {(property.disciplines || []).length ? (
          <Definition label="Disciplines">
            <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
              {(property.disciplines || []).map((d, i) => <Pill key={i} kind="accent">{d}</Pill>)}
            </div>
            <div className="text-xs text-3" style={{ marginTop: 6 }}>
              Tag these individually on a brand match — the property stays one relationship.
            </div>
          </Definition>
        ) : null}
        <Definition label="Engagement type">
          <span className="text-sm text-2">
            <strong>{engagementMeta(property.engagementType).label}</strong> — {engagementMeta(property.engagementType).desc}
          </span>
        </Definition>
        <Definition label="Key markets">
          <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
            {(property.keyMarkets || []).map((m, i) => <Pill key={i} kind="muted">{m}</Pill>)}
          </div>
        </Definition>
        <Definition label="Audience profile" value={property.audience?.profile} />
        <Definition label="Reach" value={property.audience?.reach} />
        <Definition label="Geographic reach" value={property.geographicReach} />
        {property.notes ? <Definition label="Positioning" value={property.notes} /> : null}
      </Card>

      {/* Market-by-market briefing — written by property research, so the team
          does not have to google "why does this market matter". */}
      {(property.marketNotes || []).length ? (
        <Card lg>
          <div className="eyebrow row gap-1" style={{ marginBottom: 10 }}>
            <window.I.globe size={12} stroke="var(--ink-3)" />
            Market-by-market briefing
          </div>
          <div className="col gap-2">
            {(property.marketNotes || []).map((m, i) => (
              <div key={i} style={{ padding: '10px 12px', background: 'var(--card-alt)', borderRadius: 8 }}>
                <div className="text-sm" style={{ fontWeight: 600, marginBottom: 2 }}>{m.market}</div>
                <div className="text-sm text-2" style={{ lineHeight: 1.55 }}>{m.why}</div>
              </div>
            ))}
          </div>
        </Card>
      ) : null}

      {/* Current sponsorships — the key "what's taken / what's free" view */}
      <Card lg>
        <div className="row" style={{ justifyContent: 'space-between', marginBottom: 10 }}>
          <div className="eyebrow">Current & recent sponsorships</div>
          {takenCategories.length ? (
            <span className="text-xs text-3">Categories taken: {takenCategories.join(', ')}</span>
          ) : null}
        </div>
        {sponsors.length === 0 ? (
          <div className="text-3 text-sm" style={{ padding: '8px 0' }}>No sponsors on record yet. Click <strong>Research</strong> above to pull current partners.</div>
        ) : detailed ? (
          <div style={{ overflowX: 'auto' }}>
          <table className="tbl" style={{ minWidth: 520 }}>
            <thead>
              <tr>
                <th>Sponsor</th>
                <th style={{ width: 150 }}>Category</th>
                <th style={{ width: 170 }}>Deal</th>
                <th style={{ width: 130 }}>Tenure</th>
                <th style={{ width: 90 }}>Status</th>
              </tr>
            </thead>
            <tbody>
              {sponsors.map((s, i) => (
                <tr key={i}>
                  <td data-label="Sponsor" style={{ fontWeight: 500 }}>{s.sponsor}</td>
                  <td data-label="Category" className="text-sm text-2">{s.category || '—'}</td>
                  <td data-label="Deal" className="text-sm text-2">{s.deal || '—'}</td>
                  <td data-label="Tenure" className="text-xs text-3 mono">{s.tenure || '—'}</td>
                  <td data-label="Status"><Pill kind={SPONSOR_STATUS_META[s.status].kind}>{SPONSOR_STATUS_META[s.status].label}</Pill></td>
                </tr>
              ))}
            </tbody>
          </table>
          </div>
        ) : (
          <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
            {sponsors.map((s, i) => (
              <span key={i} className="pill" style={{ background: 'transparent', border: '1px solid var(--hairline)', color: 'var(--ink-2)' }}>{s.sponsor}</span>
            ))}
          </div>
        )}
      </Card>

      {/* Do / Avoid guidance */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 12 }}>
        <Card lg>
          <div className="eyebrow row gap-1" style={{ marginBottom: 10, color: 'var(--positive)' }}>
            <window.I.check size={12} /> Available to offer
          </div>
          {(property.availableRights || []).length ? (
            <div className="row gap-1" style={{ flexWrap: 'wrap' }}>
              {property.availableRights.map((r, i) => <Pill key={i} kind="accent">{r}</Pill>)}
            </div>
          ) : <span className="text-3 text-sm">No assets listed yet.</span>}
        </Card>
        <Card lg>
          <div className="eyebrow row gap-1" style={{ marginBottom: 10, color: 'var(--attention)' }}>
            <window.I.ban size={12} /> Avoid / category exclusions
          </div>
          {(property.exclusions || []).length ? (
            <div className="col gap-2">
              {property.exclusions.map((e, i) => (
                <div key={i} className="row gap-2 text-sm">
                  <window.I.warn size={12} stroke="var(--attention)" style={{ marginTop: 3, flexShrink: 0 }} />
                  <span>{e}</span>
                </div>
              ))}
            </div>
          ) : <span className="text-3 text-sm">No exclusions recorded.</span>}
        </Card>
      </div>

      {property.approvalRules ? (
        <Card lg>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Approval rules & lead time</div>
          <div className="text-sm text-2" style={{ lineHeight: 1.6 }}>{property.approvalRules}</div>
        </Card>
      ) : null}

      {/* How this property appears in outreach drafts */}
      <Card lg>
        <div className="row" style={{ justifyContent: 'space-between', marginBottom: 8 }}>
          <div className="eyebrow">Outreach emails</div>
          <Pill kind={property.nameInEmails ? 'accent' : 'muted'}>
            {property.nameInEmails ? 'Named in emails' : 'Kept anonymous in emails'}
          </Pill>
        </div>
        {property.emailPitch ? (
          <div className="text-sm text-2" style={{ lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>{property.emailPitch}</div>
        ) : (
          <div className="text-3 text-sm">
            No approved email pitch stored yet. {property.nameInEmails
              ? 'Drafts will describe this property from its profile fields — add a pitch here to control the exact wording and statistics.'
              : 'Drafts refer to this property anonymously (e.g. "a Premier League club").'}
          </div>
        )}
      </Card>
      </>
      ) : null}
    </div>
  );
}

// ─── Outreach per property — who we approached and where each stands ───────
// ─── Brand approvals ───────────────────────────────────────────────────────
// The list of brands we picked for this property, and the rightsholder's answer
// on each. Export sends them the sheet; import reads their answers back.
//
// Loaded from the API rather than from the in-memory brands list: the reason
// text and the approval rows are not in the brands projection the app holds,
// and pushing them into it would grow every page load for one panel.

const APPROVAL_STATES = [
  { value: 'not-raised',  label: 'Not raised',  kind: 'muted',    short: 'Not raised' },
  { value: 'pending',     label: 'Awaiting reply', kind: 'pending', short: 'Awaiting' },
  { value: 'approved',    label: 'Approved',    kind: 'positive', short: 'Approved' },
  { value: 'conditional', label: 'Approved with conditions', kind: 'positive', short: 'Conditional' },
  { value: 'discuss',     label: 'Needs discussion', kind: 'pending', short: 'Discuss' },
  { value: 'rejected',    label: 'Not approved', kind: 'negative', short: 'Not approved' },
];
const approvalMeta = (v) => APPROVAL_STATES.find((s) => s.value === v) || APPROVAL_STATES[0];
// 'not-raised' is a computed state (no row yet), so it is not something you can
// pick — you leave it by exporting the sheet or recording an answer.
const SETTABLE_STATES = APPROVAL_STATES.filter((s) => s.value !== 'not-raised');

function PropertyBrandApprovals({ property, onToast, onNavigate, onApprovalsChanged }) {
  const [rows, setRows] = React.useState(null);   // null = still loading
  const [error, setError] = React.useState('');
  const [busy, setBusy] = React.useState('');     // '' | 'export' | 'import' | brandId
  const [filter, setFilter] = React.useState('all');
  const [openId, setOpenId] = React.useState(null);
  const [showClashes, setShowClashes] = React.useState(false);
  const fileRef = React.useRef(null);

  const load = React.useCallback(async () => {
    setError('');
    try {
      setRows(await window.IPSEM_API.getPropertyBrands(property.id));
    } catch (e) {
      console.error('getPropertyBrands failed:', e);
      setRows([]);
      setError(e.message || 'Could not load the brands for this property.');
    }
  }, [property.id]);

  React.useEffect(() => { setRows(null); setOpenId(null); setFilter('all'); setShowClashes(false); load(); }, [property.id]);

  // Update one row in place. The server is the authority on what it stored, but
  // only for the row that changed — re-reading the whole list here would throw
  // away a reason someone is part-way through typing in another row.
  const patchRow = async (brandId, patch, successMessage) => {
    setBusy(brandId);
    try {
      const saved = await window.IPSEM_API.setBrandApproval(property.id, brandId, patch);
      setRows((cur) => (cur || []).map((r) => r.brandId !== brandId ? r : {
        ...r,
        // Only a status patch changes the status. Saving a reason also creates
        // the row, and the row's stored default ('pending') is not the state
        // this list shows — 'not raised' is the absence of an ask, not of a row.
        status: patch.status !== undefined ? (saved.status || patch.status) : r.status,
        reason: saved.reason !== undefined && saved.reason !== null ? saved.reason : r.reason,
        reasonIsAI: patch.reason !== undefined ? !patch.reason : r.reasonIsAI,
        hasReason: patch.reason !== undefined ? !!patch.reason : r.hasReason,
        // api.js camel-cases every response key, so these are propertyNote /
        // decidedAt / decidedBy here, not the column names.
        propertyNote: saved.propertyNote !== undefined ? (saved.propertyNote || '') : r.propertyNote,
        decidedAt: saved.decidedAt || null,
        decidedBy: saved.decidedBy || null,
      }));
      // A status change alters what Draft Review is allowed to do, so the
      // app-wide refusal list has to be re-read — not just this table.
      if (patch.status !== undefined && onApprovalsChanged) onApprovalsChanged();
      if (successMessage) onToast && onToast(successMessage);
    } catch (e) {
      console.error('setBrandApproval failed:', e);
      onToast && onToast(e.message || 'Could not save that approval');
      await load();  // our optimistic row may now be wrong — resync just this list
    } finally {
      setBusy('');
    }
  };

  // Asking a rightsholder to approve a brand their own rules rule out is the
  // kind of mistake they remember, so it is caught before the file is built —
  // not flagged afterwards, when the sheet has already been sent.
  const clashing = (rows || []).filter((r) => (r.clashes || []).length && r.status !== 'rejected');

  const handleExport = async (skip) => {
    setBusy('export');
    try {
      const { filename } = await window.IPSEM_API.exportPropertyBrands(property.id, skip);
      const held = (skip || []).length;
      onToast && onToast(`Downloaded ${filename}${held ? ` — ${held} brand${held === 1 ? '' : 's'} held back` : ''}`);
      await load();  // the export stamps the included brands as raised
    } catch (e) {
      console.error('exportPropertyBrands failed:', e);
      onToast && onToast(e.message || 'Could not build the approval sheet');
    } finally {
      setBusy('');
    }
  };

  const startExport = () => {
    if (clashing.length) { setShowClashes(true); return; }
    handleExport([]);
  };

  const handleImport = async (file) => {
    if (!file) return;
    setBusy('import');
    try {
      const res = await window.IPSEM_API.importPropertyBrands(property.id, file);
      await load();
      if (onApprovalsChanged) onApprovalsChanged();
      if (res.message) onToast && onToast(res.message);
      else if (!res.applied) onToast && onToast('Nothing to apply — no answered rows matched this property.');
      else {
        const extra = (res.unmatched || []).length
          ? ` · ${res.unmatched.length} row${res.unmatched.length === 1 ? '' : 's'} did not match a brand here`
          : '';
        onToast && onToast(`Recorded ${res.applied} decision${res.applied === 1 ? '' : 's'}${extra}`);
      }
      if ((res.unmatched || []).length) console.warn('Unmatched approval rows:', res.unmatched);
    } catch (e) {
      console.error('importPropertyBrands failed:', e);
      onToast && onToast(e.message || 'Could not read that file');
    } finally {
      setBusy('');
      if (fileRef.current) fileRef.current.value = '';  // let the same file be re-picked
    }
  };

  const counts = React.useMemo(() => {
    const c = { all: (rows || []).length };
    for (const s of APPROVAL_STATES) c[s.value] = (rows || []).filter((r) => r.status === s.value).length;
    return c;
  }, [rows]);

  const visible = (rows || []).filter((r) => filter === 'all' || r.status === filter);
  const missingReason = (rows || []).filter((r) => !r.hasReason).length;
  const showDisciplines = (rows || []).some((r) => r.disciplines);

  if (rows === null) {
    return (
      <Card lg>
        <div className="eyebrow" style={{ marginBottom: 10 }}>Brands assigned</div>
        <div className="text-3 text-sm" style={{ padding: '8px 0' }}>Loading…</div>
      </Card>
    );
  }

  return (
    <Card lg>
      <div className="row" style={{ justifyContent: 'space-between', marginBottom: 10, gap: 10, flexWrap: 'wrap' }}>
        <div>
          <div className="eyebrow">Brands assigned</div>
          <div className="text-xs text-3" style={{ marginTop: 2 }}>
            Send this list to {property.short || property.name} and ask which brands they will let us approach.
          </div>
        </div>
        <div className="row gap-2">
          <button
            className="btn btn-sm"
            onClick={startExport}
            disabled={!!busy || !rows.length}
            title={rows.length ? 'Download the approval sheet to send to the rightsholder' : 'No brands confirmed for this property yet'}
          >
            <window.I.download size={13} />
            {busy === 'export' ? 'Building…' : 'Export approval sheet'}
          </button>
          <button
            className="btn btn-sm"
            onClick={() => fileRef.current && fileRef.current.click()}
            disabled={!!busy}
            title="Upload the sheet they filled in and returned"
          >
            <window.I.upload size={13} />
            {busy === 'import' ? 'Reading…' : 'Import their answers'}
          </button>
          <input
            ref={fileRef}
            type="file"
            accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            style={{ display: 'none' }}
            onChange={(e) => handleImport(e.target.files && e.target.files[0])}
          />
        </div>
      </div>

      {error ? (
        <div className="text-sm" style={{ color: 'var(--negative)', padding: '8px 0' }}>{error}</div>
      ) : null}

      {rows.length === 0 && !error ? (
        <div className="text-3 text-sm" style={{ padding: '8px 0' }}>
          No brands confirmed for this property yet. Confirm a match in <strong>Property Match</strong> and it will appear here.
        </div>
      ) : null}

      {rows.length ? (
        <>
          {/* Status filter — the counts are the summary, so no separate tiles */}
          <div className="row gap-1" style={{ flexWrap: 'wrap', marginBottom: 10 }}>
            <button
              className={`btn btn-sm${filter === 'all' ? ' btn-primary' : ''}`}
              onClick={() => setFilter('all')}
            >
              All {counts.all}
            </button>
            {APPROVAL_STATES.filter((s) => counts[s.value]).map((s) => (
              <button
                key={s.value}
                className={`btn btn-sm${filter === s.value ? ' btn-primary' : ''}`}
                onClick={() => setFilter(s.value)}
              >
                {s.short} {counts[s.value]}
              </button>
            ))}
          </div>

          {clashing.length ? (
            <div className="text-xs" style={{
              color: 'var(--negative)', background: 'var(--card-alt)',
              border: '1px solid var(--negative)',
              padding: '8px 10px', borderRadius: 6, marginBottom: 10,
            }}>
              {clashing.length} brand{clashing.length === 1 ? '' : 's'} clash with {property.short || property.name}'s own
              rules — an excluded category, a category already sold, or a competitor of an existing partner.
              You will be asked what to do with {clashing.length === 1 ? 'it' : 'them'} when you export.
            </div>
          ) : null}

          {missingReason ? (
            <div className="text-xs" style={{
              color: 'var(--attention)', background: 'var(--card-alt)',
              padding: '8px 10px', borderRadius: 6, marginBottom: 10,
            }}>
              {missingReason} brand{missingReason === 1 ? ' has' : 's have'} no reason written yet — the research engine only
              writes one for properties it suggested itself. Click a row to add one before you export, or the sheet goes out with a blank cell.
            </div>
          ) : null}

          <div style={{ overflowX: 'auto' }}>
            <table className="tbl" style={{ minWidth: 640 }}>
              <thead>
                <tr>
                  <th>Brand</th>
                  <th style={{ width: 140 }}>Category</th>
                  {showDisciplines ? <th style={{ width: 130 }}>Discipline</th> : null}
                  <th>Why it fits</th>
                  <th style={{ width: 170 }}>Approval</th>
                </tr>
              </thead>
              <tbody>
                {visible.map((r) => {
                  const meta = approvalMeta(r.status);
                  const open = openId === r.brandId;
                  return (
                    <React.Fragment key={r.brandId}>
                      <tr
                        onClick={() => setOpenId(open ? null : r.brandId)}
                        style={{ cursor: 'pointer' }}
                        title="Open to edit the reason and read their comment"
                      >
                        <td data-label="Brand">
                          <div className="row gap-1" style={{ alignItems: 'center', flexWrap: 'wrap' }}>
                            <span style={{ fontWeight: 500 }}>{r.brand}</span>
                            {r.status === 'rejected' ? (
                              <Pill kind="negative" title="Outreach is blocked for this brand on this property">Blocked</Pill>
                            ) : null}
                            {(r.clashes || []).length && r.status !== 'rejected' ? (
                              <Pill kind="negative" title={(r.clashes || []).map((c) => c.detail).join(' · ')}>
                                Clash
                              </Pill>
                            ) : null}
                          </div>
                          {(r.clashes || []).length && r.status !== 'rejected' ? (
                            <div className="text-xs" style={{ color: 'var(--negative)' }}>
                              {(r.clashes || []).map((c) => c.detail).join(' · ')}
                            </div>
                          ) : null}
                          {!r.hasReason ? (
                            <div className="text-xs" style={{ color: 'var(--attention)' }}>No reason written</div>
                          ) : null}
                        </td>
                        <td data-label="Category" className="text-sm text-2">{r.category || '—'}</td>
                        {showDisciplines ? (
                          <td data-label="Discipline" className="text-sm text-2">{r.disciplines || '—'}</td>
                        ) : null}
                        <td data-label="Why it fits" className="text-sm text-2" style={{ lineHeight: 1.5 }}>
                          {r.reason
                            ? (r.reason.length > 150 && !open ? r.reason.slice(0, 150) + '…' : r.reason)
                            : <span className="text-3">—</span>}
                        </td>
                        <td data-label="Approval" onClick={(e) => e.stopPropagation()}>
                          <select
                            className="field"
                            style={{ height: 32, padding: '0 8px', fontSize: 12.5, width: '100%' }}
                            value={r.status === 'not-raised' ? '' : r.status}
                            disabled={busy === r.brandId}
                            onChange={(e) => patchRow(r.brandId, { status: e.target.value },
                              `${r.brand} — ${approvalMeta(e.target.value).label.toLowerCase()}`)}
                          >
                            {r.status === 'not-raised' ? <option value="">Not raised</option> : null}
                            {SETTABLE_STATES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
                          </select>
                          {r.propertyNote && !open ? (
                            <div className="text-xs text-3" style={{ marginTop: 3 }}>“{r.propertyNote}”</div>
                          ) : null}
                        </td>
                      </tr>
                      {open ? (
                        <tr>
                          <td colSpan={showDisciplines ? 5 : 4} style={{ background: 'var(--card-alt)' }}>
                            <ApprovalRowDetail
                              row={r}
                              saving={busy === r.brandId}
                              onSaveReason={(text) => patchRow(r.brandId, { reason: text }, 'Reason saved')}
                              onSaveNote={(text) => patchRow(r.brandId, { propertyNote: text }, 'Comment saved')}
                              onOpenBrand={() => onNavigate && onNavigate('research', r.brandId)}
                            />
                          </td>
                        </tr>
                      ) : null}
                    </React.Fragment>
                  );
                })}
              </tbody>
            </table>
          </div>

          {visible.length === 0 ? (
            <div className="text-3 text-sm" style={{ padding: '8px 0' }}>No brands in that state.</div>
          ) : null}
        </>
      ) : null}

      {showClashes ? (
        <ClashGate
          property={property}
          clashing={clashing}
          busy={busy === 'export'}
          onCancel={() => setShowClashes(false)}
          onExport={(skip) => { setShowClashes(false); handleExport(skip); }}
        />
      ) : null}
    </Card>
  );
}

// Shown between clicking Export and the file being built, when at least one
// brand on the sheet contradicts the property's own rules. Two ways forward,
// no default: holding a brand back and asking anyway are both legitimate — a
// category exclusion is usually firm, but "a competitor already sponsors them"
// is sometimes precisely the conversation worth having.
function ClashGate({ property, clashing, busy, onCancel, onExport }) {
  const [skip, setSkip] = React.useState(() => clashing.map((r) => r.brandId));
  const toggle = (id) => setSkip((cur) => cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id]);

  return (
    <div className="drawer-bg" onClick={onCancel}>
      <div className="drawer" onClick={(e) => e.stopPropagation()} style={{ padding: 28, maxWidth: 620 }}>
        <div className="h3" style={{ marginBottom: 6 }}>
          {clashing.length} brand{clashing.length === 1 ? '' : 's'} clash with {property.short || property.name}'s rules
        </div>
        <div className="text-sm text-2" style={{ marginBottom: 14, lineHeight: 1.55 }}>
          Ticked brands are held back from the sheet and are not marked as asked. Untick one to ask anyway.
        </div>

        <div className="col gap-2" style={{ maxHeight: 320, overflowY: 'auto' }}>
          {clashing.map((r) => (
            <label
              key={r.brandId}
              className="row gap-2"
              style={{ padding: '10px 12px', background: 'var(--card-alt)', borderRadius: 8, alignItems: 'flex-start', cursor: 'pointer' }}
            >
              <input
                type="checkbox"
                checked={skip.includes(r.brandId)}
                onChange={() => toggle(r.brandId)}
                style={{ marginTop: 3, flexShrink: 0 }}
              />
              <div className="grow" style={{ minWidth: 0 }}>
                <div style={{ fontWeight: 600, fontSize: 13.5 }}>{r.brand}</div>
                {(r.clashes || []).map((c, i) => (
                  <div key={i} className="text-xs" style={{ color: 'var(--negative)', marginTop: 2 }}>{c.detail}</div>
                ))}
              </div>
            </label>
          ))}
        </div>

        <div className="row gap-2" style={{ marginTop: 18, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
          <button className="btn" onClick={onCancel} disabled={busy}>Cancel</button>
          <button className="btn btn-primary" onClick={() => onExport(skip)} disabled={busy}>
            {busy ? 'Building…' : skip.length
              ? `Export without ${skip.length} brand${skip.length === 1 ? '' : 's'}`
              : 'Export, clashes included'}
          </button>
        </div>
      </div>
    </div>
  );
}

// The expanded row: edit the reason that goes on the sheet, and read or record
// what the rightsholder said back.
function ApprovalRowDetail({ row, saving, onSaveReason, onSaveNote, onOpenBrand }) {
  const [reason, setReason] = React.useState(row.reason || '');
  const [note, setNote] = React.useState(row.propertyNote || '');
  React.useEffect(() => { setReason(row.reason || ''); setNote(row.propertyNote || ''); }, [row.brandId]);

  const reasonChanged = reason !== (row.reason || '');
  const noteChanged = note !== (row.propertyNote || '');
  const box = {
    width: '100%', padding: '8px 10px', border: '1px solid var(--hairline-strong)',
    borderRadius: 6, font: 'inherit', fontSize: 13, lineHeight: 1.5,
    background: 'var(--card)', color: 'var(--ink)', resize: 'vertical',
  };

  return (
    <div className="col gap-3" style={{ padding: '10px 2px' }}>
      <div>
        <div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
          <span className="eyebrow">Why this brand fits — goes on the sheet</span>
          {row.reasonIsAI && row.reason ? (
            <span className="text-xs text-3">From brand research. Edit to override.</span>
          ) : null}
        </div>
        <textarea value={reason} onChange={(e) => setReason(e.target.value)} rows={3} style={box}
          placeholder="One short paragraph the rightsholder will read. Markets, audience, category, timing." />
        <div className="row gap-2" style={{ marginTop: 6 }}>
          <button className="btn btn-sm btn-primary" disabled={!reasonChanged || saving} onClick={() => onSaveReason(reason)}>
            {saving ? 'Saving…' : 'Save reason'}
          </button>
          {reasonChanged ? (
            <button className="btn btn-sm" onClick={() => setReason(row.reason || '')} disabled={saving}>Revert</button>
          ) : null}
          <button className="btn btn-sm btn-ghost" onClick={onOpenBrand}>
            Open brand research
            <window.I.arrowRight size={12} />
          </button>
        </div>
      </div>

      <div>
        <div className="eyebrow" style={{ marginBottom: 4 }}>
          {row.decidedBy ? `Their comment — recorded by ${row.decidedBy}` : 'Their comment'}
        </div>
        <textarea value={note} onChange={(e) => setNote(e.target.value)} rows={2} style={box}
          placeholder="What the rightsholder said. Filled in automatically when you import their sheet." />
        {noteChanged ? (
          <div className="row gap-2" style={{ marginTop: 6 }}>
            <button className="btn btn-sm btn-primary" disabled={saving} onClick={() => onSaveNote(note)}>
              {saving ? 'Saving…' : 'Save comment'}
            </button>
            <button className="btn btn-sm" onClick={() => setNote(row.propertyNote || '')} disabled={saving}>Revert</button>
          </div>
        ) : null}
      </div>

      {row.signal ? (
        <div>
          <div className="eyebrow" style={{ marginBottom: 4 }}>Recent signal — also on the sheet</div>
          <div className="text-sm text-2">{row.signal}</div>
        </div>
      ) : null}
    </div>
  );
}

function PropertyOutreach({ linkedBrands, contactedBrands, onNavigate }) {
  const notContacted = linkedBrands.filter((b) => !b.outreachStatus || b.outreachStatus === 'not_contacted');
  const todayISO = new Date().toISOString().slice(0, 10);

  // Most recent outreach first; brands without a recorded date sink to the bottom.
  const sorted = [...contactedBrands].sort((a, b) =>
    String(b.outreachSentDate || '').localeCompare(String(a.outreachSentDate || '')));

  const personFor = (b) => {
    if (b.outreachContact) return b.outreachContact;
    const rec = b.draft?.recipients || [];
    if (rec.length) return rec.join(', ');
    return '';
  };
  const lastActivity = (b) => {
    const h = b.outreachHistory || [];
    return h.length ? h[h.length - 1] : null;
  };

  return (
    <Card lg>
      <div className="row" style={{ justifyContent: 'space-between', marginBottom: 10 }}>
        <div className="eyebrow">Brands contacted for this property</div>
        {linkedBrands.length ? (
          <span className="text-xs text-3">
            {contactedBrands.length} of {linkedBrands.length} confirmed brand{linkedBrands.length === 1 ? '' : 's'} contacted
          </span>
        ) : null}
      </div>

      {linkedBrands.length === 0 ? (
        <div className="text-3 text-sm" style={{ padding: '8px 0' }}>
          No brands matched to this property yet. Confirm a match in <strong>Property Match</strong> and it will appear here.
        </div>
      ) : (
        <>
          {sorted.length === 0 ? (
            <div className="text-3 text-sm" style={{ padding: '8px 0' }}>
              No outreach sent yet for this property.
            </div>
          ) : (
            <div style={{ overflowX: 'auto' }}>
            <table className="tbl" style={{ minWidth: 520 }}>
              <thead>
                <tr>
                  <th>Brand</th>
                  <th style={{ width: 170 }}>Person contacted</th>
                  <th style={{ width: 110 }}>Email sent</th>
                  <th style={{ width: 130 }}>Stage</th>
                  <th style={{ width: 120 }}>Next follow-up</th>
                  <th>Last activity</th>
                </tr>
              </thead>
              <tbody>
                {sorted.map((b) => {
                  const meta = outreachMeta(b.outreachStatus);
                  const act = lastActivity(b);
                  const overdue = b.nextFollowUp && b.nextFollowUp <= todayISO &&
                    b.outreachStatus !== 'closed' && b.outreachStatus !== 'call_scheduled';
                  return (
                    <tr
                      key={b.id}
                      onClick={() => onNavigate && onNavigate('draft', b.id)}
                      style={{ cursor: onNavigate ? 'pointer' : 'default' }}
                      title="Open in Draft Review"
                    >
                      <td data-label="Brand">
                        <div style={{ fontWeight: 500 }}>{b.brand}</div>
                        {b.category ? <div className="text-xs text-3">{b.category}</div> : null}
                      </td>
                      <td data-label="Person contacted" className="text-sm text-2">{personFor(b) || '—'}</td>
                      <td data-label="Email sent" className="text-xs mono">{b.outreachSentDate ? formatDate(b.outreachSentDate) : '—'}</td>
                      <td data-label="Stage"><span className="text-xs" style={{ fontWeight: 600, color: meta.color }}>{meta.label}</span></td>
                      <td data-label="Next follow-up" className="text-xs mono" style={overdue ? { color: 'var(--attention)', fontWeight: 600 } : undefined}>
                        {b.nextFollowUp ? formatDate(b.nextFollowUp) + (overdue ? ' · due' : '') : '—'}
                      </td>
                      <td data-label="Last activity" className="text-xs text-3">
                        {act ? `${act.note || act.action}${act.date ? ' · ' + formatDate(act.date) : ''}` : '—'}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
            </div>
          )}

          {notContacted.length ? (
            <div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid var(--hairline)' }}>
              <span className="text-xs text-3" style={{ marginRight: 8 }}>Confirmed but not contacted yet:</span>
              <span className="row gap-1" style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
                {notContacted.map((b) => (
                  <span
                    key={b.id}
                    className="pill"
                    onClick={() => onNavigate && onNavigate('draft', b.id)}
                    style={{ background: 'transparent', border: '1px solid var(--hairline)', color: 'var(--ink-2)', cursor: onNavigate ? 'pointer' : 'default' }}
                    title="Open in Draft Review"
                  >
                    {b.brand}
                  </span>
                ))}
              </span>
            </div>
          ) : null}
        </>
      )}
    </Card>
  );
}

function MiniStat({ label, value, icon }) {
  const Ico = icon && window.I[icon];
  return (
    <div className="card card-pad" style={{ padding: '12px 14px' }}>
      <div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
        <span className="eyebrow" style={{ fontSize: 10 }}>{label}</span>
        {Ico ? <Ico size={13} stroke="var(--ink-3)" /> : null}
      </div>
      <span className="mono" style={{ fontSize: 22, fontWeight: 500 }}>{value}</span>
    </div>
  );
}

function PropertyEditor({ property, onSave, onCancel }) {
  // Sponsors edit as one per line: "Sponsor | category | deal | tenure | status"
  const sponsorsToText = (list) => (list || []).map((s) => {
    const n = normSponsor(s);
    return [n.sponsor, n.category, n.deal, n.tenure, n.status].join(' | ');
  }).join('\n');
  const textToSponsors = (txt) => (txt || '').split('\n').map((line) => {
    const parts = line.split('|').map((p) => p.trim());
    if (!parts[0]) return null;
    const status = (parts[4] || 'current').toLowerCase();
    return {
      sponsor: parts[0], category: parts[1] || '', deal: parts[2] || '', tenure: parts[3] || '',
      status: ['current', 'expiring', 'past'].includes(status) ? status : 'current',
    };
  }).filter(Boolean);

  const [form, setForm] = React.useState({
    name: property.name,
    sport: property.sport,
    rightsholder: property.rightsholder,
    // Guarded like disciplines below. A property added via Parse from text is
    // whatever the AI returned, so any of these arrays can be absent — and an
    // unguarded .join() here throws inside useState's initialiser, which blanks
    // the entire Properties screen rather than just failing to open the editor.
    keyMarkets: (property.keyMarkets || []).join(', '),
    audienceProfile: property.audience?.profile || '',
    audienceReach: property.audience?.reach || '',
    geographicReach: property.geographicReach || '',
    existingSponsors: sponsorsToText(property.existingSponsors),
    exclusions: (property.exclusions || []).join(', '),
    availableRights: (property.availableRights || []).join(', '),
    approvalRules: property.approvalRules || '',
    notes: property.notes || '',
    disciplines: (property.disciplines || []).join(', '),
    engagementType: property.engagementType || 'ongoing',
    nameInEmails: !!property.nameInEmails,
    emailPitch: property.emailPitch || '',
  });

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

  return (
    <Card lg>
      <div className="eyebrow" style={{ marginBottom: 14 }}>Edit property</div>
      <div className="col gap-3">
        <EditField label="Name" value={form.name} onChange={(v) => set('name', v)} />
        <EditField label="Sport / category" value={form.sport} onChange={(v) => set('sport', v)} />
        <EditField label="Rightsholder" value={form.rightsholder} onChange={(v) => set('rightsholder', v)} />
        <div>
          <div className="eyebrow" style={{ marginBottom: 6 }}>
            Disciplines (comma separated) — tagged individually on a brand match, e.g. Dressage, Show Jumping, Eventing
          </div>
          <input
            value={form.disciplines}
            onChange={(e) => set('disciplines', e.target.value)}
            placeholder="Leave empty if the property has no sub-disciplines"
            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>
        <div>
          <div className="eyebrow" style={{ marginBottom: 6 }}>Engagement type</div>
          <select
            value={form.engagementType}
            onChange={(e) => set('engagementType', e.target.value)}
            className="field"
            style={{ height: 36, padding: '0 10px', fontSize: 13 }}
          >
            {ENGAGEMENT_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label} — {t.desc}</option>)}
          </select>
        </div>
        <EditField label="Key markets (comma separated)" value={form.keyMarkets} onChange={(v) => set('keyMarkets', v)} />
        <EditField label="Audience profile" value={form.audienceProfile} onChange={(v) => set('audienceProfile', v)} multiline />
        <EditField label="Audience reach" value={form.audienceReach} onChange={(v) => set('audienceReach', v)} />
        <EditField label="Geographic reach" value={form.geographicReach} onChange={(v) => set('geographicReach', v)} multiline />
        <EditField label="Sponsorships — one per line: Sponsor | category | deal | tenure | status (current/expiring/past)" value={form.existingSponsors} onChange={(v) => set('existingSponsors', v)} multiline />
        <EditField label="Category exclusions (comma separated)" value={form.exclusions} onChange={(v) => set('exclusions', v)} />
        <EditField label="Available rights / assets (comma separated)" value={form.availableRights} onChange={(v) => set('availableRights', v)} />
        <EditField label="Approval rules" value={form.approvalRules} onChange={(v) => set('approvalRules', v)} multiline />
        <EditField label="Notes" value={form.notes} onChange={(v) => set('notes', v)} multiline />

        <div style={{ paddingTop: 12, borderTop: '1px solid var(--hairline)' }}>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Outreach emails</div>
          <label className="row gap-2" style={{ cursor: 'pointer', marginBottom: 12, alignItems: 'flex-start' }}>
            <input
              type="checkbox"
              checked={form.nameInEmails}
              onChange={(e) => set('nameInEmails', e.target.checked)}
              style={{ marginTop: 2 }}
            />
            <span className="text-sm">
              <strong>Name this property in outreach emails</strong>
              <div className="text-xs text-3" style={{ marginTop: 2 }}>
                On for federations/event bodies (e.g. Volleyball World, WTT). Off for clubs — drafts then say "a Premier League club" and never reveal the name.
              </div>
            </span>
          </label>
          <EditField
            label='Email pitch — approved credibility paragraph(s) used near-verbatim in drafts (statistics stay exactly as written here)'
            value={form.emailPitch}
            onChange={(v) => set('emailPitch', v)}
            multiline
          />
        </div>
      </div>
      <div className="row gap-2" style={{ marginTop: 18, justifyContent: 'flex-end' }}>
        <button className="btn" onClick={onCancel}>Cancel</button>
        <button className="btn btn-primary" onClick={() => onSave({
          name: form.name,
          sport: form.sport,
          rightsholder: form.rightsholder,
          keyMarkets: form.keyMarkets.split(',').map((s) => s.trim()).filter(Boolean),
          audience: { ...property.audience, profile: form.audienceProfile, reach: form.audienceReach },
          geographicReach: form.geographicReach,
          existingSponsors: textToSponsors(form.existingSponsors),
          exclusions: form.exclusions.split(',').map((s) => s.trim()).filter(Boolean),
          availableRights: form.availableRights.split(',').map((s) => s.trim()).filter(Boolean),
          approvalRules: form.approvalRules,
          notes: form.notes,
          disciplines: form.disciplines.split(',').map((s) => s.trim()).filter(Boolean),
          engagementType: form.engagementType,
          nameInEmails: form.nameInEmails,
          emailPitch: form.emailPitch,
        })}>Save changes</button>
      </div>
    </Card>
  );
}

function 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.5,
            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>
  );
}

function NewPropertyModal({ onSave, onCancel }) {
  const [form, setForm] = React.useState({
    name: '',
    short: '',
    sport: '',
    rightsholder: '',
    keyMarkets: '',
    geographicReach: '',
    availableRights: '',
    disciplines: '',
    engagementType: 'ongoing',
  });
  const set = (k, v) => setForm((cur) => ({ ...cur, [k]: v }));

  const canSave = form.name && form.sport;
  const handleSave = () => {
    onSave({
      id: form.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 20),
      name: form.name,
      short: form.short || form.name,
      sport: form.sport,
      rightsholder: form.rightsholder || form.name,
      keyMarkets: form.keyMarkets.split(',').map((s) => s.trim()).filter(Boolean),
      audience: { profile: '', reach: '' },
      geographicReach: form.geographicReach,
      existingSponsors: [],
      exclusions: [],
      availableRights: form.availableRights.split(',').map((s) => s.trim()).filter(Boolean),
      disciplines: form.disciplines.split(',').map((s) => s.trim()).filter(Boolean),
      engagementType: form.engagementType,
      notes: '',
      approvalRules: '',
    });
  };

  return (
    <div className="drawer-bg" onClick={onCancel}>
      <div className="drawer" onClick={(e) => e.stopPropagation()} style={{ padding: 28 }}>
        <div className="row" style={{ justifyContent: 'space-between', marginBottom: 18 }}>
          <h2 className="h2" style={{ margin: 0 }}>New property</h2>
          <button className="btn btn-icon btn-ghost" onClick={onCancel}>
            <window.I.x size={16} />
          </button>
        </div>
        <div className="col gap-3">
          <EditField label="Name" value={form.name} onChange={(v) => set('name', v)} />
          <EditField label="Short label" value={form.short} onChange={(v) => set('short', v)} />
          <EditField label="Sport / category" value={form.sport} onChange={(v) => set('sport', v)} />
          <EditField label="Rightsholder" value={form.rightsholder} onChange={(v) => set('rightsholder', v)} />
          <EditField label="Disciplines (comma separated, optional)" value={form.disciplines} onChange={(v) => set('disciplines', v)} />
          <div>
            <div className="eyebrow" style={{ marginBottom: 6 }}>Engagement type</div>
            <select
              value={form.engagementType}
              onChange={(e) => set('engagementType', e.target.value)}
              className="field"
              style={{ height: 36, padding: '0 10px', fontSize: 13 }}
            >
              {ENGAGEMENT_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label} — {t.desc}</option>)}
            </select>
          </div>
          <EditField label="Key markets (comma separated)" value={form.keyMarkets} onChange={(v) => set('keyMarkets', v)} />
          <EditField label="Geographic reach" value={form.geographicReach} onChange={(v) => set('geographicReach', v)} multiline />
          <EditField label="Available rights (comma separated)" value={form.availableRights} onChange={(v) => set('availableRights', v)} />
        </div>
        <div className="row gap-2" style={{ marginTop: 26, justifyContent: 'flex-end' }}>
          <button className="btn" onClick={onCancel}>Cancel</button>
          <button className="btn btn-primary" disabled={!canSave} onClick={handleSave}>
            Add property
          </button>
        </div>
      </div>
    </div>
  );
}

// ─── Parse-from-text modal ────────────────────────────────────────────────
function ParseModal({ selected, onSave, onCancel, onToast }) {
  const [text, setText] = React.useState('');
  const [mode, setMode] = React.useState('new');     // 'new' | 'update'
  const [parsing, setParsing] = React.useState(false);
  const [parsed, setParsed] = React.useState(null);   // parsed preview (camelCase)
  const [saving, setSaving] = React.useState(false);
  const fileRef = React.useRef(null);

  const onFile = (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = () => setText(String(reader.result || ''));
    reader.onerror = () => onToast('Could not read that file');
    reader.readAsText(f);
  };

  const runParse = async () => {
    if (!text.trim()) return;
    setParsing(true);
    setParsed(null);
    try {
      const res = await window.IPSEM_API.parseProperty(text);
      setParsed(res);  // camelCased by api.js
    } catch (e) {
      console.error('parse failed:', e);
      onToast('Parse failed — check backend console');
    } finally {
      setParsing(false);
    }
  };

  const save = async () => {
    if (!parsed) return;
    setSaving(true);
    try { await onSave(parsed, mode); }
    finally { setSaving(false); }
  };

  const Row = ({ label, value }) => (
    <div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 10, padding: '6px 0', borderBottom: '1px solid var(--hairline)' }}>
      <span className="eyebrow text-xs" style={{ paddingTop: 2 }}>{label}</span>
      <span className="text-sm text-2" style={{ lineHeight: 1.5 }}>{value || <span className="text-4">—</span>}</span>
    </div>
  );

  return (
    <div className="drawer-bg" onClick={onCancel}>
      <div className="drawer" onClick={(e) => e.stopPropagation()} style={{ padding: 28, width: 'min(640px, 94vw)' }}>
        <div className="row" style={{ justifyContent: 'space-between', marginBottom: 8 }}>
          <h2 className="h2" style={{ margin: 0 }}>Parse property from text</h2>
          <button className="btn btn-icon btn-ghost" onClick={onCancel}><window.I.x size={16} /></button>
        </div>
        <p className="text-2 text-sm" style={{ marginTop: 0, marginBottom: 18, lineHeight: 1.55 }}>
          Paste a brief or research write-up (or upload a .txt/.md file) and AI will extract the property fields. Review the result, then save.
        </p>

        {/* Mode */}
        <div className="toggle-group" style={{ marginBottom: 14 }}>
          <button className={mode === 'new' ? 'is-active' : ''} onClick={() => setMode('new')}>Create new property</button>
          <button
            className={mode === 'update' ? 'is-active' : ''}
            onClick={() => selected && setMode('update')}
            disabled={!selected}
            title={selected ? '' : 'Select a property first to update it'}
          >
            Update {selected ? selected.name : 'selected'}
          </button>
        </div>

        {/* Input */}
        <textarea
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="Paste text about one property here…"
          rows={8}
          style={{
            width: '100%', padding: '10px 12px', border: '1px solid var(--hairline-strong)',
            borderRadius: 6, font: 'inherit', fontSize: 13.5, lineHeight: 1.5,
            background: 'var(--card)', color: 'var(--ink)', resize: 'vertical',
          }}
        />
        <div className="row gap-2" style={{ marginTop: 10, justifyContent: 'space-between' }}>
          <div className="row gap-2">
            <input ref={fileRef} type="file" accept=".txt,.md,text/*" onChange={onFile} style={{ display: 'none' }} />
            <button className="btn btn-sm" onClick={() => fileRef.current && fileRef.current.click()}>
              <window.I.inbox size={13} />
              Upload .txt / .md
            </button>
            {text ? <button className="btn btn-sm btn-ghost" onClick={() => { setText(''); setParsed(null); }}>Clear</button> : null}
          </div>
          <button className="btn btn-primary" onClick={runParse} disabled={!text.trim() || parsing}>
            <window.I.sparkle size={13} className={parsing ? 'spin' : ''} />
            {parsing ? 'Parsing…' : 'Parse'}
          </button>
        </div>

        {/* Preview */}
        {parsed ? (
          <div style={{ marginTop: 20 }}>
            <div className="eyebrow" style={{ marginBottom: 8 }}>Parsed result — review before saving</div>
            <div className="card card-pad" style={{ background: 'var(--card-alt)' }}>
              <Row label="Name" value={parsed.name} />
              <Row label="Sport" value={parsed.sport} />
              <Row label="Rightsholder" value={parsed.rightsholder} />
              <Row label="Key markets" value={(parsed.keyMarkets || []).join(', ')} />
              <Row label="Audience" value={parsed.audience?.profile} />
              <Row label="Reach" value={parsed.audience?.reach} />
              <Row label="Geographic" value={parsed.geographicReach} />
              <Row label="Sponsors" value={(parsed.existingSponsors || []).join(', ')} />
              <Row label="Avail. rights" value={(parsed.availableRights || []).join(', ')} />
              <Row label="Exclusions" value={(parsed.exclusions || []).join(', ')} />
              <Row label="Notes" value={parsed.notes} />
            </div>
            {!parsed.name ? (
              <div className="text-xs" style={{ color: 'var(--negative)', marginTop: 8 }}>
                No property name was extracted — check the text and parse again.
              </div>
            ) : null}
            <div className="row gap-2" style={{ marginTop: 16, justifyContent: 'flex-end' }}>
              <button className="btn" onClick={onCancel} disabled={saving}>Cancel</button>
              <button className="btn btn-primary" onClick={save} disabled={saving || !parsed.name}>
                <window.I.check size={13} />
                {saving ? 'Saving…' : (mode === 'update' ? 'Apply to property' : 'Save as new property')}
              </button>
            </div>
          </div>
        ) : null}
      </div>
    </div>
  );
}

window.PropertiesLibrary = PropertiesLibrary;
