/* Right-side Detail slide-over.
   v2: single-date default + range toggle, product conditional on category=Products,
       sub-product (MegaETH app) conditional on product=MegaETH, sub-tasks for
       multi-day events, SVG header icons, prominent stage chip.
*/

const { useState: useStateDt, useRef: useRefDt, useEffect: useEffectDt, useCallback: useCallbackDt, useMemo: useMemoDt } = React;
const UD = window.mcalUtils;

/* Regex constants hoisted to module scope so they're not re-compiled
   per function call. */
const SCHEME_RE = /^[a-z][a-z0-9+\-.]*:/i;
const MENTION_SEG_RE = /^[A-Za-z0-9_\-.]*$/;
const NON_WORD_RE = /[^A-Za-z0-9_]/;
const URL_PREFIX_RE = /^(https?:\/\/[^\s<>'"]+|www\.[^\s<>'"]+)/i;

/* Normalize a user-entered URL — if they typed bare 'typefully.com/...'
   without a scheme, prepend https:// so the link actually opens. */
function normalizeUrl(raw) {
  if (!raw) return '';
  const v = raw.trim();
  if (!v) return '';
  if (SCHEME_RE.test(v)) return v; // already has scheme
  return 'https://' + v;
}

/* Display a URL compactly — hostname + truncated path. */
function prettyUrl(raw) {
  try {
    const u = new URL(normalizeUrl(raw));
    const host = u.hostname.replace(/^www\./, '');
    const path = (u.pathname + (u.search || '')).replace(/\/$/, '');
    if (!path) return host;
    const short = path.length > 28 ? path.slice(0, 28) + '…' : path;
    return host + short;
  } catch {
    return raw;
  }
}

/* SVG icons — bigger / more legible than the unicode glyphs */
const Ic = {
  ExternalLink: () => (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M6 3H3v10h10v-3" />
      <path d="M9 3h4v4" />
      <path d="M7 9l6-6" />
    </svg>
  ),
  Trash: () => (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M2 4h12" />
      <path d="M5.5 4V2.5h5V4" />
      <path d="M3.5 4l.7 9.2a1 1 0 001 .8h5.6a1 1 0 001-.8L12.5 4" />
      <path d="M6.5 7v5M9.5 7v5" />
    </svg>
  ),
  Close: () => (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
      <path d="M4 4l8 8M12 4l-8 8" />
    </svg>
  ),
  Plus: () => (
    <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
      <path d="M8 3v10M3 8h10" />
    </svg>
  ),
};

/* Stage glyph rendered as SVG — gives precise pie-fill control,
   so 35% / 75% etc are renderable (Unicode only offers 0/25/50/75/100). */
function StageGlyph({ pct, size = 12 }) {
  const p = Math.max(0, Math.min(100, pct || 0));
  const r = 5;
  const cx = 6, cy = 6;
  if (p <= 0) {
    return (
      <svg width={size} height={size} viewBox="0 0 12 12" style={{ display: 'block', flexShrink: 0 }}>
        <circle cx={cx} cy={cy} r={r} fill="none" stroke="currentColor" strokeWidth="1.1" />
      </svg>
    );
  }
  if (p >= 100) {
    return (
      <svg width={size} height={size} viewBox="0 0 12 12" style={{ display: 'block', flexShrink: 0 }}>
        <circle cx={cx} cy={cy} r={r} fill="currentColor" />
      </svg>
    );
  }
  const angle = (p / 100) * 2 * Math.PI;
  const endX = cx + r * Math.sin(angle);
  const endY = cy - r * Math.cos(angle);
  const largeArc = p > 50 ? 1 : 0;
  return (
    <svg width={size} height={size} viewBox="0 0 12 12" style={{ display: 'block', flexShrink: 0 }}>
      <circle cx={cx} cy={cy} r={r} fill="none" stroke="currentColor" strokeWidth="1.1" />
      <path d={`M ${cx} ${cy} L ${cx} ${cy - r} A ${r} ${r} 0 ${largeArc} 1 ${endX} ${endY} Z`} fill="currentColor" />
    </svg>
  );
}

function StageChip({ stageId, options, bigger, full }) {
  const s = UD.stageById(options, stageId);
  if (!s) return null;
  const label = full ? s.name : (s.short || s.name);
  return (
    <span className={'stage-chip' + (bigger ? ' bigger' : '') + (full ? ' full' : '')} data-stage={stageId} title={`Stage: ${s.name}`}>
      <StageGlyph pct={s.fillPct != null ? s.fillPct : ((s.order + 1) / 4) * 100} size={bigger ? 13 : 11} />
      <span>{label}</span>
    </span>
  );
}

function ProductTag({ event, options, bigger }) {
  if (!event.product) return null;
  const prod = options.products.find(p => p.id === event.product);
  if (!prod) return null;
  const sub = event.subproduct ? (options.subproducts || []).find(s => s.id === event.subproduct) : null;
  return (
    <span className="ptag product-tag"
      style={{
        color: prod.color,
        borderColor: prod.color,
        height: bigger ? 16 : 14,
        fontSize: bigger ? 10 : 9,
      }}
      title={sub ? `${prod.name} · ${sub.name}` : prod.name}
    >
      {prod.name.toUpperCase()}
      {sub && <span style={{ opacity: 0.65, marginLeft: 4 }}>/ {sub.name.toUpperCase()}</span>}
    </span>
  );
}

window.mcalStageChip = StageChip;
window.mcalStageGlyph = StageGlyph;
window.mcalProductTag = ProductTag;

/* Glyph-only stage indicator used on tight month-view pills.
   No text — just the SVG circle in its stage color. */
function StageGlyphPill({ stageId, options, size }) {
  const s = UD.stageById(options, stageId);
  if (!s) return null;
  const pct = s.fillPct != null ? s.fillPct : ((s.order + 1) / 4) * 100;
  return (
    <span
      title={`Stage: ${s.name}`}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        flexShrink: 0,
        color: s.color || 'var(--fg-3)',
        marginRight: 1,
      }}
    >
      <StageGlyph pct={pct} size={size || 11} />
    </span>
  );
}
window.mcalStageGlyphPill = StageGlyphPill;

/* Render free text with auto-linked URLs AND @mention highlighting.
   - URLs (http(s):// or bare www.*) → <a target="_blank">
   - @Name where Name matches an owner's full name OR first name →
     bold span colored with the owner's accent.
   Returns an array of strings/elements, or the original text if no
   matches. Empty input returns null. */
function renderRichText(text, options) {
  if (!text) return null;
  const owners = (options && options.owners) || [];
  // Pre-compute lowercase name + first-name pairs once per call so the
  // inner mention loop doesn't re-toLowerCase on every character.
  const ownerKeys = owners.length
    ? owners.map(o => {
        const lc = (o.name || '').toLowerCase();
        const first = lc.split(' ')[0] || '';
        return { o, candidates: lc === first ? [lc] : [lc, first] };
      })
    : null;
  const parts = [];
  let buf = '';
  let keyN = 0;
  const flush = () => { if (buf) { parts.push(buf); buf = ''; } };
  let i = 0;
  while (i < text.length) {
    // URL detection
    const urlMatch = text.slice(i).match(URL_PREFIX_RE);
    if (urlMatch) {
      flush();
      const raw = urlMatch[0];
      const href = raw.startsWith('http') ? raw : 'https://' + raw;
      parts.push(
        <a
          key={'l' + keyN++}
          href={href}
          target="_blank"
          rel="noopener noreferrer"
          className="notes-link"
          onClick={e => e.stopPropagation()}
        >{raw}</a>
      );
      i += raw.length;
      continue;
    }
    // @mention detection — match the longest of (full name, first name)
    // for any owner. Boundary is end-of-string or any non-word char so
    // "@Aaron." or "@Aaron," still resolve.
    if (text[i] === '@' && ownerKeys) {
      const restLc = text.slice(i + 1).toLowerCase();
      let matched = null;
      let matchLen = 0;
      for (const ok of ownerKeys) {
        for (const cand of ok.candidates) {
          if (!cand) continue;
          if (cand.length <= matchLen) continue;
          if (restLc.startsWith(cand)) {
            const next = text[i + 1 + cand.length];
            if (next === undefined || NON_WORD_RE.test(next)) {
              matchLen = cand.length;
              matched = ok.o;
            }
          }
        }
      }
      if (matched) {
        flush();
        // Display the canonical-cased slice the user typed (the lower-
        // case `matchLen` is the same length as the user's slice).
        const displayed = text.slice(i, i + 1 + matchLen);
        parts.push(
          <strong
            key={'m' + keyN++}
            className="mention"
            style={{ color: matched.color, fontWeight: 700 }}
          >{displayed}</strong>
        );
        i += 1 + matchLen;
        continue;
      }
    }
    buf += text[i];
    i++;
  }
  flush();
  return parts.length === 0 ? text : parts;
}
window.mcalRichText = renderRichText;

/* Notes editor — display mode renders auto-linked text + @mentions
   colored by the owner's accent. Click to edit, blur or Cmd/Ctrl+Enter
   to save, Esc to revert. */
function NotesField({ value, onChange, placeholder, rows, options }) {
  const [editing, setEditing] = useStateDt(false);
  const [draft, setDraft] = useStateDt(value || '');
  const taRef = useRefDt(null);

  useEffectDt(() => { if (!editing) setDraft(value || ''); }, [value, editing]);
  // Auto-grow: each render in edit mode, resize textarea to fit content.
  // Set height to 'auto' first so scrollHeight reflects the natural size
  // (otherwise it keeps the previous larger height).
  const autoSize = () => {
    const ta = taRef.current;
    if (!ta) return;
    ta.style.height = 'auto';
    ta.style.height = ta.scrollHeight + 'px';
  };
  useEffectDt(() => {
    if (editing && taRef.current) {
      taRef.current.focus();
      const v = taRef.current.value;
      taRef.current.setSelectionRange(v.length, v.length);
      autoSize();
    }
  }, [editing]);
  useEffectDt(() => { if (editing) autoSize(); }, [draft, editing]);

  if (editing) {
    return (
      <NotesEditor
        taRef={taRef}
        draft={draft}
        setDraft={setDraft}
        value={value}
        rows={rows}
        placeholder={placeholder}
        options={options}
        onChange={onChange}
        setEditing={setEditing}
      />
    );
  }
  const display = value || '';
  return (
    <div
      className={'notes-display' + (display ? '' : ' empty')}
      onClick={() => setEditing(true)}
      title="Click to edit"
    >
      {display ? renderRichText(display, options) : (placeholder || 'Click to add notes…')}
    </div>
  );
}

function NotesEditor({ taRef, draft, setDraft, value, rows, placeholder, options, onChange, setEditing }) {
  const mention = useMentionAutocomplete(taRef, draft, setDraft, options);
  return (
    <span className="mention-input-wrap" style={{ display: 'block' }}>
      <textarea
        ref={taRef}
        className="dt-textarea auto-grow"
        value={draft}
        rows={rows}
        placeholder={placeholder}
        onChange={mention.handleChange}
        onBlur={() => {
          mention.handleBlur();
          if (draft !== (value || '')) onChange(draft);
          setEditing(false);
        }}
        onKeyDown={e => {
          if (mention.handleKeyDown(e)) return;
          if (e.key === 'Escape') { e.preventDefault(); setDraft(value || ''); setEditing(false); }
          if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
            e.preventDefault();
            if (draft !== (value || '')) onChange(draft);
            setEditing(false);
          }
        }}
      />
      {mention.popup}
    </span>
  );
}

function DetailPanel({ event, options, update, remove, close, leaving, openSubtaskId }) {
  const [confirmDelete, setConfirmDelete] = useStateDt(false);
  /* Stable patch reference — only changes when the selected event id
     changes. Keeps section children's prop identity stable across edits
     so segmented toggles + stage rail clicks feel snappy. */
  const eventId = event ? event.id : null;
  const patch = useCallbackDt(p => { if (eventId) update(eventId, p); }, [eventId, update]);
  if (!event) return null;
  const isMultiDay = event.isRange || event.end !== event.start;
  const appsForCategory = UD.subproductsFor(options, event.category);
  // Suppress the App sub-dropdown when category is MegaETH — those events
  // don't pick a specific app. Other app-bearing categories (e.g. MegaMafia)
  // keep their App dropdown.
  const showAppField = appsForCategory.length > 0 && event.category !== 'megaeth';
  // Hide the Product field whenever an App field IS shown — keep it hidden
  // for MegaETH too (events there are about the chain itself, not products).
  const showProductField = !showAppField && event.category !== 'megaeth';

  return (
    <aside className={'detail' + (leaving ? ' is-leaving' : '') + (event && event.isHoliday ? ' is-holiday' : '')}>
      <div className="dt-head">
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="crumb">
            <span className="stamp">EVENT</span>
            <span>
              {UD.shortDate(event.start)}
              {event.end !== event.start && (
                <>
                  <span className="dr-arrow">→</span>
                  {UD.shortDate(event.end)}
                </>
              )}
            </span>
            <span>·</span>
            <StageChip stageId={event.stage} options={options} full />
            {event.tbd && <span style={{ color: 'var(--danger)' }}>TBD</span>}
            {event.needsReview && <span style={{ color: 'var(--needs-review, #9B6FBC)' }}>NEEDS REVIEW</span>}
          </div>
          <EventTitleInput
            value={event.title}
            onCommit={v => patch({ title: v })}
            placeholder="Untitled event"
            autoFocusEmpty={!event.title}
            options={options}
          />
        </div>
        <div className="actions">
          <button className="iconbtn" title="Open URL"
            disabled={!event.url}
            style={{ opacity: event.url ? 1 : 0.4 }}
            onClick={() => event.url && window.open(event.url, '_blank')}><Ic.ExternalLink /></button>
          <button className="iconbtn" title="Delete event"
            onClick={() => setConfirmDelete(true)}><Ic.Trash /></button>
          <button className="iconbtn" title="Close" onClick={close}><Ic.Close /></button>
        </div>
      </div>
      {confirmDelete && (
        <div className="confirm-overlay" onClick={() => setConfirmDelete(false)}>
          <div className="confirm-card" onClick={e => e.stopPropagation()}>
            <div className="confirm-tag">Delete event</div>
            <div className="confirm-title">{event.title || 'Untitled event'}</div>
            <div className="confirm-sub">
              {UD.shortDate(event.start)}
              {event.end !== event.start && (
                <>
                  <span className="dr-arrow">→</span>
                  {UD.shortDate(event.end)}
                </>
              )}
              {' · '}This can't be undone.
            </div>
            <div className="confirm-actions">
              <button className="confirm-btn" onClick={() => setConfirmDelete(false)}>Cancel</button>
              <button className="confirm-btn danger" onClick={() => { setConfirmDelete(false); remove(event.id); }}>Delete</button>
            </div>
          </div>
        </div>
      )}

      <div className="dt-body">
        {/* Dates */}
        <div className="dt-section">
          <h4>Date</h4>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>Type</span>
            <div className="range-toggle">
              <button
                className={!isMultiDay ? 'on' : ''}
                onClick={() => patch({ isRange: false, end: event.start })}
              >Single day</button>
              <button
                className={isMultiDay ? 'on' : ''}
                onClick={() => {
                  // collapse → range default: +2 days
                  const newEnd = event.end > event.start ? event.end : UD.addDays(event.start, 2);
                  patch({ isRange: true, end: newEnd });
                }}
              >Date range</button>
            </div>
          </div>
          {isMultiDay ? (
            <div className="dt-date-block">
              <input type="date" className="dt-input" value={event.start}
                onChange={e => {
                  const newStart = e.target.value;
                  if (!newStart) return;
                  let newEnd = event.end;
                  if (newEnd < newStart) newEnd = newStart;
                  patch({ start: newStart, end: newEnd });
                }} />
              <span className="dt-arrow">→</span>
              <input type="date" className="dt-input" value={event.end} min={event.start}
                onChange={e => patch({ end: e.target.value || event.start })} />
            </div>
          ) : (
            <input type="date" className="dt-input" value={event.start}
              onChange={e => {
                const s = e.target.value;
                if (!s) return;
                patch({ start: s, end: s });
              }} />
          )}
          <div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>Status</span>
            <div className="tbd-toggle">
              <button className={!event.tbd ? 'on' : ''} onClick={() => patch({ tbd: false })}>OK</button>
              <button className={'tbd ' + (event.tbd ? 'on' : '')} onClick={() => patch({ tbd: true })}>TBD</button>
            </div>
          </div>
          <div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>Review</span>
            <div className="needs-review-toggle">
              <button className={!event.needsReview ? 'on' : ''} onClick={() => patch({ needsReview: false })}>OK</button>
              <button className={'nr ' + (event.needsReview ? 'on' : '')} onClick={() => patch({ needsReview: true })}>Needs review</button>
            </div>
          </div>
        </div>

        {/* Sub-tasks — available for both single-day and multi-day events.
            A single-day parent simply pins all its sub-tasks to that one
            date; the user can shift the parent later if needed. */}
        <SubtaskSection event={event} options={options} patch={patch} openSubtaskId={openSubtaskId} />

        {/* Classification */}
        <div className="dt-section">
          <h4>Classification</h4>
          <SelectRow label="Channel"  type="channels"   value={event.channel}  options={options} onChange={v => patch({ channel: v })} />
          <OwnersRow event={event} options={options} onChange={ids => patch({ owners: ids, owner: undefined })} />
          <SelectRow label="Category" type="categories" value={event.category} options={options} onChange={v => {
            const np = { category: v };
            if (UD.subproductsFor(options, v).length === 0) np.subproduct = '';
            // Switching INTO an app-bearing category clears the product
            if (UD.subproductsFor(options, v).length > 0) np.product = '';
            patch(np);
          }} />
          {showProductField && (
            <SelectRow label="Product" type="products" value={event.product} options={options} onChange={v => patch({ product: v })} />
          )}
          {showAppField && (
            <AppRow event={event} options={options} appsForCategory={appsForCategory} onChange={v => patch({ subproduct: v })} />
          )}
          <SelectRow label="Format"   type="formats"    value={event.format}   options={options} onChange={v => patch({ format: v })} />
        </div>

        {/* Stage rail */}
        <div className="dt-section">
          <h4>Stage <span style={{ fontWeight: 400, color: 'var(--fg-3)', textTransform: 'none', letterSpacing: '0.02em', fontFamily: 'var(--font-mono)', fontSize: 10 }}>· click to set</span></h4>
          <div className="stage-rail">
            {options.stages.map(s => (
              <div key={s.id}
                className={'stp' + (s.id === event.stage ? ' at' : (UD.stageOrder(options, s.id) < UD.stageOrder(options, event.stage) ? ' on' : ''))}
                onClick={() => patch({ stage: s.id })}>
                <span style={{ marginRight: 6, display: 'inline-flex', verticalAlign: '-2px' }}>
                  <StageGlyph pct={s.fillPct != null ? s.fillPct : ((s.order + 1) / 4) * 100} size={11} />
                </span>{s.name}
              </div>
            ))}
          </div>
        </div>

        {/* Result link — only shown after the event ships. Stores the
            actual artifact URL (X post, Instagram post, blog URL, etc.).
            Sits between Stage and Notes so the team sees the deliverable
            right after marking it done. */}
        {event.stage === 'done' && (
          <div className="dt-section">
            <h4>Result
              <span style={{ fontWeight: 400, color: 'var(--fg-3)', textTransform: 'none', letterSpacing: '0.02em', fontFamily: 'var(--font-mono)', fontSize: 10, marginLeft: 8 }}>
                · what shipped
              </span>
              {event.result_url && (
                <a className="h-action" href={normalizeUrl(event.result_url)} target="_blank" rel="noopener noreferrer">
                  Open ↗
                </a>
              )}
            </h4>
            <div className="primary-link-row">
              <input
                className="dt-input"
                placeholder="https://x.com/megaeth/… · instagram.com/p/… · youtu.be/…"
                value={event.result_url || ''}
                onChange={e => patch({ result_url: e.target.value })}
              />
              <button
                className="iconbtn sm"
                title={event.result_url ? 'Open result in new tab' : 'Add a URL first'}
                disabled={!event.result_url}
                onClick={() => event.result_url && window.open(normalizeUrl(event.result_url), '_blank', 'noopener,noreferrer')}
              ><Ic.ExternalLink /></button>
            </div>
            {event.result_url && (
              <div className="link-card-wrap">
                <MediaCard url={normalizeUrl(event.result_url)} />
              </div>
            )}
          </div>
        )}

        {/* Notes */}
        <div className="dt-section">
          <h4>Notes & updates</h4>
          <NotesField
            value={event.notes || ''}
            onChange={v => patch({ notes: v })}
            placeholder="Free text. URLs link automatically. @Name tags an owner. Click to edit."
            options={options}
          />
        </div>

        {/* URL */}
        <div className="dt-section">
          <h4>Primary Link
            {event.url && (
              <a className="h-action" href={normalizeUrl(event.url)} target="_blank" rel="noopener noreferrer">
                Open ↗
              </a>
            )}
          </h4>
          <div className="primary-link-row">
            <input className="dt-input" placeholder="https://typefully.com/t/… or docs.google.com/…"
              value={event.url || ''} onChange={e => patch({ url: e.target.value })} />
            <button
              className="iconbtn sm"
              title={event.url ? 'Open link in new tab' : 'Add a URL first'}
              disabled={!event.url}
              onClick={() => event.url && window.open(normalizeUrl(event.url), '_blank', 'noopener,noreferrer')}
            ><Ic.ExternalLink /></button>
          </div>
          {event.url && (
            <div className="link-card-wrap">
              <MediaCard url={normalizeUrl(event.url)} />
            </div>
          )}
        </div>

        {/* Media */}
        <MediaSection event={event} patch={patch} />
      </div>
    </aside>
  );
}

/* Multi-select owner picker. Stores `event.owners` as an array of owner
   ids. Reads legacy `event.owner` (single id) so older events keep
   working until they're edited. */
function OwnersRow({ event, options, onChange }) {
  const [open, setOpen] = useStateDt(false);
  const wrapRef = useRefDt(null);
  useEffectDt(() => {
    if (!open) return;
    const onDoc = e => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);

  const allOwners = options.owners || [];
  const current = UD.ownerIdsOf(event);
  const gtmTeam = options.gtmTeam || [];
  // Restrict the dropdown to GTM Team when populated; always keep the
  // currently-assigned owners visible so existing tasks remain editable
  // even if those owners were later removed from the team.
  const list = gtmTeam.length > 0
    ? allOwners.filter(o => gtmTeam.includes(o.id) || current.includes(o.id))
    : allOwners;
  const selected = new Set(current);
  const toggle = (id) => {
    const next = new Set(selected);
    if (next.has(id)) next.delete(id); else next.add(id);
    onChange([...next]);
  };
  const currentOwners = current.map(id => allOwners.find(o => o.id === id)).filter(Boolean);
  // Always show actual names (joined). Truncation falls back to the
  // .mcs-label ellipsis if the trigger is narrow, but for short
  // first names this comfortably fits 3-4 owners on one line.
  const summary = current.length === 0
    ? '— none —'
    : currentOwners.map(o => o.name).join(', ').toUpperCase();

  return (
    <div className="dt-row" ref={wrapRef}>
      <span className="lbl">Owners</span>
      <div className={'mcal-select' + (open ? ' open' : '')} style={{ position: 'relative' }}>
        <button
          type="button"
          className="mcs-trigger mono"
          onClick={() => setOpen(o => !o)}
          aria-expanded={open}
        >
          {currentOwners.length === 0 ? (
            <span className="mcs-swatch" style={{ background: 'transparent', borderStyle: 'dashed' }}></span>
          ) : currentOwners.length === 1 ? (
            <span className="mcs-swatch" style={{ background: currentOwners[0].color }}></span>
          ) : (
            <span className="mcs-swatch-stack">
              {currentOwners.slice(0, 4).map(o => (
                <span key={o.id} className="mcs-swatch" style={{ background: o.color }}></span>
              ))}
            </span>
          )}
          <span className={'mcs-label' + (current.length === 0 ? ' empty' : '')}>{summary}</span>
          <span className="mcs-caret">▾</span>
        </button>
        {open && (
          <div className="popover owners-popover" style={{ left: 0, right: 0, minWidth: 0 }}>
            {list.length === 0 ? (
              <div style={{ padding: 12, fontSize: 12, color: 'var(--fg-3)' }}>
                Owners come from @megaeth.com sign-ins. Have a teammate sign in to add them here.
              </div>
            ) : list.map(o => (
              <div key={o.id} className={'po-item' + (selected.has(o.id) ? ' on' : '')} onClick={() => toggle(o.id)}>
                <span className="swatch" style={{ background: o.color }}></span>
                <span style={{ flex: 1 }}>{o.name}</span>
                <span className="check">{selected.has(o.id) ? '✓' : ''}</span>
              </div>
            ))}
            {current.length > 0 && (
              <>
                <div className="po-divider"></div>
                <div className="po-clear" onClick={() => onChange([])}>Clear all</div>
              </>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

function SelectRow({ label, type, value, options, onChange, hint }) {
  const list = options[type] || [];
  const CustomSelect = window.mcalSelect.CustomSelect;
  return (
    <div className="dt-row">
      <span className="lbl">
        {label}
        {hint && <span className="opt-hint" title={hint}> ⓘ</span>}
      </span>
      <CustomSelect
        value={value || ''}
        options={list}
        onChange={onChange}
        placeholder="— none —"
        allowClear
        mono
      />
    </div>
  );
}

function AppRow({ event, options, appsForCategory, onChange }) {
  const CustomSelect = window.mcalSelect.CustomSelect;
  const cat = options.categories.find(c => c.id === event.category);
  return (
    <div className="dt-row" style={{ gridTemplateColumns: '92px 1fr', paddingLeft: 20, position: 'relative' }}>
      <span style={{
        position: 'absolute', left: 4, top: '50%', transform: 'translateY(-50%)',
        fontFamily: 'var(--font-mono)', color: 'var(--fg-4)', fontSize: 11,
      }}>↳</span>
      <span className="lbl sub">{cat ? `${cat.name} App` : 'App'} <span className="opt-hint">optional</span></span>
      <CustomSelect
        value={event.subproduct || ''}
        options={appsForCategory}
        onChange={onChange}
        placeholder="— any —"
        allowClear
        mono
      />
    </div>
  );
}

/* ─── Sub-tasks ───
   Each sub-task supports the same customization model as a regular event:
   channel, owner, category, product, subproduct (app), format, stage,
   notes, url, media. Fields default to inherit from the parent event but
   can be explicitly overridden per sub-task. Click a row to expand the
   inline editor; the collapsed view stays compact for daily-breakdown use.
*/
function SubtaskSection({ event, options, patch, openSubtaskId }) {
  const subtasks = event.subtasks || [];
  const [draft, setDraft] = useStateDt('');
  const [expandedId, setExpandedId] = useStateDt(null);
  // When a sub-pill is clicked elsewhere, auto-expand the matching row.
  // No scrolling — the user can scroll the panel themselves if needed.
  // IMPORTANT: depend only on `openSubtaskId`. Earlier this also
  // depended on `subtasks.length`, which meant adding a new sub-task
  // re-fired this effect and snapped the expansion back to whichever
  // sub-task originally opened the panel — clobbering the freshly-added
  // one that `addSub` had just expanded.
  useEffectDt(() => {
    if (!openSubtaskId) return;
    if (subtasks.some(s => s.id === openSubtaskId)) {
      setExpandedId(openSubtaskId);
    }
  }, [openSubtaskId]);

  const addSub = () => {
    if (!draft.trim()) return;
    // Leave classification fields undefined so they inherit from the
    // parent. SubtaskItem's `eff` falls back to parent.{channel,...} when
    // the override is undefined/null — that's the "inherit" semantic.
    const st = {
      id: 'st_' + Math.random().toString(36).slice(2, 7),
      title: draft.trim(),
      date: event.start,
      stage: 'ideation',
      notes: '',
      url: '',
      media: [],
    };
    patch({ subtasks: [...subtasks, st] });
    setDraft('');
    setExpandedId(st.id);
  };
  const updateSub = (id, p) => {
    patch({ subtasks: subtasks.map(s => s.id === id ? { ...s, ...p } : s) });
  };
  const delSub = id => {
    patch({ subtasks: subtasks.filter(s => s.id !== id) });
    if (expandedId === id) setExpandedId(null);
  };

  // Clamp subtask dates to event range
  const minDate = event.start;
  const maxDate = event.end;

  return (
    <div className="dt-section">
      <h4>Sub-tasks <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)', textTransform: 'none', letterSpacing: '0.02em' }}>
        {subtasks.length === 0 ? 'none yet' : `${subtasks.length} · daily breakdown`}
      </span></h4>
      {subtasks.length > 0 && (
        <div className="subtask-list">
          {subtasks
            .slice().sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0)
            .map(st => (
            <SubtaskItem
              key={st.id}
              st={st}
              parent={event}
              options={options}
              minDate={minDate}
              maxDate={maxDate}
              expanded={expandedId === st.id}
              onToggleExpand={() => setExpandedId(prev => prev === st.id ? null : st.id)}
              onChange={p => updateSub(st.id, p)}
              onRemove={() => delSub(st.id)}
            />
          ))}
        </div>
      )}
      <div className="add-subtask">
        <input
          placeholder="Add sub-task title…"
          value={draft}
          onChange={e => setDraft(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addSub()}
        />
        <button onClick={addSub}><Ic.Plus /></button>
      </div>
      <div style={{ marginTop: 8, fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-4)', letterSpacing: '0.02em' }}>
        Sub-tasks appear under the parent on their scheduled day. Click a row to customize channel, owner, category, format, notes, media — same model as a regular event.
      </div>
    </div>
  );
}

/* A single sub-task row + expandable inline editor.
   Inherited fields fall back to the parent event; explicit nulls indicate
   the user cleared an override. We pass `inheritedFrom` so the UI can
   visually mark inherited values without baking the parent value into the
   sub-task data. */
function SubtaskItem({ st, parent, options, minDate, maxDate, expanded, onToggleExpand, onChange, onRemove }) {
  const parentOwners = UD.ownerIdsOf(parent);
  const eff = {
    channel:    st.channel    !== undefined && st.channel    !== null ? st.channel    : parent.channel,
    owners:     UD.ownerIdsOf(st, parentOwners),
    category:   st.category   !== undefined && st.category   !== null ? st.category   : parent.category,
    product:    st.product    !== undefined && st.product    !== null ? st.product    : parent.product,
    subproduct: st.subproduct !== undefined && st.subproduct !== null ? st.subproduct : parent.subproduct,
    format:     st.format     !== undefined && st.format     !== null ? st.format     : parent.format,
    stage:      st.stage || 'ideation',
  };

  // Reuse parent's category-driven product/app visibility logic. MegaETH
  // category never surfaces an App or Product picker.
  const appsForCategory = UD.subproductsFor(options, eff.category) || [];
  const showAppField = appsForCategory.length > 0 && eff.category !== 'megaeth';
  const showProductField = !showAppField && eff.category !== 'megaeth';

  const isOverride = key => st[key] !== undefined && st[key] !== null && st[key] !== '';

  // Faux-patch for nested editor — proxies to onChange
  const patchSt = (p) => onChange(p);

  return (
    <div className={'subtask-row' + (expanded ? ' is-expanded' : '')} data-st-id={st.id}>
      <div className="st-summary" onClick={onToggleExpand}>
        <span className="st-handle" title="Drag to reorder">⋮</span>
        <button className="st-caret" onClick={e => { e.stopPropagation(); onToggleExpand(); }} title={expanded ? 'Collapse' : 'Customize'}>
          <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform 160ms cubic-bezier(0.16,1,0.3,1)' }}>
            <path d="M4 2l4 4-4 4" />
          </svg>
        </button>
        <input
          type="date"
          className="st-date"
          value={st.date}
          min={minDate}
          max={maxDate}
          onClick={e => e.stopPropagation()}
          onChange={e => patchSt({ date: e.target.value })}
        />
        <SubtaskTitleInput
          value={st.title}
          onCommit={v => patchSt({ title: v })}
          options={options}
        />
        <button className="st-del" title="Remove" onClick={e => { e.stopPropagation(); onRemove(); }}><Ic.Close /></button>
      </div>
      {expanded && (
        <div className="st-editor">
          <div className="st-editor-grid">
            <SubtaskSelectField label="Channel"  overridden={isOverride('channel')}
              value={st.channel} parentValue={parent.channel} optionList={options.channels}
              onChange={v => patchSt({ channel: v })} />
            <SubtaskOwnersField
              st={st} parent={parent} options={options}
              onChange={ids => patchSt({ owners: ids, owner: undefined })}
              onInherit={() => patchSt({ owners: undefined, owner: undefined })}
            />
            <SubtaskSelectField label="Category" overridden={isOverride('category')}
              value={st.category} parentValue={parent.category} optionList={options.categories}
              onChange={v => {
                if (v === undefined) {
                  patchSt({ category: undefined, product: undefined, subproduct: undefined });
                } else {
                  const np = { category: v };
                  if (UD.subproductsFor(options, v).length === 0) np.subproduct = '';
                  if (UD.subproductsFor(options, v).length > 0) np.product = '';
                  patchSt(np);
                }
              }} />
            {showProductField && (
              <SubtaskSelectField label="Product" overridden={isOverride('product')}
                value={st.product} parentValue={parent.product} optionList={options.products}
                onChange={v => patchSt({ product: v })} />
            )}
            {showAppField && (
              <SubtaskSelectField
                label={(options.categories.find(c => c.id === eff.category)?.name || 'Category') + ' App'}
                overridden={isOverride('subproduct')}
                value={st.subproduct} parentValue={parent.subproduct} optionList={appsForCategory}
                onChange={v => patchSt({ subproduct: v })} />
            )}
            <SubtaskSelectField label="Format" overridden={isOverride('format')}
              value={st.format} parentValue={parent.format} optionList={options.formats}
              onChange={v => patchSt({ format: v })} />
          </div>

          <div className="st-editor-row column">
            <span className="st-editor-label">Stage</span>
            <div className="stage-rail">
              {options.stages.map(s => (
                <div key={s.id}
                  className={'stp' + (s.id === eff.stage ? ' at' : (UD.stageOrder(options, s.id) < UD.stageOrder(options, eff.stage) ? ' on' : ''))}
                  onClick={() => patchSt({ stage: s.id })}>
                  <span style={{ marginRight: 6, display: 'inline-flex', verticalAlign: '-2px' }}>
                    <StageGlyph pct={s.fillPct != null ? s.fillPct : ((s.order + 1) / 4) * 100} size={11} />
                  </span>{s.name}
                </div>
              ))}
            </div>
          </div>

          <div className="st-editor-row" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <span className="st-editor-label" style={{ minWidth: 60 }}>Review</span>
            <div className="needs-review-toggle">
              <button className={!st.needsReview ? 'on' : ''} onClick={() => patchSt({ needsReview: false })}>OK</button>
              <button className={'nr ' + (st.needsReview ? 'on' : '')} onClick={() => patchSt({ needsReview: true })}>Needs review</button>
            </div>
          </div>

          <div className="st-editor-row column">
            <span className="st-editor-label">Notes</span>
            <NotesField
              value={st.notes || ''}
              onChange={v => patchSt({ notes: v })}
              placeholder="Sub-task notes. URLs link · @Name tags an owner."
              rows={2}
              options={options}
            />
          </div>

          <div className="st-editor-row column">
            <span className="st-editor-label">
              Primary Link
              {st.url && (
                <a className="h-action" href={normalizeUrl(st.url)} target="_blank" rel="noopener noreferrer" style={{ marginLeft: 8 }}>
                  Open ↗
                </a>
              )}
            </span>
            <div className="primary-link-row">
              <input className="dt-input" placeholder="https://typefully.com/t/… or docs.google.com/…"
                value={st.url || ''} onChange={e => patchSt({ url: e.target.value })} />
              <button
                className="iconbtn sm"
                title={st.url ? 'Open link in new tab' : 'Add a URL first'}
                disabled={!st.url}
                onClick={() => st.url && window.open(normalizeUrl(st.url), '_blank', 'noopener,noreferrer')}
              ><Ic.ExternalLink /></button>
            </div>
            {st.url && (
              <div className="link-card-wrap">
                <MediaCard url={normalizeUrl(st.url)} />
              </div>
            )}
          </div>

          <div className="st-editor-row column">
            <span className="st-editor-label">Media</span>
            <MediaSection event={st} patch={patchSt} compact />
          </div>
        </div>
      )}
    </div>
  );
}

/* Reusable @mention autocomplete hook. Watches an input/textarea's
   value as the user types; when the caret sits inside an `@<letters>`
   token (no space yet), pops a dropdown of matching owners. Arrow
   keys + Enter/Tab pick a match; clicking a row does the same.
   Returns:
     - handleChange: wire to <input onChange>
     - handleKeyDown: wire to <input onKeyDown>, returns true if the
       keystroke was consumed by the autocomplete (caller should
       early-return in that case before its own keydown logic runs)
     - handleBlur: closes the popup when focus moves elsewhere
     - popup: a React element to render directly after the input */
function useMentionAutocomplete(inputRef, value, setValue, options) {
  const [open, setOpen] = useStateDt(false);
  const [query, setQuery] = useStateDt('');
  const [start, setStart] = useStateDt(-1);
  const [idx, setIdx] = useStateDt(0);
  const pendingCaret = useRefDt(null);

  const allOwners = (options && options.owners) || [];
  const gtmTeam = (options && options.gtmTeam) || [];
  // When the GTM Team has members, restrict the mention dropdown to
  // just those owners. Otherwise fall back to the full roster.
  const owners = gtmTeam.length > 0
    ? allOwners.filter(o => gtmTeam.includes(o.id))
    : allOwners;

  // Match logic: case-insensitive prefix against full name OR first
  // name. Caps at 8 results so a 50-person org doesn't overflow.
  const matches = useMemoDt(() => {
    if (!open || !owners.length) return [];
    const q = (query || '').toLowerCase();
    return owners.filter(o => {
      const n = (o.name || '').toLowerCase();
      const f = (n.split(' ')[0] || '');
      return q === '' || n.startsWith(q) || f.startsWith(q);
    }).slice(0, 8);
  }, [open, owners, query]);

  const handleChange = e => {
    const text = e.target.value;
    setValue(text);
    const caret = e.target.selectionStart;
    const upTo = text.slice(0, caret);
    const at = upTo.lastIndexOf('@');
    if (at >= 0) {
      const seg = upTo.slice(at + 1);
      // Open the popup if we're typing a word right after @ (no space
      // yet) AND the @ is at a word boundary (start of input or after
      // a non-word char) so emails like foo@bar.com don't trigger it.
      const okSeg = MENTION_SEG_RE.test(seg);
      const okBoundary = at === 0 || NON_WORD_RE.test(upTo[at - 1]);
      if (okSeg && okBoundary) {
        setOpen(true);
        setQuery(seg);
        setStart(at);
        setIdx(0);
        return;
      }
    }
    if (open) setOpen(false);
  };

  const apply = (i) => {
    const owner = matches[i];
    if (!owner || start < 0) return;
    const before = value.slice(0, start);
    const after = value.slice(start + 1 + query.length);
    const inserted = before + '@' + owner.name + ' ' + after.replace(/^\s+/, '');
    const newCaret = (before + '@' + owner.name + ' ').length;
    pendingCaret.current = newCaret;
    setValue(inserted);
    setOpen(false);
  };

  const handleKeyDown = e => {
    if (!open || matches.length === 0) return false;
    if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(i + 1, matches.length - 1)); return true; }
    if (e.key === 'ArrowUp')   { e.preventDefault(); setIdx(i => Math.max(i - 1, 0)); return true; }
    if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); apply(idx); return true; }
    if (e.key === 'Escape')    { e.preventDefault(); setOpen(false); return true; }
    return false;
  };

  const handleBlur = () => {
    // Close on blur via a tiny delay so popup clicks (which preserve
    // focus through onMouseDown.preventDefault) finish first.
    setTimeout(() => setOpen(false), 80);
  };

  useEffectDt(() => {
    if (pendingCaret.current != null && inputRef.current) {
      const pos = pendingCaret.current;
      inputRef.current.setSelectionRange(pos, pos);
      pendingCaret.current = null;
    }
  });

  const popup = open && matches.length > 0 ? (
    <div className="mention-popup">
      {matches.map((o, i) => (
        <div
          key={o.id}
          className={'mention-item' + (i === idx ? ' on' : '')}
          onMouseDown={e => e.preventDefault()}
          onClick={() => apply(i)}
        >
          <span className="mention-swatch" style={{ background: o.color }}></span>
          <span className="mention-name">{o.name}</span>
        </div>
      ))}
    </div>
  ) : null;

  return { handleChange, handleKeyDown, handleBlur, popup };
}

/* Same local-draft pattern as SubtaskTitleInput — the parent-event
   title input was suffering from the identical "cursor jumps to end on
   every keystroke" bug because every char flushed up through
   `patch → update → setEventsRaw`, producing a new event object that
   re-rendered the input with a stale React-managed value. */
function EventTitleInput({ value, onCommit, placeholder, autoFocusEmpty, options }) {
  const [draft, setDraft] = useStateDt(value || '');
  const ref = useRefDt(null);
  useEffectDt(() => {
    if (document.activeElement !== ref.current) setDraft(value || '');
  }, [value]);
  const flush = () => {
    const v = draft;
    if (v !== (value || '')) onCommit(v);
  };
  const mention = useMentionAutocomplete(ref, draft, setDraft, options);
  return (
    <span className="mention-input-wrap">
      <input
        ref={ref}
        className="ttl-input"
        value={draft}
        placeholder={placeholder}
        autoFocus={autoFocusEmpty}
        onChange={mention.handleChange}
        onBlur={() => { mention.handleBlur(); flush(); }}
        onKeyDown={e => {
          if (mention.handleKeyDown(e)) return;
          if (e.key === 'Enter') { e.preventDefault(); flush(); e.currentTarget.blur(); }
          if (e.key === 'Escape') { setDraft(value || ''); e.currentTarget.blur(); }
        }}
      />
      {mention.popup}
    </span>
  );
}

/* Uncontrolled-style title input — keeps typing local and only flushes
   upstream on blur / Enter. Prevents the React-controlled-input cursor
   reset that surfaced when every keystroke roundtripped through the
   parent state (selectedEvent → subtask array spread → re-render). */
function SubtaskTitleInput({ value, onCommit, options }) {
  const [draft, setDraft] = useStateDt(value);
  const ref = useRefDt(null);
  useEffectDt(() => {
    if (document.activeElement !== ref.current) setDraft(value);
  }, [value]);
  const flush = () => {
    if (draft !== value) onCommit(draft);
  };
  const mention = useMentionAutocomplete(ref, draft, setDraft, options);
  return (
    <span className="mention-input-wrap" onClick={e => e.stopPropagation()}>
      <input
        ref={ref}
        className="st-title"
        value={draft}
        onChange={mention.handleChange}
        onBlur={() => { mention.handleBlur(); flush(); }}
        onKeyDown={e => {
          if (mention.handleKeyDown(e)) return;
          if (e.key === 'Enter') { e.preventDefault(); flush(); e.currentTarget.blur(); }
          if (e.key === 'Escape') { setDraft(value); e.currentTarget.blur(); }
        }}
      />
      {mention.popup}
    </span>
  );
}

function SubtaskField({ label, overridden, children }) {
  return (
    <div className={'st-field' + (overridden ? ' is-override' : '')}>
      <span className="st-field-label">{label}</span>
      {children}
    </div>
  );
}

/* Sub-task owners — multi-select with an inline "inherit from parent"
   chip. Setting `st.owners = undefined` means inherit; an explicit empty
   array means "no owners" (which is rare but valid). */
function SubtaskOwnersField({ st, parent, options, onChange, onInherit }) {
  const [open, setOpen] = useStateDt(false);
  const ref = useRefDt(null);
  useEffectDt(() => {
    if (!open) return;
    const onDoc = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);

  const allOwners = options.owners || [];
  const overridden = (Array.isArray(st.owners)) || (st.owner !== undefined && st.owner !== null && st.owner !== '');
  const parentOwners = UD.ownerIdsOf(parent);
  const effective = overridden ? UD.ownerIdsOf(st) : parentOwners;
  const gtmTeam = options.gtmTeam || [];
  // Same rule as parent-event owner picker — limit to GTM Team but
  // keep the parent's owners + any explicit overrides visible.
  const visiblePool = new Set([...effective, ...parentOwners]);
  const list = gtmTeam.length > 0
    ? allOwners.filter(o => gtmTeam.includes(o.id) || visiblePool.has(o.id))
    : allOwners;
  const selected = new Set(effective);
  const toggle = id => {
    const next = new Set(overridden ? effective : []);
    if (next.has(id)) next.delete(id); else next.add(id);
    onChange([...next]);
  };
  const effectiveOwners = effective.map(id => allOwners.find(o => o.id === id)).filter(Boolean);
  const summary = effective.length === 0
    ? (overridden ? '— none —' : 'Inherit · — none —').toUpperCase()
    : (overridden ? '' : 'Inherit · ') + effectiveOwners.map(o => o.name).join(', ').toUpperCase();

  return (
    <SubtaskField label="Owners" overridden={overridden}>
      <div className={'mcal-select' + (open ? ' open' : '')} style={{ position: 'relative' }} ref={ref}>
        <button type="button" className="mcs-trigger mono" onClick={() => setOpen(o => !o)}>
          {effectiveOwners.length === 0 ? (
            <span className="mcs-swatch" style={{ background: 'transparent', borderStyle: 'dashed' }}></span>
          ) : effectiveOwners.length === 1 ? (
            <span className="mcs-swatch" style={{ background: effectiveOwners[0].color }}></span>
          ) : (
            <span className="mcs-swatch-stack">
              {effectiveOwners.slice(0, 4).map(o => (
                <span key={o.id} className="mcs-swatch" style={{ background: o.color }}></span>
              ))}
            </span>
          )}
          <span className={'mcs-label' + (effective.length === 0 ? ' empty' : '')}>{summary}</span>
          <span className="mcs-caret">▾</span>
        </button>
        {open && (
          <div className="popover owners-popover">
            {overridden && (
              <>
                <div className={'po-item'} onClick={() => { onInherit(); setOpen(false); }}>
                  <span className="swatch" style={{ background: 'transparent', borderStyle: 'dashed' }}></span>
                  <span style={{ flex: 1, color: 'var(--fg-2)' }}>Inherit from parent</span>
                </div>
                <div className="po-divider"></div>
              </>
            )}
            {list.length === 0 ? (
              <div style={{ padding: 10, fontSize: 12, color: 'var(--fg-3)' }}>No teammates have signed in yet.</div>
            ) : list.map(o => (
              <div key={o.id} className={'po-item' + (selected.has(o.id) ? ' on' : '')} onClick={() => toggle(o.id)}>
                <span className="swatch" style={{ background: o.color }}></span>
                <span style={{ flex: 1 }}>{o.name}</span>
                <span className="check">{selected.has(o.id) ? '✓' : ''}</span>
              </div>
            ))}
          </div>
        )}
      </div>
    </SubtaskField>
  );
}

/* Sub-task select that surfaces inheritance through the dropdown itself
   rather than via a separate side-label. The first option is a synthetic
   "Inherit (Parent Value)" — picking it clears the override; picking any
   other option sets the explicit value. */
const INHERIT_VAL = '__inherit__';
function SubtaskSelectField({ label, value, parentValue, options, onChange, optionList, placeholder, overridden }) {
  // Compute the inherit label by finding the parent's display value in the option list.
  const parentOpt = optionList.find(o => o.id === parentValue);
  const parentName = parentOpt ? parentOpt.name : '— none —';
  const parentColor = parentOpt && parentOpt.color;
  const augmented = [
    { id: INHERIT_VAL, name: `Inherit · ${parentName}`, color: parentColor },
    ...optionList,
  ];
  const displayValue = overridden ? (value || '') : INHERIT_VAL;
  return (
    <SubtaskField label={label} overridden={overridden}>
      <window.mcalSelect.CustomSelect
        value={displayValue}
        options={augmented}
        onChange={v => onChange(v === INHERIT_VAL ? undefined : v)}
        placeholder={placeholder || '— select —'}
        mono
      />
    </SubtaskField>
  );
}

// ─── Media section ───
function MediaSection({ event, patch, compact }) {
  const [draftUrl, setDraftUrl] = useStateDt('');
  const [dragover, setDragover] = useStateDt(false);
  const mediaList = event.media || [];

  const addUrl = raw => {
    const url = (raw || '').trim();
    if (!url) return;
    if (mediaList.includes(url)) return;
    patch({ media: [...mediaList, url] });
    setDraftUrl('');
  };
  const removeUrl = url => patch({ media: mediaList.filter(u => u !== url) });

  const onPaste = e => {
    const txt = e.clipboardData.getData('text');
    if (/^https?:\/\//.test(txt.trim())) {
      e.preventDefault();
      addUrl(txt);
    }
  };
  const onDrop = e => {
    e.preventDefault();
    setDragover(false);
    const u = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain');
    if (u && /^https?:\/\//.test(u.trim())) addUrl(u.trim());
  };

  return (
    <div className={'dt-section' + (compact ? ' dt-section--compact' : '')}>
      {!compact && (
        <h4>Media · attachments
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-3)', textTransform: 'none', letterSpacing: '0.02em' }}>
            {mediaList.length} {mediaList.length === 1 ? 'link' : 'links'}
          </span>
        </h4>
      )}
      <div
        className={'media-zone' + (dragover ? ' dragover' : '') + (compact ? ' compact' : '')}
        onDragOver={e => { e.preventDefault(); setDragover(true); }}
        onDragLeave={() => setDragover(false)}
        onDrop={onDrop}
        onPaste={onPaste}
      >
        {mediaList.length === 0 ? (
          <div className="media-empty">
            Drop a URL or paste here.
            <span className="hint">YouTube · Google Drive · Docs · Typefully · Images · anything with an href</span>
          </div>
        ) : (
          <div className="media-list">
            {mediaList.map(url => (
              <MediaCard key={url} url={url} onRemove={() => removeUrl(url)} />
            ))}
          </div>
        )}
        <div className="media-add">
          <input
            placeholder="Paste a URL…"
            value={draftUrl}
            onChange={e => setDraftUrl(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') addUrl(draftUrl); }}
          />
          <button onClick={() => addUrl(draftUrl)}>Attach</button>
        </div>
      </div>
    </div>
  );
}

function MediaCard({ url, onRemove, compact }) {
  const info = window.detectMedia(url);
  const href = url.startsWith('http') ? url : 'https://' + url;
  return (
    <div className={'media-card' + (compact ? ' compact' : '')}>
      <a
        className="mc-head mc-head-link"
        href={href}
        target="_blank"
        rel="noopener noreferrer"
        onClick={e => e.stopPropagation()}
        title={'Open ' + href}
      >
        <div className={'mc-icon ' + iconClassFor(info)}>{iconGlyphFor(info)}</div>
        <div className="mc-meta">
          <div className="mc-title">{info.title || url}</div>
          <div className="mc-host">{labelFor(info)}</div>
        </div>
        <div className="mc-actions">
          <span className="mc-open" title="Open in new tab"><Ic.ExternalLink /></span>
          {onRemove && (
            <button
              className="x"
              onClick={e => { e.preventDefault(); e.stopPropagation(); onRemove(); }}
              title="Remove"
            ><Ic.Close /></button>
          )}
        </div>
      </a>
      {!compact && renderPreview(info)}
    </div>
  );
}

window.mcalMediaCard = MediaCard;

function iconClassFor(info) {
  switch (info.type) {
    case 'youtube': return 'yt';
    case 'gdrive-file':
    case 'gdrive-folder': return 'drive';
    case 'gdoc': return info.kind || 'doc';
    case 'typefully': return 'typefully';
    case 'image': return 'image';
    case 'tweet': return 'tweet';
    default: return '';
  }
}
/* Google's officially hosted product icons. Stable CDN URLs with the
   actual brand colors (Docs blue, Sheets green, Slides yellow) — beats
   the previous "D" / "S" / "P" letter glyphs. */
const GDOC_ICON_URL = {
  doc:    'https://ssl.gstatic.com/docs/doclist/images/mediatype/icon_1_document_x32.png',
  sheet:  'https://ssl.gstatic.com/docs/doclist/images/mediatype/icon_1_spreadsheet_x32.png',
  slides: 'https://ssl.gstatic.com/docs/doclist/images/mediatype/icon_1_presentation_x32.png',
};
function iconGlyphFor(info) {
  switch (info.type) {
    case 'youtube': return '▶';
    case 'gdrive-file': return '◆';
    case 'gdrive-folder': return '▤';
    case 'gdoc': {
      const src = GDOC_ICON_URL[info.kind] || GDOC_ICON_URL.doc;
      const alt = info.kind === 'doc' ? 'Google Docs'
                : info.kind === 'sheet' ? 'Google Sheets'
                : 'Google Slides';
      return (
        <img
          className="mc-icon-img"
          src={src}
          alt={alt}
          referrerPolicy="no-referrer"
        />
      );
    }
    case 'typefully':
      // Real Typefully wordmark/glyph via the favicon service. Caches well,
      // and renders the actual brand mark instead of a generic "T".
      return (
        <img
          className="mc-icon-img"
          src="https://www.google.com/s2/favicons?domain=typefully.com&sz=64"
          alt="Typefully"
          referrerPolicy="no-referrer"
        />
      );
    case 'image': return '▣';
    case 'tweet': return '𝕏';
    case 'notion': return 'N';
    case 'invalid': return '!';
    default: return '↗';
  }
}
function labelFor(info) {
  switch (info.type) {
    case 'youtube': return 'YouTube · embed';
    case 'gdrive-file': return 'Google Drive · file preview';
    case 'gdrive-folder': return 'Google Drive · folder';
    case 'gdoc':
      return info.kind === 'doc' ? 'Google Docs' : info.kind === 'sheet' ? 'Google Sheets' : 'Google Slides';
    case 'typefully': return 'Typefully · draft';
    case 'image': return 'Image · thumbnail';
    case 'tweet': return 'X · post';
    case 'notion': return 'Notion';
    case 'invalid': return 'Invalid URL';
    default: return info.host || 'Link';
  }
}
function renderPreview(info) {
  if (info.type === 'youtube' && info.embed) {
    return (
      <div className="mc-preview">
        <iframe src={info.embed} allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
      </div>
    );
  }
  if (info.type === 'gdrive-file' && info.embed) {
    return (
      <div className="mc-preview">
        <iframe src={info.embed} allow="autoplay" allowFullScreen></iframe>
      </div>
    );
  }
  if (info.type === 'image') {
    return (
      <div className="mc-preview">
        <img src={info.url} alt="" loading="lazy"
          onError={e => { e.target.replaceWith(Object.assign(document.createElement('div'), { className: 'img-fallback', textContent: 'Image failed to load' })); }} />
      </div>
    );
  }
  return null;
}

/* Memoize so the panel doesn't re-render when unrelated App state changes
   (theme tick, syncedAt timestamp, etc.). Patches still flow through
   because they replace the event object reference. */
window.mcalDetail = { DetailPanel: React.memo(DetailPanel) };
