// ─────────────────────────────────────────────────────────────
// LifeWheel Coach — Cohort CMS.
// Replaces hand-editing the cohort manifest in the Firebase RTDB console
// with a guided web form. Hosts can edit their cohort copy, day plans,
// expert card, and hero photos directly.
//
// Auth gate (mirrors marathon.jsx):
//   - Caller must be signed in.
//   - /users/{uid}/role === 'expert' (or 'admin').
//   - /users/{uid}/expertId must match the cohort being edited.
// Server-side enforcement is in cloud function `cohortUpsert`; this page
// only does best-effort client checks to avoid wasted round-trips.
//
// Save flow: form → client validation → cohortUpsert (onCall) →
// server re-validates + writes /cohorts/{id} → success toast + reset
// dirty state.
//
// Date format gotcha: cohort timestamps are Unix epoch SECONDS. The
// form uses <input type="datetime-local"> local-tz strings; we convert
// at the boundary.
//
// Empty-string-on-nil convention: optional string fields write "" (not
// missing) so the iOS mapper sees an explicit clear and the RTDB
// `setValue(nil)` foot-gun never fires.
//   See feedback/firebase_dict_nil_clear_pattern_2026_05_06.md
// ─────────────────────────────────────────────────────────────

// Theme bridge — every sub-component reads styles via context so the CMS
// works in both dark and light coach-portal modes (and any accent recolor).
// `S` carries the entire stylesheet, derived from the active palette.
const CMSStyleCtx = React.createContext(null);
function useCMS() { return React.useContext(CMSStyleCtx); }

function LWCohortCMS() {
  const lw = (typeof useLW === 'function') ? useLW() : null;
  const c = (lw && lw.c) || _fallbackPalette();
  const fonts = (lw && lw.fonts) || _fallbackFonts();
  const styles = React.useMemo(() => makeStyles(c, fonts), [c, fonts]);

  const [phase, setPhase] = React.useState('loading'); // loading | error | empty | ready
  const [errorMsg, setErrorMsg] = React.useState(null);
  const [cohorts, setCohorts] = React.useState([]);   // [{id, expertId, potokNumber, startsAt}]
  const [activeId, setActiveId] = React.useState(null);
  const [serverManifest, setServerManifest] = React.useState(null); // last known server state
  const [form, setForm] = React.useState(null);                     // current form state
  const [saving, setSaving] = React.useState(false);
  const [saveError, setSaveError] = React.useState(null);
  const [savedAt, setSavedAt] = React.useState(null);
  const [previewName, setPreviewName] = React.useState('Anna');
  const [authedUid, setAuthedUid] = React.useState(null);
  const [accessHint, setAccessHint] = React.useState(null);
  const [activeTab, setActiveTab] = React.useState('expert');

  // 1) Resolve which cohorts the signed-in user can host.
  React.useEffect(() => {
    (async () => {
      try {
        const auth = window.LWFB && window.LWFB.auth.currentUser;
        if (!auth) { setPhase('error'); setErrorMsg('not_signed_in'); return; }
        setAuthedUid(auth.uid);
        const expertSnap = await window.LWFB.db.ref(`/users/${auth.uid}/expertId`).get();
        const roleSnap   = await window.LWFB.db.ref(`/users/${auth.uid}/role`).get();
        const expertId = expertSnap.exists() ? String(expertSnap.val() || '') : '';
        const role = roleSnap.exists() ? String(roleSnap.val() || '') : '';

        if (!expertId && role !== 'admin') {
          setAccessHint({ uid: auth.uid, role, expertId });
          setPhase('empty'); setErrorMsg('no_expert'); return;
        }

        const allSnap = await window.LWFB.db.ref('/cohorts').get();
        const matching = [];
        allSnap.forEach(child => {
          const c = child.val() || {};
          if (role === 'admin' || c.expertId === expertId) {
            matching.push({
              id: child.key,
              expertId: c.expertId || '',
              potokNumber: c.potokNumber || 0,
              startsAt: c.startsAt || 0,
            });
          }
        });
        if (!matching.length) {
          setAccessHint({ uid: auth.uid, role, expertId });
          setPhase('empty'); setErrorMsg('no_cohorts'); return;
        }
        matching.sort((a, b) => (b.startsAt || 0) - (a.startsAt || 0));
        setCohorts(matching);
        setActiveId(matching[0].id);
      } catch (e) {
        console.error('[cms] init', e);
        setPhase('error'); setErrorMsg(String(e && e.message || e));
      }
    })();
  }, []);

  // 2) Load full manifest for the active cohort.
  React.useEffect(() => {
    if (!activeId) return;
    setPhase('loading'); setSaveError(null);
    (async () => {
      try {
        const snap = await window.LWFB.db.ref(`/cohorts/${activeId}`).get();
        if (!snap.exists()) {
          setPhase('error'); setErrorMsg('cohort_not_found'); return;
        }
        const raw = snap.val() || {};
        // Strip server-managed `aggregates` from the editable form.
        const { aggregates: _agg, ...manifest } = raw;
        // Normalize the server snapshot through the same form roundtrip used
        // for save. Without this, the dirty-state check (formToManifest(form)
        // vs serverManifest) shows a legacy cohort as "dirty" the moment it
        // loads — because manifestToForm fills in defaults like
        // `registrationOpen: true` that don't exist in the legacy snapshot.
        // Comparing roundtripped-vs-roundtripped keeps dirty honest.
        const normalized = formToManifest(manifestToForm(manifest));
        setServerManifest(normalized);
        setForm(manifestToForm(manifest));
        setPhase('ready');
      } catch (e) {
        console.error('[cms] load', e);
        setPhase('error'); setErrorMsg(String(e && e.message || e));
      }
    })();
  }, [activeId]);

  // Derived: dirty state vs validation errors.
  const dirty = form != null && serverManifest != null
    && JSON.stringify(formToManifest(form)) !== JSON.stringify(serverManifest);
  const validation = form != null ? validateForm(form) : [];
  const canSave = dirty && validation.length === 0 && !saving;

  function update(producer) {
    setForm(prev => {
      const next = JSON.parse(JSON.stringify(prev));
      producer(next);
      return next;
    });
    setSaveError(null);
  }

  async function onSave() {
    if (!form || !activeId) return;
    setSaving(true); setSaveError(null);
    try {
      const manifest = formToManifest(form);
      const fn = firebase.functions().httpsCallable('cohortUpsert');
      const res = await fn({ cohortId: activeId, manifest });
      setServerManifest(manifest);
      setSavedAt(Date.now());
      console.log('[cms] saved', res && res.data);
    } catch (e) {
      console.error('[cms] save', e);
      const detail = (e && e.details && e.details.errors) ? e.details.errors.join('\n') : '';
      setSaveError((e && e.message ? e.message : String(e)) + (detail ? '\n' + detail : ''));
    } finally {
      setSaving(false);
    }
  }

  function onDiscard() {
    if (!serverManifest) return;
    setForm(manifestToForm(serverManifest));
    setSaveError(null);
  }

  // ── Render branches (wrapped in style context) ──
  return (
    <CMSStyleCtx.Provider value={{ S: styles, c, fonts }}>
      {(() => {
        if (phase === 'loading' && !form) return <CMSShell><div style={styles.muted}>Загрузка манифеста…</div></CMSShell>;
        if (phase === 'empty' && errorMsg === 'no_expert') {
          return <CMSShell><AccessHelp hint={accessHint} mode="no_expert" /></CMSShell>;
        }
        if (phase === 'empty' && errorMsg === 'no_cohorts') {
          return <CMSShell><AccessHelp hint={accessHint} mode="no_cohorts" /></CMSShell>;
        }
        if (phase === 'error' && errorMsg === 'not_signed_in') {
          return <CMSShell><EmptyState
            title="Нужен вход"
            body={<>
              Сначала войди как ведущая в коуч-портал, потом возвращайся сюда.{' '}
              <a href="#/login" style={{ color: c.accent, textDecoration: 'underline' }}>Войти →</a>
            </>}
          /></CMSShell>;
        }
        if (phase === 'error') {
          return <CMSShell><EmptyState title="Ошибка" body={errorMsg || 'unknown'} /></CMSShell>;
        }
        if (!form) return <CMSShell><div style={styles.muted}>…</div></CMSShell>;

        return (
          <CMSShell>
            <Toolbar
              cohorts={cohorts}
              activeId={activeId}
              onSelect={setActiveId}
              dirty={dirty}
              validation={validation}
              saving={saving}
              savedAt={savedAt}
              canSave={canSave}
              onSave={onSave}
              onDiscard={onDiscard}
              previewName={previewName}
              setPreviewName={setPreviewName}
            />

            {saveError && (
              <div style={{ ...styles.card, borderColor: c.danger || '#E85D5D', color: c.danger || '#E85D5D', whiteSpace: 'pre-wrap', marginTop: 12 }}>
                Не сохранилось: {saveError}
              </div>
            )}

            {validation.length > 0 && (
              <div style={{ ...styles.card, borderColor: c.warning || '#E8943A', color: c.warning || '#E8943A', marginTop: 12 }}>
                <div style={{ marginBottom: 6, fontWeight: 600 }}>Нужно поправить ({validation.length}):</div>
                <ul style={{ margin: 0, paddingLeft: 20, fontSize: 13, lineHeight: 1.5 }}>
                  {validation.slice(0, 12).map((e, i) => <li key={i}>{e}</li>)}
                  {validation.length > 12 && <li style={styles.muted}>…ещё {validation.length - 12}</li>}
                </ul>
              </div>
            )}

            <TabNav active={activeTab} onChange={setActiveTab} />

            <div style={styles.splitPane}>
              <div style={styles.formCol}>
                {activeTab === 'expert'   && <ExpertPanel   form={form} update={update} cohortId={activeId} />}
                {activeTab === 'days'     && <DaysPanel     form={form} update={update} cohortId={activeId} />}
                {activeTab === 'schedule' && <SchedulePanel form={form} update={update} />}
                {activeTab === 'welcome'  && <WelcomePanel  form={form} update={update} />}
                {activeTab === 'advanced' && <AdvancedPanel form={form} update={update} cohortId={activeId} />}
              </div>
              <aside style={styles.previewCol}>
                <PreviewPane form={form} sampleName={previewName} />
              </aside>
            </div>
          </CMSShell>
        );
      })()}
    </CMSStyleCtx.Provider>
  );
}

// Self-serve panel shown when the signed-in user lacks role/expertId or
// can see no cohorts. Surfaces their UID + the exact RTDB paths to set
// so a host can copy-paste into the Firebase console without waiting on
// the LifeWheel team for every onboarding.
function AccessHelp({ hint, mode }) {
  const { S, c } = useCMS();
  const uid = (hint && hint.uid) || '(unknown)';
  const role = (hint && hint.role) || '(unset)';
  const expertId = (hint && hint.expertId) || '(unset)';
  const title = mode === 'no_cohorts' ? 'Марафонов под твоим expertId пока нет' : 'Нет доступа к CMS';

  return (
    <div style={{ ...S.card, marginTop: 16 }}>
      <div style={{ ...S.cardTitle, marginBottom: 10 }}>{title}</div>
      <div style={{ ...S.muted, marginBottom: 14, lineHeight: 1.55, color: c.textSecondary }}>
        {mode === 'no_cohorts'
          ? <>Аккаунт привязан как ведущая, но под этим <code>expertId</code> в базе нет когорты. Создай её admin-скриптом или попроси команду LifeWheel.</>
          : <>Чтобы редактировать когорту, у твоего пользователя должны быть проставлены <code>role=expert</code> и <code>expertId</code>. Эти поля защищены правилами RTDB — их выставляет admin вручную в Firebase Console.</>}
      </div>
      <div style={{ ...S.subCardCompact, marginBottom: 12 }}>
        <div style={{ ...S.fieldLabel, marginBottom: 6 }}>Текущее состояние</div>
        <KeyVal label="UID"      value={uid} mono />
        <KeyVal label="role"     value={role} mono />
        <KeyVal label="expertId" value={expertId} mono />
      </div>
      <div style={S.fieldLabel}>Что добавить в Firebase Console → Realtime Database → lifewheel-dev</div>
      <div style={{ ...S.subCardCompact, fontFamily: "'IBM Plex Mono', monospace", fontSize: 13, lineHeight: 1.7, color: c.textPrimary }}>
        <div>/users/<b>{uid}</b>/role = <span style={{ color: c.accent }}>"expert"</span></div>
        <div>/users/<b>{uid}</b>/expertId = <span style={{ color: c.accent }}>"smoke_madinka"</span></div>
      </div>
      <div style={{ ...S.muted, fontSize: 12, marginTop: 10, color: c.textTertiary }}>
        После сохранения перезагрузи страницу.
      </div>
    </div>
  );
}

function KeyVal({ label, value, mono }) {
  const { S, c } = useCMS();
  return (
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginBottom: 4 }}>
      <div style={{ width: 80, fontSize: 11, fontWeight: 600, letterSpacing: 1, textTransform: 'uppercase', color: c.textTertiary }}>{label}</div>
      <div style={{ fontFamily: mono ? "'IBM Plex Mono', monospace" : undefined, fontSize: 13, color: c.textPrimary, wordBreak: 'break-all' }}>{value}</div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Form ↔ manifest translation.
// The form keeps a few fields in human-friendly shapes (datetime-local
// strings, stats with stable client ids, etc.) so React inputs don't
// thrash. We translate at the boundary.
// ──────────────────────────────────────────────────────────────

function manifestToForm(m) {
  const ob = m.onboarding || {};
  const intro = ob.intro || {};
  const wheel = m.wheel || {};
  // Evergreen migration: if the manifest doesn't carry an explicit
  // `registrationOpen` flag yet, default it to `true`. Surface only the
  // optional sunset date (`closesAt`) in the form — the other three legacy
  // date fields are kept on the form so back-compat data round-trips, but
  // they're not exposed to the host anymore.
  const registrationOpen = (typeof m.registrationOpen === 'boolean') ? m.registrationOpen : true;
  return {
    expertId: str(m.expertId),
    potokNumber: numOr(m.potokNumber, 1),
    locale: str(m.locale, 'ru'),
    durationDays: numOr(m.durationDays, 5),
    registrationOpen,
    closesAt: secondsToLocal(m.registrationClosesAt), // optional sunset
    // Legacy date fields preserved verbatim for back-compat with synchronized
    // cohorts. Evergreen saves write 0 for these.
    registrationOpensAt: secondsToLocal(m.registrationOpensAt),
    startsAt: secondsToLocal(m.startsAt),
    registrationClosesAt: secondsToLocal(m.registrationClosesAt),
    endsAt: secondsToLocal(m.endsAt),
    marathonDescription: str(m.marathonDescription),
    onboarding: {
      intro: {
        expertName: str(intro.expertName),
        avatarUrl: str(intro.avatarUrl),
        welcomeCopy: str(intro.welcomeCopy),
        welcomeVideoUrl: str(intro.welcomeVideoUrl),
        expertBio: str(intro.expertBio),
        expertTagline: str(intro.expertTagline),
        expertInstagramUrl: str(intro.expertInstagramUrl),
        expertTelegramUrl: str(intro.expertTelegramUrl),
        expertYoutubeUrl: str(intro.expertYoutubeUrl),
        expertHeroPhoto1Url: str(intro.expertHeroPhoto1Url),
        expertHeroPhoto2Url: str(intro.expertHeroPhoto2Url),
        expertHeroPhoto3Url: str(intro.expertHeroPhoto3Url),
        expertStats: (intro.expertStats || []).map(s => ({
          _key: cryptoLikeKey(),
          value: str(s.value),
          label: str(s.label),
        })),
      },
      quiz: (ob.quiz || []).map(q => ({
        _key: cryptoLikeKey(),
        id: str(q.id),
        type: str(q.type, 'single'),
        question: str(q.question),
        required: !!q.required,
        savesAs: str(q.savesAs),
        options: (q.options || []).map(o => ({ _key: cryptoLikeKey(), text: String(o) })),
      })),
      declarationTemplate: str(ob.declarationTemplate),
    },
    wheel: {
      title: str(wheel.title),
      axes: (wheel.axes || []).map(a => ({
        _key: cryptoLikeKey(),
        key: str(a.key),
        label: str(a.label),
        description: str(a.description),
      })),
      quiz: (wheel.quiz || []).map(q => ({
        _key: cryptoLikeKey(),
        id: str(q.id),
        question: str(q.question),
        options: (q.options || []).map(o => ({
          _key: cryptoLikeKey(),
          text: str(o.text),
          // keep scores as raw object — UI edits as JSON textarea per option
          scores: o.scores || {},
        })),
      })),
    },
    segments: (m.segments || []).map(s => ({
      _key: cryptoLikeKey(),
      id: str(s.id),
      title: str(s.title),
      oneLiner: str(s.oneLiner),
    })),
    days: (m.days || []).map(d => ({
      _key: cryptoLikeKey(),
      day: numOr(d.day, 1),
      kind: str(d.kind, 'shared'),
      // For v1 we only edit shared days. Branched days are preserved
      // as-is in `_rawBranches` so we don't drop content on save.
      _rawBranches: d.branches || null,
      shared: contentToForm(d.kind === 'shared' ? d : (d.shared || {})),
    })),
  };
}

function contentToForm(c) {
  c = c || {};
  const ta = c.taskAction || null;
  const ht = c.habitTemplate || null;
  return {
    title: str(c.title),
    taskCopy: str(c.taskCopy),
    videoUrl: str(c.videoUrl),
    finalCtaUrl: str(c.finalCtaUrl),
    finalCtaLabel: str(c.finalCtaLabel),
    taskAction: ta ? {
      type: str(ta.type, 'createJournalEntry'),
      title: str(ta.title),
      prefillTitle: str(ta.prefillTitle),
      prefillBody: str(ta.prefillBody),
      prompts: (ta.prompts || []).map(p => ({ _key: cryptoLikeKey(), text: str(p) })),
      subtasks: (ta.subtasks || []).map(s => ({ _key: cryptoLikeKey(), text: str(s) })),
    } : null,
    habitTemplate: ht ? {
      title: str(ht.title),
      timesPerDay: numOr(ht.timesPerDay, 1),
      durationMinutes: ht.durationMinutes != null ? numOr(ht.durationMinutes, 5) : null,
      timeOfDay: str(ht.timeOfDay),
    } : null,
  };
}

function formToManifest(f) {
  const intro = f.onboarding.intro;
  // Evergreen mode: write registrationOpen + optional sunset (closesAt).
  // Legacy date fields write 0 — server validation now allows that.
  const sunsetSeconds = localToSeconds(f.closesAt);
  return {
    expertId: f.expertId,
    potokNumber: int(f.potokNumber),
    locale: f.locale,
    durationDays: int(f.durationDays),
    registrationOpen: !!f.registrationOpen,
    registrationOpensAt: 0,
    startsAt: 0,
    registrationClosesAt: sunsetSeconds,
    endsAt: 0,
    marathonDescription: f.marathonDescription || '',
    onboarding: {
      intro: {
        expertName: intro.expertName,
        avatarUrl: intro.avatarUrl || '',
        welcomeCopy: intro.welcomeCopy,
        welcomeVideoUrl: intro.welcomeVideoUrl || '',
        expertBio: intro.expertBio || '',
        expertTagline: intro.expertTagline || '',
        expertInstagramUrl: intro.expertInstagramUrl || '',
        expertTelegramUrl: intro.expertTelegramUrl || '',
        expertYoutubeUrl: intro.expertYoutubeUrl || '',
        expertHeroPhoto1Url: intro.expertHeroPhoto1Url || '',
        expertHeroPhoto2Url: intro.expertHeroPhoto2Url || '',
        expertHeroPhoto3Url: intro.expertHeroPhoto3Url || '',
        expertStats: (intro.expertStats || []).map(s => ({ value: s.value, label: s.label })),
      },
      quiz: (f.onboarding.quiz || []).map(q => ({
        id: q.id,
        type: q.type,
        question: q.question,
        options: (q.type === 'single' || q.type === 'multi')
          ? (q.options || []).map(o => o.text)
          : null,
        required: !!q.required,
        savesAs: q.savesAs || '',
      })).map(stripNulls),
      declarationTemplate: f.onboarding.declarationTemplate,
    },
    wheel: {
      title: f.wheel.title,
      axes: (f.wheel.axes || []).map(a => stripNulls({
        key: a.key,
        label: a.label,
        description: a.description || null,
      })),
      quiz: (f.wheel.quiz || []).map(q => ({
        id: q.id,
        question: q.question,
        options: (q.options || []).map(o => ({
          text: o.text,
          scores: o.scores || {},
        })),
      })),
    },
    segments: (f.segments || []).map(s => ({
      id: s.id,
      title: s.title,
      oneLiner: s.oneLiner || '',
    })),
    days: (f.days || []).map((d, i) => {
      // Always write `day` as 1..N to mirror the mapper guard.
      const base = { day: i + 1, kind: d.kind };
      if (d.kind === 'branched' && d._rawBranches) {
        return { ...base, branches: d._rawBranches };
      }
      const c = d.shared || {};
      const out = {
        ...base,
        title: c.title,
        videoUrl: c.videoUrl || '',
        taskCopy: c.taskCopy,
        finalCtaUrl: c.finalCtaUrl || '',
        finalCtaLabel: c.finalCtaLabel || '',
      };
      if (c.taskAction) {
        out.taskAction = {
          type: c.taskAction.type,
          title: c.taskAction.title,
          prefillTitle: c.taskAction.prefillTitle || '',
          prefillBody: c.taskAction.prefillBody || '',
          prompts:  (c.taskAction.prompts  || []).map(p => p.text).filter(Boolean),
          subtasks: (c.taskAction.subtasks || []).map(s => s.text).filter(Boolean),
        };
      }
      if (c.habitTemplate) {
        out.habitTemplate = {
          title: c.habitTemplate.title,
          timesPerDay: int(c.habitTemplate.timesPerDay) || 1,
          durationMinutes: c.habitTemplate.durationMinutes != null ? int(c.habitTemplate.durationMinutes) : null,
          timeOfDay: c.habitTemplate.timeOfDay || '',
        };
        if (out.habitTemplate.durationMinutes == null) delete out.habitTemplate.durationMinutes;
      }
      return out;
    }),
  };
}

function stripNulls(obj) {
  const out = {};
  for (const k of Object.keys(obj)) if (obj[k] != null) out[k] = obj[k];
  return out;
}

// ──────────────────────────────────────────────────────────────
// Client-side validation. Mirrors server checks so hosts get instant
// feedback. Server is still the source of truth.
// ──────────────────────────────────────────────────────────────

const TASK_TYPES = ['createJournalEntry', 'createGoal', 'createHabit', 'createVisionItem'];
const QUIZ_TYPES = ['single', 'multi', 'text', 'scale'];
const URL_RE = /^(https?:\/\/|asset:|bundle:)/;

function validateForm(f) {
  const errs = [];
  if (!f.expertId) errs.push('Базовое: expertId обязателен');
  if (!Number.isInteger(int(f.potokNumber)) || int(f.potokNumber) < 1) errs.push('Базовое: номер потока ≥ 1');
  if (!f.locale) errs.push('Базовое: язык (locale) обязателен — например "ru"');
  const dd = int(f.durationDays);
  if (!dd || dd < 1 || dd > 60) errs.push('Базовое: длительность 1–60 дней');
  // Evergreen: дата необязательна. Если задана, проверим, что она в будущем.
  if (f.closesAt) {
    const sunset = localToSeconds(f.closesAt);
    if (sunset > 0 && sunset < Math.floor(Date.now() / 1000)) {
      errs.push('Расписание: «Закрыть регистрацию с» — дата уже в прошлом');
    }
  }
  if ((f.days || []).length !== dd) errs.push(`Дни: их ${(f.days || []).length}, а должно быть ${dd}`);

  const intro = f.onboarding.intro;
  if (!intro.expertName) errs.push('Ведущая: имя обязательно');
  if (!intro.welcomeCopy) errs.push('Ведущая: приветствие (welcomeCopy) обязательно');
  for (const [k, label] of [
    ['avatarUrl', 'Аватар'],
    ['welcomeVideoUrl', 'Видео-приветствие'],
    ['expertInstagramUrl', 'Instagram'],
    ['expertTelegramUrl', 'Telegram'],
    ['expertYoutubeUrl', 'YouTube'],
    ['expertHeroPhoto1Url', 'Hero photo 1'],
    ['expertHeroPhoto2Url', 'Hero photo 2'],
    ['expertHeroPhoto3Url', 'Hero photo 3'],
  ]) {
    const v = intro[k];
    if (v && !URL_RE.test(v)) errs.push(`Ведущая: ${label} должен быть http(s):// или asset:`);
  }
  if (!f.onboarding.declarationTemplate) errs.push('Онбординг: декларация-шаблон обязательна');
  (f.onboarding.quiz || []).forEach((q, i) => {
    if (!q.id) errs.push(`Онбординг — вопрос ${i + 1}: id`);
    if (!q.question) errs.push(`Онбординг — вопрос ${i + 1}: текст`);
    if (!QUIZ_TYPES.includes(q.type)) errs.push(`Онбординг — вопрос ${i + 1}: тип`);
  });

  if (!f.wheel.title) errs.push('Колесо: название');
  if (!f.wheel.axes || !f.wheel.axes.length) errs.push('Колесо: добавь хотя бы одну ось');
  (f.wheel.axes || []).forEach((a, i) => {
    if (!a.key) errs.push(`Колесо — ось ${i + 1}: key`);
    if (!a.label) errs.push(`Колесо — ось ${i + 1}: label`);
  });

  (f.days || []).forEach((d, i) => {
    if (d.kind === 'branched') return; // can't edit in v1
    const c = d.shared || {};
    if (!c.title) errs.push(`День ${i + 1}: название`);
    if (!c.taskCopy) errs.push(`День ${i + 1}: задание (taskCopy)`);
    if (c.videoUrl && !URL_RE.test(c.videoUrl)) errs.push(`День ${i + 1}: видео — http(s):// или bundle:`);
    if (c.taskAction) {
      if (!TASK_TYPES.includes(c.taskAction.type)) errs.push(`День ${i + 1}: тип задания`);
      if (!c.taskAction.title) errs.push(`День ${i + 1}: название кнопки задания`);
    }
    if (c.habitTemplate && !c.habitTemplate.title) errs.push(`День ${i + 1}: название привычки`);
  });

  (f.segments || []).forEach((s, i) => {
    if (!s.id) errs.push(`Сегмент ${i + 1}: id`);
    if (!s.title) errs.push(`Сегмент ${i + 1}: title`);
  });
  return errs;
}

// ──────────────────────────────────────────────────────────────
// Shell + toolbar
// ──────────────────────────────────────────────────────────────

function CMSShell({ children }) {
  const { S } = useCMS();
  return (
    <div style={S.shell}>
      <header style={S.header}>
        <div>
          <div style={S.eyebrow}>CMS</div>
          <h1 style={S.h1}>Cohort manifest editor</h1>
        </div>
        <a href="#/marathon" style={S.linkBack}>← Марафон-портал</a>
      </header>
      {children}
    </div>
  );
}

function Toolbar({ cohorts, activeId, onSelect, dirty, validation, saving, savedAt, canSave, onSave, onDiscard, previewName, setPreviewName }) {
  const { S } = useCMS();
  return (
    <div style={S.toolbar}>
      <select value={activeId || ''} onChange={e => onSelect(e.target.value)} style={S.select}>
        {cohorts.map(c => (
          <option key={c.id} value={c.id}>Поток #{c.potokNumber} — {c.id}</option>
        ))}
      </select>

      <div style={{ flex: 1 }} />

      <label style={S.previewNameLabel}>
        <span style={S.muted}>Превью имени:</span>
        <input
          value={previewName}
          onChange={e => setPreviewName(e.target.value)}
          style={{ ...S.input, width: 110, marginLeft: 8 }}
          placeholder="Anna"
        />
      </label>

      {savedAt && !dirty && (
        <span style={{ ...S.muted, marginRight: 12 }}>
          Сохранено {timeAgo(savedAt)}
        </span>
      )}

      {dirty && (
        <button onClick={onDiscard} style={{ ...S.btn, ...S.btnGhost }}>
          Откатить
        </button>
      )}
      <button
        onClick={onSave}
        disabled={!canSave}
        style={{
          ...S.btn,
          ...S.btnPrimary,
          opacity: canSave ? 1 : 0.4,
          cursor: canSave ? 'pointer' : 'not-allowed',
        }}
      >
        {saving ? 'Сохраняю…' : (validation.length ? `Поправь ${validation.length}` : (dirty ? 'Сохранить' : 'Сохранено'))}
      </button>
    </div>
  );
}

function EmptyState({ title, body }) {
  const { S } = useCMS();
  return (
    <div style={{ ...S.card, marginTop: 32 }}>
      <div style={{ ...S.cardTitle, marginBottom: 8 }}>{title}</div>
      <div style={S.muted}>{body}</div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Form sections
// ──────────────────────────────────────────────────────────────

function Section({ title, children, defaultOpen = true, count }) {
  const { S } = useCMS();
  const [open, setOpen] = React.useState(defaultOpen);
  return (
    <section style={S.card}>
      <button
        onClick={() => setOpen(v => !v)}
        style={{
          background: 'none', border: 'none', cursor: 'pointer', color: 'inherit',
          width: '100%', textAlign: 'left', padding: 0,
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          marginBottom: open ? 16 : 0,
        }}>
        <div style={S.cardTitle}>
          {title}
          {count != null && <span style={{ marginLeft: 8, opacity: 0.6 }}>({count})</span>}
        </div>
        <span style={{ ...S.muted, fontSize: 12 }}>{open ? '▾' : '▸'}</span>
      </button>
      {open && <div>{children}</div>}
    </section>
  );
}

function Field({ label, hint, children, full }) {
  const { S } = useCMS();
  return (
    <label style={{ display: 'block', marginBottom: 14, gridColumn: full ? '1 / -1' : 'auto' }}>
      <div style={S.fieldLabel}>{label}</div>
      {children}
      {hint && <div style={S.fieldHint}>{hint}</div>}
    </label>
  );
}

function TextInput({ value, onChange, placeholder, mono, type = 'text' }) {
  const { S } = useCMS();
  return (
    <input
      type={type}
      value={value || ''}
      onChange={e => onChange(e.target.value)}
      placeholder={placeholder}
      style={{ ...S.input, fontFamily: mono ? "'IBM Plex Mono', monospace" : undefined }}
    />
  );
}
function NumberInput({ value, onChange, min, max, step }) {
  const { S } = useCMS();
  return (
    <input
      type="number"
      value={value == null ? '' : value}
      min={min} max={max} step={step}
      onChange={e => onChange(e.target.value === '' ? null : Number(e.target.value))}
      style={{ ...S.input, width: 100 }}
    />
  );
}
function TextArea({ value, onChange, rows = 3, placeholder }) {
  const { S } = useCMS();
  return (
    <textarea
      value={value || ''}
      onChange={e => onChange(e.target.value)}
      rows={rows}
      placeholder={placeholder}
      style={{ ...S.input, fontFamily: "'Inter', sans-serif", resize: 'vertical', minHeight: 60 }}
    />
  );
}
function DateTimeInput({ value, onChange }) {
  const { S } = useCMS();
  return (
    <input
      type="datetime-local"
      value={value || ''}
      onChange={e => onChange(e.target.value)}
      style={S.input}
    />
  );
}
function Select({ value, onChange, options }) {
  const { S } = useCMS();
  return (
    <select value={value || ''} onChange={e => onChange(e.target.value)} style={S.input}>
      {options.map(o => (
        <option key={typeof o === 'string' ? o : o.value} value={typeof o === 'string' ? o : o.value}>
          {typeof o === 'string' ? o : o.label}
        </option>
      ))}
    </select>
  );
}
function Checkbox({ value, onChange, label }) {
  return (
    <label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
      <input type="checkbox" checked={!!value} onChange={e => onChange(e.target.checked)} />
      <span>{label}</span>
    </label>
  );
}

// ──────────────────────────────────────────────────────────────
// Tab navigator. Five focused panels — each maps to one piece of
// the host's mental model, not the underlying schema. Order is
// "what you'll touch most" → "what you'll touch rarely".
// ──────────────────────────────────────────────────────────────

const CMS_TABS = [
  { id: 'expert',   label: 'Эксперт',     hint: 'Карточка ведущей в приложении' },
  { id: 'days',     label: 'Дни',         hint: 'Что происходит каждый день марафона' },
  { id: 'schedule', label: 'Расписание',  hint: 'Старт, конец, длительность' },
  { id: 'welcome',  label: 'Приветствие', hint: 'Первый экран и описание марафона' },
  { id: 'advanced', label: 'Расширенно',  hint: 'Колесо, сегменты, кастомные вопросы' },
];

function TabNav({ active, onChange }) {
  const { S, c } = useCMS();
  return (
    <div style={{ display: 'flex', gap: 6, marginTop: 16, flexWrap: 'wrap' }}>
      {CMS_TABS.map(tab => {
        const isActive = active === tab.id;
        return (
          <button
            key={tab.id}
            onClick={() => onChange(tab.id)}
            title={tab.hint}
            style={{
              ...S.btn,
              padding: '10px 16px',
              borderRadius: 10,
              fontSize: 14,
              fontWeight: isActive ? 600 : 500,
              background: isActive ? c.accentMuted || _hexA(c.accent || '#3EC08D', 0.15) : 'transparent',
              color: isActive ? (c.accent || '#3EC08D') : (c.textSecondary || 'rgba(255,255,255,0.7)'),
              border: `1px solid ${isActive ? (c.borderDefault || _hexA(c.accent || '#3EC08D', 0.35)) : (c.borderSubtle || 'rgba(255,255,255,0.10)')}`,
              cursor: 'pointer',
            }}>
            {tab.label}
          </button>
        );
      })}
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Panel 1: Эксперт — the visible expert card on the marathon
// overview screen. Most-edited surface, so it leads the tab list.
// Order matches the iOS render: hero photos, then headline copy,
// then stats, then socials.
// ──────────────────────────────────────────────────────────────

function ExpertPanel({ form, update, cohortId }) {
  const { S } = useCMS();
  const intro = form.onboarding.intro;
  const set = (k, v) => update(f => f.onboarding.intro[k] = v);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <Section title="Фотографии">
        <div style={{ ...S.fieldHint, marginBottom: 10 }}>
          Три фото в карусели на главном экране марафона. Меняются сами каждые 4.5 секунды.
          Перетащи файл прямо в слот или нажми «Загрузить».
        </div>
        <HeroPhotos
          cohortId={cohortId}
          expertId={form.expertId}
          urls={[intro.expertHeroPhoto1Url, intro.expertHeroPhoto2Url, intro.expertHeroPhoto3Url]}
          onChange={(idx, url) => update(f => {
            const k = `expertHeroPhoto${idx + 1}Url`;
            f.onboarding.intro[k] = url;
          })}
        />
      </Section>

      <Section title="Имя и хук">
        <div style={{ display: 'flex', gap: 16, alignItems: 'flex-start', marginBottom: 14 }}>
          <AvatarUploader
            expertId={form.expertId}
            url={intro.avatarUrl}
            expertName={intro.expertName}
            onChange={v => set('avatarUrl', v)}
          />
          <div style={{ flex: 1, minWidth: 0 }}>
            <Field label="Имя">
              <TextInput value={intro.expertName} onChange={v => set('expertName', v)} placeholder="Madinka" />
            </Field>
            <Field label="Заголовок-хук" hint="Жирная одна строка над био. Лучше с цифрой. Без эмодзи.">
              <TextArea value={intro.expertTagline} onChange={v => set('expertTagline', v)} rows={2}
                placeholder="106 000 подписчиков за 2 года с нуля. Без съёмочной команды." />
            </Field>
          </div>
        </div>
        <Field label="О себе" hint="Кто ты, что делаешь, чему учишь. Два-три предложения.">
          <TextArea value={intro.expertBio} onChange={v => set('expertBio', v)} rows={4}
            placeholder="Madinka запустила Instagram в 2024…" />
        </Field>
      </Section>

      <Section title="Цифры (бейджи под био)">
        <div style={{ ...S.fieldHint, marginBottom: 10 }}>
          Маленькие плашки с твоими цифрами: «106к подписчиков», «2 года роста», «320 учениц». Минимум одна, лучше 2–4.
        </div>
        <StatsEditor stats={intro.expertStats} update={update} />
      </Section>

      <Section title="Социальные сети" defaultOpen={false}>
        <div style={S.grid3}>
          <Field label="Instagram"><TextInput value={intro.expertInstagramUrl} onChange={v => set('expertInstagramUrl', v)} mono placeholder="https://instagram.com/…" /></Field>
          <Field label="Telegram"><TextInput value={intro.expertTelegramUrl} onChange={v => set('expertTelegramUrl', v)} mono placeholder="https://t.me/…" /></Field>
          <Field label="YouTube"><TextInput value={intro.expertYoutubeUrl} onChange={v => set('expertYoutubeUrl', v)} mono placeholder="https://youtube.com/…" /></Field>
        </div>
      </Section>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Panel 2: Дни — the marathon's daily content. Each day is a
// clickable card; click expands the editor in place.
// ──────────────────────────────────────────────────────────────

function DaysPanel({ form, update, cohortId }) {
  const { S } = useCMS();
  // Auto-grow days array when host bumps duration; never shrink silently.
  React.useEffect(() => {
    const dd = int(form.durationDays);
    if (!dd || dd < 1) return;
    if ((form.days || []).length === dd) return;
    if ((form.days || []).length < dd) {
      update(f => {
        while (f.days.length < dd) f.days.push(emptyDay(f.days.length + 1));
      });
    }
  }, [form.durationDays]);

  return (
    <Section title={`Дни марафона · ${(form.days || []).length}`}>
      <div style={{ ...S.fieldHint, marginBottom: 12 }}>
        Один день — один урок, одно задание участнице. Если задания нет, день не будет считаться завершённым.
        Чтобы добавить или убрать дни, измени «Длительность» во вкладке «Расписание».
      </div>
      {form.days.map((d, i) => (
        <DayEditor key={d._key} day={d} index={i} update={update} canRemove={i + 1 > int(form.durationDays)} cohortId={cohortId} />
      ))}
    </Section>
  );
}

// ──────────────────────────────────────────────────────────────
// Panel 3: Расписание — when the marathon runs.
// ──────────────────────────────────────────────────────────────

function SchedulePanel({ form, update }) {
  const { S, c } = useCMS();
  const sunsetSet = !!form.closesAt;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <Section title="Регистрация">
        <div style={{ ...S.fieldHint, marginBottom: 14 }}>
          Каждая участница начинает свой марафон в день, когда присоединилась — её «День 1» это день регистрации.
          Поэтому марафон работает как evergreen-курс: ты включаешь приём — и любая может зайти, когда захочет.
        </div>

        <div style={{
          display: 'flex', alignItems: 'center', gap: 14, padding: 16,
          borderRadius: 12, border: `1px solid ${c.borderSubtle}`,
          background: form.registrationOpen ? (c.accentMuted || 'rgba(62,192,141,0.10)') : 'transparent',
          marginBottom: 14,
        }}>
          <div style={{ fontSize: 22 }}>{form.registrationOpen ? '🟢' : '🔴'}</div>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 600, color: c.textPrimary }}>
              {form.registrationOpen ? 'Регистрация открыта' : 'Регистрация закрыта'}
            </div>
            <div style={{ ...S.fieldHint, marginTop: 2 }}>
              {form.registrationOpen
                ? 'Любая участница, у которой есть код, может присоединиться прямо сейчас.'
                : 'Новые участницы не смогут зайти. Уже стартовавшие проходят марафон спокойно.'}
            </div>
          </div>
          <button
            onClick={() => update(f => f.registrationOpen = !f.registrationOpen)}
            style={{ ...S.btn, ...(form.registrationOpen ? S.btnGhost : S.btnPrimary) }}>
            {form.registrationOpen ? 'Закрыть' : 'Открыть'}
          </button>
        </div>

        <Field label="Закрыть регистрацию автоматически (необязательно)" hint="Если задана дата — после неё новые регистрации блокируются. Оставь пустым, чтобы держать открытым бесконечно.">
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <DateTimeInput value={form.closesAt} onChange={v => update(f => f.closesAt = v)} />
            {sunsetSet && (
              <button onClick={() => update(f => f.closesAt = '')} style={{ ...S.btn, ...S.btnGhost }}>
                Убрать
              </button>
            )}
          </div>
        </Field>
      </Section>

      <Section title="Параметры марафона">
        <div style={S.grid3}>
          <Field label="Длительность (дней)" hint="Сколько дней проходит участница с момента старта">
            <NumberInput value={form.durationDays} onChange={v => update(f => f.durationDays = v)} min={1} max={60} />
          </Field>
          <Field label="Номер потока" hint="Чисто для тебя — на экране у участниц не показывается">
            <NumberInput value={form.potokNumber} onChange={v => update(f => f.potokNumber = v)} min={1} />
          </Field>
          <Field label="Язык" hint="ru / en / uk / pt-BR / it">
            <TextInput value={form.locale} onChange={v => update(f => f.locale = v)} placeholder="ru" />
          </Field>
        </div>
      </Section>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Panel 4: Приветствие — what users see first when they join.
// ──────────────────────────────────────────────────────────────

function WelcomePanel({ form, update }) {
  const { S } = useCMS();
  const intro = form.onboarding.intro;
  const set = (k, v) => update(f => f.onboarding.intro[k] = v);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <Section title="Приветствие в онбординге">
        <Field label="Текст приветствия" hint='Первое сообщение, которое видит участница. Можно «{name}» для подстановки имени.'>
          <TextArea value={intro.welcomeCopy} onChange={v => set('welcomeCopy', v)} rows={4}
            placeholder="За 5 дней ты выйдешь с первой публикацией, {name}…" />
        </Field>
        <Field label="Видео-приветствие (опционально)" hint="URL видео. Можно YouTube/Vimeo/CDN.">
          <TextInput value={intro.welcomeVideoUrl} onChange={v => set('welcomeVideoUrl', v)} mono placeholder="https://…" />
        </Field>
      </Section>

      <Section title="Шаблон декларации">
        <Field label="Шаблон" hint="Участница заполняет свою декларацию по этому шаблону. Подсказки в [квадратных скобках] участница заменит на свои.">
          <TextArea value={form.onboarding.declarationTemplate}
                    onChange={v => update(f => f.onboarding.declarationTemplate = v)} rows={3}
                    placeholder="Я эксперт по [впиши свою нишу], которая делится знаниями ежедневно." />
        </Field>
      </Section>

      <Section title="Описание марафона на главном экране">
        <Field label="О чём марафон" hint="Карточка «О марафоне» на экране после регистрации. Можно «{name}».">
          <TextArea value={form.marathonDescription} onChange={v => update(f => f.marathonDescription = v)} rows={5}
                    placeholder="5 дней — от чистого листа до первой публикации…" />
        </Field>
      </Section>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Panel 5: Расширенно — schema-shaped technical settings.
// Most hosts won't touch these; surface them last, collapsed.
// ──────────────────────────────────────────────────────────────

function AdvancedPanel({ form, update, cohortId }) {
  const { S, c } = useCMS();
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <Section title="Идентификаторы" defaultOpen={false}>
        <div style={S.grid2}>
          <Field label="ID когорты" hint="Изменить нельзя — это ключ в базе.">
            <div style={{ ...S.input, opacity: 0.7, fontFamily: "'IBM Plex Mono', monospace", color: c.textTertiary }}>{cohortId}</div>
          </Field>
          <Field label="Expert ID" hint="Должен совпадать с /users/{твой uid}/expertId">
            <TextInput value={form.expertId} onChange={v => update(f => f.expertId = v)} mono />
          </Field>
        </div>
      </Section>

      <Section title="Кастомные вопросы онбординга" defaultOpen={false} count={(form.onboarding.quiz || []).length}>
        <div style={{ ...S.fieldHint, marginBottom: 12 }}>
          Вопросы внутри онбординга. Если вопрос с <code>savesAs="segmentId"</code>, ответ участницы определит её сегмент в branched-днях.
        </div>
        <CustomQuizEditor form={form} update={update} />
      </Section>

      <Section title="Сегменты" defaultOpen={false} count={(form.segments || []).length}>
        <div style={{ ...S.fieldHint, marginBottom: 12 }}>
          Сегменты используются только при branched-днях. Для обычного 5-дневного марафона нужны как описание на welcome-экране.
        </div>
        <SegmentsEditor form={form} update={update} />
      </Section>

      <Section title="Колесо баланса (CreatorWheel)" defaultOpen={false}>
        <WheelEditor form={form} update={update} />
      </Section>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Editors used by the panels
// ──────────────────────────────────────────────────────────────

function StatsEditor({ stats, update }) {
  const { S } = useCMS();
  return (
    <div style={{ marginBottom: 8 }}>
      {stats.length === 0 && <div style={{ ...S.muted, marginBottom: 8 }}>Пока пусто. Добавь "106к подписчиков", "2 года роста", и т.д.</div>}
      {stats.map((s, i) => (
        <div key={s._key} style={S.rowCompact}>
          <input value={s.value} onChange={e => update(f => f.onboarding.intro.expertStats[i].value = e.target.value)}
                 placeholder="106к" style={{ ...S.input, width: 120 }} />
          <input value={s.label} onChange={e => update(f => f.onboarding.intro.expertStats[i].label = e.target.value)}
                 placeholder="подписчиков" style={{ ...S.input, flex: 1 }} />
          <button onClick={() => update(f => f.onboarding.intro.expertStats.splice(i, 1))} style={S.btnIconDanger}>×</button>
        </div>
      ))}
      <button
        onClick={() => update(f => f.onboarding.intro.expertStats.push({ _key: cryptoLikeKey(), value: '', label: '' }))}
        style={{ ...S.btn, ...S.btnGhost, marginTop: 4 }}>
        + добавить
      </button>
    </div>
  );
}

function HeroPhotos({ cohortId, expertId, urls, onChange }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
      {[0, 1, 2].map(i => (
        <HeroPhotoSlot
          key={i}
          index={i}
          expertId={expertId}
          url={urls[i]}
          onChange={url => onChange(i, url)}
        />
      ))}
    </div>
  );
}

// Avatar uploader — circular preview + drag-drop. Writes to Storage at
// `experts/{expertId}/avatar.jpg`. Falls back to URL paste so legacy
// `asset:Foo` references stay editable.
function AvatarUploader({ expertId, url, expertName, onChange }) {
  const { S, c } = useCMS();
  const [uploading, setUploading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const inputRef = React.useRef();

  async function handleFile(file) {
    if (!file) return;
    if (!/^image\//.test(file.type)) { setError('Это не изображение'); return; }
    if (!expertId) { setError('Сначала задай Expert ID'); return; }
    const safeId = _safeStoragePathSegment(expertId);
    if (!safeId) { setError('Expert ID содержит недопустимые символы'); return; }
    if (!firebase.storage) { setError('Firebase Storage SDK не подключён'); return; }
    setError(null); setUploading(true);
    try {
      const path = `experts/${safeId}/avatar.jpg`;
      const ref = firebase.storage().ref().child(path);
      await ref.put(file, { contentType: file.type, cacheControl: 'public,max-age=31536000' });
      const downloadUrl = await ref.getDownloadURL();
      onChange(downloadUrl);
    } catch (e) {
      console.error('[avatar upload]', e);
      setError(String(e && e.message || e));
    } finally {
      setUploading(false);
    }
  }

  const isAsset = url && url.startsWith('asset:');
  const showImg = url && !isAsset;
  const initial = (expertName || '?').slice(0, 1).toUpperCase();

  return (
    <div style={{ width: 130, flex: '0 0 130px' }}>
      <div style={S.fieldLabel}>Аватар</div>
      <div
        onDragOver={e => { e.preventDefault(); }}
        onDrop={e => { e.preventDefault(); handleFile(e.dataTransfer.files && e.dataTransfer.files[0]); }}
        style={{
          width: 130, height: 130, borderRadius: 65, marginBottom: 8,
          background: c.bgSubtle || 'rgba(255,255,255,0.05)',
          border: `1.5px ${url ? 'solid' : 'dashed'} ${c.borderDefault || 'rgba(62,192,141,0.30)'}`,
          overflow: 'hidden', position: 'relative',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          cursor: 'pointer',
        }}
        onClick={() => inputRef.current && inputRef.current.click()}>
        {showImg ? (
          <img src={url} alt="avatar" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
        ) : isAsset ? (
          <div style={{ textAlign: 'center', padding: 8 }}>
            <div style={{ fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 36, color: c.accent, lineHeight: 1 }}>
              {initial}
            </div>
            <div style={{ ...S.fieldHint, marginTop: 4, fontSize: 10 }}>{url}</div>
          </div>
        ) : (
          <div style={{ textAlign: 'center', color: c.textTertiary, fontSize: 11 }}>
            <div style={{ fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 40, color: c.textTertiary, lineHeight: 1 }}>
              {initial}
            </div>
            <div style={{ marginTop: 4 }}>перетащи или нажми</div>
          </div>
        )}
        {uploading && (
          <div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.55)',
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                        color: 'white', fontSize: 12 }}>
            загружаю…
          </div>
        )}
      </div>
      <input ref={inputRef} type="file" accept="image/*" style={{ display: 'none' }}
             onChange={e => handleFile(e.target.files && e.target.files[0])} />
      <div style={{ display: 'flex', gap: 6 }}>
        <button onClick={() => inputRef.current && inputRef.current.click()}
                style={{ ...S.btn, ...S.btnGhost, flex: 1, fontSize: 12, padding: '6px 8px' }} disabled={uploading}>
          {url ? 'Заменить' : 'Загрузить'}
        </button>
        {url && (
          <button onClick={() => onChange('')} style={S.btnIconDanger} title="Очистить">×</button>
        )}
      </div>
      {error && <div style={{ ...S.fieldHint, color: c.danger || '#E85D5D', marginTop: 6 }}>{error}</div>}
      <div style={{ ...S.fieldHint, marginTop: 8, fontSize: 11 }}>Или вставь ссылку:</div>
      <input
        value={url || ''}
        onChange={e => onChange(e.target.value)}
        placeholder="https://…"
        style={{ ...S.input, fontSize: 11, fontFamily: "'IBM Plex Mono', monospace", padding: '6px 8px' }}
      />
    </div>
  );
}

function HeroPhotoSlot({ index, expertId, url, onChange }) {
  const { S, c } = useCMS();
  const [uploading, setUploading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const inputRef = React.useRef();

  async function handleFile(file) {
    if (!file) return;
    if (!/^image\//.test(file.type)) { setError('not an image'); return; }
    if (!expertId) { setError('Сначала задай Expert ID'); return; }
    const safeId = _safeStoragePathSegment(expertId);
    if (!safeId) { setError('Expert ID содержит недопустимые символы'); return; }
    setError(null); setUploading(true);
    try {
      if (!firebase.storage) throw new Error('Firebase Storage SDK не подключён');
      const storage = firebase.storage();
      const path = `experts/${safeId}/hero_${index + 1}.jpg`;
      const ref = storage.ref().child(path);
      // contentType inferred from file
      await ref.put(file, { contentType: file.type, cacheControl: 'public,max-age=31536000' });
      const downloadUrl = await ref.getDownloadURL();
      onChange(downloadUrl);
    } catch (e) {
      console.error('[hero upload]', e);
      setError(String(e && e.message || e));
    } finally {
      setUploading(false);
    }
  }

  const isAsset = url && url.startsWith('asset:');
  const showImg = url && !isAsset;

  return (
    <div
      onDragOver={e => { e.preventDefault(); }}
      onDrop={e => { e.preventDefault(); handleFile(e.dataTransfer.files && e.dataTransfer.files[0]); }}
      style={S.heroSlot}>
      {showImg && (
        <img src={url} alt={`hero ${index + 1}`} style={{ width: '100%', height: 130, objectFit: 'cover', borderRadius: 8, marginBottom: 8 }} />
      )}
      {isAsset && (
        <div style={{ ...S.heroAssetBadge }}>{url}</div>
      )}
      {!url && (
        <div style={{ height: 130, borderRadius: 8, background: c.bgSubtle,
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      color: c.textTertiary, fontSize: 12, marginBottom: 8 }}>
          Hero {index + 1}
        </div>
      )}
      <input ref={inputRef} type="file" accept="image/*" style={{ display: 'none' }}
             onChange={e => handleFile(e.target.files && e.target.files[0])} />
      <div style={{ display: 'flex', gap: 6 }}>
        <button onClick={() => inputRef.current && inputRef.current.click()}
                style={{ ...S.btn, ...S.btnGhost, flex: 1 }} disabled={uploading}>
          {uploading ? '⏳' : (url ? 'Заменить' : 'Загрузить')}
        </button>
        {url && (
          <button onClick={() => onChange('')} style={S.btnIconDanger}>×</button>
        )}
      </div>
      {error && <div style={{ ...S.fieldHint, color: c.danger || '#E85D5D', marginTop: 6 }}>{error}</div>}
      <div style={{ ...S.fieldHint, marginTop: 6, fontSize: 11 }}>Перетащи файл сюда или нажми</div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Video uploader — drag-drop a file → Firebase Storage. Used for
// each day's lesson video. Storage path:
//   cohorts/{cohortId}/day{N}.{mp4|mov|webm}
// Hosts can also paste a hosted URL (YouTube, Vimeo, CDN) instead
// of uploading — the URL field accepts either.
//
// Storage rule needed (lifewheel-dev):
//   match /cohorts/{cohortId}/{file=**} {
//     allow read: if true;
//     allow write: if request.auth != null;
//   }
// ──────────────────────────────────────────────────────────────

function VideoUploader({ cohortId, dayNumber, url, onChange }) {
  const { S, c } = useCMS();
  const [uploading, setUploading] = React.useState(false);
  const [progress, setProgress] = React.useState(0);
  const [error, setError] = React.useState(null);
  const inputRef = React.useRef();

  const isStorageUrl = url && /firebasestorage\.googleapis\.com|appspot\.com/i.test(url);
  const isHostedUrl  = url && /^https?:\/\//.test(url) && !isStorageUrl;
  const isLegacyBundle = url && url.startsWith('bundle:');

  async function handleFile(file) {
    if (!file) return;
    if (!/^video\//.test(file.type)) { setError('Это не видео-файл'); return; }
    if (!cohortId) { setError('Сначала открой когорту'); return; }
    const safeCohort = _safeStoragePathSegment(cohortId);
    if (!safeCohort) { setError('Cohort ID содержит недопустимые символы'); return; }
    if (!firebase.storage) { setError('Firebase Storage SDK не подключён'); return; }
    setError(null); setUploading(true); setProgress(0);
    try {
      const ext = (file.name.match(/\.([a-z0-9]+)$/i) || [, 'mp4'])[1].toLowerCase();
      const path = `cohorts/${safeCohort}/day${dayNumber}.${ext}`;
      const ref = firebase.storage().ref().child(path);
      const task = ref.put(file, { contentType: file.type, cacheControl: 'public,max-age=31536000' });
      task.on('state_changed', snap => {
        if (snap.totalBytes > 0) setProgress(Math.round(100 * snap.bytesTransferred / snap.totalBytes));
      });
      await task;
      const downloadUrl = await ref.getDownloadURL();
      onChange(downloadUrl);
    } catch (e) {
      console.error('[video upload]', e);
      setError(String(e && e.message || e));
    } finally {
      setUploading(false);
    }
  }

  return (
    <div>
      <div
        onDragOver={e => { e.preventDefault(); }}
        onDrop={e => { e.preventDefault(); handleFile(e.dataTransfer.files && e.dataTransfer.files[0]); }}
        style={{
          ...S.subCardCompact, marginBottom: 8, padding: 14,
          borderStyle: url ? 'solid' : 'dashed',
        }}>
        {url && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
            <span style={{ fontSize: 18 }}>🎬</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 13, color: c.textPrimary, fontWeight: 500 }}>
                {isStorageUrl ? 'Загружено в Firebase Storage'
                 : isHostedUrl ? 'Внешняя ссылка'
                 : isLegacyBundle ? 'Старый формат (видео в бандле приложения)'
                 : 'Видео'}
              </div>
              <div style={{ fontSize: 11, color: c.textTertiary, fontFamily: "'IBM Plex Mono', monospace",
                            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {url}
              </div>
            </div>
            <button onClick={() => onChange('')} style={S.btnIconDanger}>×</button>
          </div>
        )}
        {!url && (
          <div style={{ textAlign: 'center', padding: '14px 8px', color: c.textTertiary, fontSize: 13 }}>
            Перетащи видео сюда или нажми «Загрузить»
          </div>
        )}
        {uploading && (
          <div style={{ marginTop: 8 }}>
            <div style={{ height: 4, borderRadius: 2, background: c.bgSubtle, overflow: 'hidden' }}>
              <div style={{ height: '100%', width: `${progress}%`, background: c.accent, transition: 'width 200ms' }} />
            </div>
            <div style={{ ...S.fieldHint, marginTop: 6, textAlign: 'center' }}>Загружаю… {progress}%</div>
          </div>
        )}
        {error && <div style={{ ...S.fieldHint, color: c.danger || '#E85D5D', marginTop: 6 }}>{error}</div>}
        <input ref={inputRef} type="file" accept="video/*" style={{ display: 'none' }}
               onChange={e => handleFile(e.target.files && e.target.files[0])} />
        <div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
          <button onClick={() => inputRef.current && inputRef.current.click()}
                  style={{ ...S.btn, ...S.btnGhost, flex: 1 }} disabled={uploading}>
            {url ? 'Заменить файл' : 'Загрузить файл'}
          </button>
        </div>
      </div>
      <Field label="…или вставь ссылку (YouTube, Vimeo, CDN)" hint="Если у тебя уже есть видео где-то — просто вставь URL.">
        <TextInput value={url} onChange={onChange} mono placeholder="https://…" />
      </Field>
    </div>
  );
}

function emptyDay(n) {
  return {
    _key: cryptoLikeKey(),
    day: n,
    kind: 'shared',
    _rawBranches: null,
    shared: {
      title: '',
      taskCopy: '',
      videoUrl: '',
      finalCtaUrl: '',
      finalCtaLabel: '',
      taskAction: {
        type: 'createJournalEntry',
        title: '',
        prefillTitle: '',
        prefillBody: '',
        prompts: [],
        subtasks: [],
      },
      habitTemplate: null,
    },
  };
}

// One day card — collapsed by default except day 1. Order matches
// host's mental model: "что мы делаем сегодня → как это снять →
// что участница делает → нужна ли привычка". Schema-shaped fields
// (taskAction internals, finalCta) hide behind a "Расширенно" toggle.
function DayEditor({ day, index, update, canRemove, cohortId }) {
  const { S, c } = useCMS();
  const [showAdvanced, setShowAdvanced] = React.useState(false);
  const cnt = day.shared || {};
  const setC = (k, v) => update(f => f.days[index].shared[k] = v);
  const setT = (k, v) => update(f => f.days[index].shared.taskAction[k] = v);
  const setH = (k, v) => update(f => f.days[index].shared.habitTemplate[k] = v);

  if (day.kind === 'branched') {
    return (
      <div style={S.dayCard}>
        <div style={S.dayHeader}>День {index + 1} · сегментированный</div>
        <div style={{ ...S.muted, padding: '0 16px 16px' }}>
          Этот день разветвлён по сегментам ({Object.keys(day._rawBranches || {}).join(', ')}).
          Пока редактируется через Firebase Console — поддержка в CMS придёт следующей версией.
        </div>
      </div>
    );
  }

  return (
    <details style={S.dayCard} open={index < 1}>
      <summary style={S.dayHeader}>
        <span style={{ color: c.accent, fontFamily: "'Nunito', sans-serif", fontSize: 11, fontWeight: 700, letterSpacing: 1.4, textTransform: 'uppercase', marginRight: 10 }}>
          День {index + 1}
        </span>
        <span style={{ color: c.textPrimary }}>{cnt.title || <span style={S.muted}>без названия</span>}</span>
        {canRemove && (
          <button
            onClick={(e) => { e.preventDefault(); update(f => f.days.splice(index, 1)); }}
            style={{ ...S.btnIconDanger, marginLeft: 'auto' }} title="Удалить день">×</button>
        )}
      </summary>

      <div style={{ padding: '4px 16px 18px' }}>
        <Field label="Название дня" hint="Одно-два слова. Покажется крупно в списке дней.">
          <TextInput value={cnt.title} onChange={v => setC('title', v)} placeholder="Идентичность" />
        </Field>

        <Field label="Что должна сделать участница сегодня" hint='Текст задания. Можно «{name}» — подставится её имя.'>
          <TextArea value={cnt.taskCopy} onChange={v => setC('taskCopy', v)} rows={3}
                    placeholder="{Name}, сформулируй декларацию: кто ты как эксперт и для кого" />
        </Field>

        <div style={S.fieldLabel}>Видео-урок</div>
        <VideoUploader
          cohortId={cohortId}
          dayNumber={index + 1}
          url={cnt.videoUrl}
          onChange={v => setC('videoUrl', v)}
        />

        <div style={S.subTitle}>Привычка дня (необязательно)</div>
        <div style={{ ...S.fieldHint, marginTop: -6, marginBottom: 10 }}>
          Привычка останется в трекере участницы после марафона.
        </div>
        {cnt.habitTemplate ? (
          <div style={S.subCardCompact}>
            <Field label="Название привычки" full>
              <TextInput value={cnt.habitTemplate.title} onChange={v => setH('title', v)} placeholder="Сказать декларацию вслух" />
            </Field>
            <div style={S.grid3}>
              <Field label="Раз в день"><NumberInput value={cnt.habitTemplate.timesPerDay} onChange={v => setH('timesPerDay', v)} min={1} max={20} /></Field>
              <Field label="Длительность, мин"><NumberInput value={cnt.habitTemplate.durationMinutes} onChange={v => setH('durationMinutes', v)} min={1} max={120} /></Field>
              <Field label="Время суток">
                <Select value={cnt.habitTemplate.timeOfDay || ''} onChange={v => setH('timeOfDay', v)}
                        options={[
                          { value: '',          label: 'любое' },
                          { value: 'morning',   label: 'утро' },
                          { value: 'afternoon', label: 'день' },
                          { value: 'evening',   label: 'вечер' },
                          { value: 'anytime',   label: 'всегда доступна' },
                        ]} />
              </Field>
            </div>
            <button onClick={() => update(f => f.days[index].shared.habitTemplate = null)} style={{ ...S.btn, ...S.btnGhost }}>
              Убрать привычку
            </button>
          </div>
        ) : (
          <button
            onClick={() => update(f => f.days[index].shared.habitTemplate = { title: '', timesPerDay: 1, durationMinutes: null, timeOfDay: '' })}
            style={{ ...S.btn, ...S.btnGhost }}>
            + добавить привычку дня
          </button>
        )}

        <button
          onClick={() => setShowAdvanced(v => !v)}
          style={{ ...S.btn, ...S.btnGhost, marginTop: 18, fontSize: 12, color: c.textTertiary, padding: '6px 12px' }}>
          {showAdvanced ? '▾' : '▸'} Расширенно — что создаётся в приложении
        </button>

        {showAdvanced && (
          <div style={{ ...S.subCard, marginTop: 10 }}>
            <div style={{ ...S.fieldHint, marginBottom: 10 }}>
              Когда участница нажимает кнопку задания, в её LifeWheel создаётся артефакт: запись в дневник, цель, привычка или тайл в Vision Space. Этим вы превращаете контент марафона в реальные данные в приложении.
            </div>
            {cnt.taskAction ? (
              <div>
                <div style={S.grid2}>
                  <Field label="Что создать">
                    <Select value={cnt.taskAction.type} onChange={v => setT('type', v)} options={[
                      { value: 'createJournalEntry', label: 'Запись в дневник' },
                      { value: 'createGoal',         label: 'Цель с подзадачами' },
                      { value: 'createHabit',        label: 'Привычка' },
                      { value: 'createVisionItem',   label: 'Тайл Vision Space' },
                    ]} />
                  </Field>
                  <Field label="Текст кнопки в приложении">
                    <TextInput value={cnt.taskAction.title} onChange={v => setT('title', v)} placeholder="Записать декларацию" />
                  </Field>
                  <Field label="Префилл названия">
                    <TextInput value={cnt.taskAction.prefillTitle} onChange={v => setT('prefillTitle', v)} placeholder="Моя декларация эксперта" />
                  </Field>
                  <Field label="Префилл текста">
                    <TextInput value={cnt.taskAction.prefillBody} onChange={v => setT('prefillBody', v)} placeholder="Я эксперт по…" />
                  </Field>
                </div>
                <div style={S.fieldLabel}>Подсказки-вопросы (для дневника)</div>
                <ListEditor
                  items={cnt.taskAction.prompts}
                  onAdd={() => update(f => f.days[index].shared.taskAction.prompts.push({ _key: cryptoLikeKey(), text: '' }))}
                  onChange={(i2, text) => update(f => f.days[index].shared.taskAction.prompts[i2].text = text)}
                  onRemove={(i2) => update(f => f.days[index].shared.taskAction.prompts.splice(i2, 1))}
                  placeholder="Кому конкретно ты помогаешь?"
                />
                <div style={{ ...S.fieldLabel, marginTop: 12 }}>Подзадачи (для цели)</div>
                <ListEditor
                  items={cnt.taskAction.subtasks}
                  onAdd={() => update(f => f.days[index].shared.taskAction.subtasks.push({ _key: cryptoLikeKey(), text: '' }))}
                  onChange={(i2, text) => update(f => f.days[index].shared.taskAction.subtasks[i2].text = text)}
                  onRemove={(i2) => update(f => f.days[index].shared.taskAction.subtasks.splice(i2, 1))}
                  placeholder="Поставить телефон вертикально"
                />
                <button onClick={() => update(f => f.days[index].shared.taskAction = null)} style={{ ...S.btn, ...S.btnGhost, marginTop: 12 }}>
                  Убрать task action
                </button>
              </div>
            ) : (
              <button
                onClick={() => update(f => f.days[index].shared.taskAction = {
                  type: 'createJournalEntry', title: '', prefillTitle: '', prefillBody: '', prompts: [], subtasks: [],
                })}
                style={{ ...S.btn, ...S.btnGhost }}>
                + добавить task action
              </button>
            )}

            <div style={{ ...S.subTitle, marginTop: 18 }}>Финальный CTA (только последний день)</div>
            <div style={S.grid2}>
              <Field label="Текст кнопки">
                <TextInput value={cnt.finalCtaLabel} onChange={v => setC('finalCtaLabel', v)} placeholder="Перейти на курс →" />
              </Field>
              <Field label="URL">
                <TextInput value={cnt.finalCtaUrl} onChange={v => setC('finalCtaUrl', v)} mono placeholder="https://…" />
              </Field>
            </div>
          </div>
        )}
      </div>
    </details>
  );
}

function ListEditor({ items, onAdd, onChange, onRemove, placeholder }) {
  const { S } = useCMS();
  return (
    <div>
      {items.map((it, i) => (
        <div key={it._key || i} style={S.rowCompact}>
          <input value={it.text} onChange={e => onChange(i, e.target.value)}
                 placeholder={placeholder} style={{ ...S.input, flex: 1 }} />
          <button onClick={() => onRemove(i)} style={S.btnIconDanger}>×</button>
        </div>
      ))}
      <button onClick={onAdd} style={{ ...S.btn, ...S.btnGhost, marginTop: 4 }}>+ строка</button>
    </div>
  );
}

// Custom onboarding quiz — used inside AdvancedPanel.
function CustomQuizEditor({ form, update }) {
  const { S } = useCMS();
  const ob = form.onboarding;
  return (
    <div>
      {(ob.quiz || []).map((q, i) => (
        <div key={q._key} style={S.subCard}>
          <div style={S.grid2}>
            <Field label="ID"><TextInput value={q.id} onChange={v => update(f => f.onboarding.quiz[i].id = v)} mono placeholder={`q${i + 1}`} /></Field>
            <Field label="Тип ответа">
              <Select value={q.type} onChange={v => update(f => f.onboarding.quiz[i].type = v)} options={[
                { value: 'single', label: 'Один из вариантов' },
                { value: 'multi',  label: 'Несколько вариантов' },
                { value: 'text',   label: 'Свободный текст' },
                { value: 'scale',  label: 'Шкала' },
              ]} />
            </Field>
            <Field label="Вопрос" full>
              <TextInput value={q.question} onChange={v => update(f => f.onboarding.quiz[i].question = v)} />
            </Field>
            <Field label="Сохранить как" hint='Если "segmentId" — ответ определяет сегмент участницы.'>
              <TextInput value={q.savesAs} onChange={v => update(f => f.onboarding.quiz[i].savesAs = v)} mono />
            </Field>
            <Field label="Обязательный?">
              <Checkbox value={q.required} onChange={v => update(f => f.onboarding.quiz[i].required = v)} label="да" />
            </Field>
          </div>
          {(q.type === 'single' || q.type === 'multi') && (
            <div>
              <div style={S.fieldLabel}>Варианты ответа</div>
              <ListEditor
                items={q.options}
                onAdd={() => update(f => f.onboarding.quiz[i].options.push({ _key: cryptoLikeKey(), text: '' }))}
                onChange={(j, text) => update(f => f.onboarding.quiz[i].options[j].text = text)}
                onRemove={(j) => update(f => f.onboarding.quiz[i].options.splice(j, 1))}
                placeholder="Эксперт без блога"
              />
            </div>
          )}
          <button onClick={() => update(f => f.onboarding.quiz.splice(i, 1))} style={{ ...S.btn, ...S.btnGhost, marginTop: 8 }}>
            Удалить вопрос
          </button>
        </div>
      ))}
      <button
        onClick={() => update(f => f.onboarding.quiz.push({
          _key: cryptoLikeKey(),
          id: 'q' + (f.onboarding.quiz.length + 1),
          type: 'single', question: '', required: true, savesAs: '', options: [],
        }))}
        style={{ ...S.btn, ...S.btnGhost, marginTop: 8 }}>
        + добавить вопрос
      </button>
    </div>
  );
}

// Wheel editor — used inside AdvancedPanel.
function WheelEditor({ form, update }) {
  const { S } = useCMS();
  const w = form.wheel;
  return (
    <div>
      <Field label="Название колеса">
        <TextInput value={w.title} onChange={v => update(f => f.wheel.title = v)} placeholder="Колесо баланса эксперта" />
      </Field>

      <div style={S.subTitle}>Оси колеса</div>
      {w.axes.map((a, i) => (
        <div key={a._key} style={S.subCardCompact}>
          <div style={S.grid3}>
            <Field label="Ключ (ID)"><TextInput value={a.key} onChange={v => update(f => f.wheel.axes[i].key = v)} mono /></Field>
            <Field label="Подпись"><TextInput value={a.label} onChange={v => update(f => f.wheel.axes[i].label = v)} /></Field>
            <Field label="Описание"><TextInput value={a.description} onChange={v => update(f => f.wheel.axes[i].description = v)} /></Field>
          </div>
          <button onClick={() => update(f => f.wheel.axes.splice(i, 1))} style={{ ...S.btn, ...S.btnGhost }}>
            Удалить ось
          </button>
        </div>
      ))}
      <button
        onClick={() => update(f => f.wheel.axes.push({ _key: cryptoLikeKey(), key: '', label: '', description: '' }))}
        style={{ ...S.btn, ...S.btnGhost, marginBottom: 16 }}>
        + ось
      </button>

      <div style={S.subTitle}>Вопросы колеса</div>
      <div style={{ ...S.muted, marginBottom: 8, fontSize: 12 }}>
        Скоринг каждого варианта пока редактируется как JSON: <code>{'{"camera": 5, "stability": 2}'}</code>. Визуальный редактор — следующая версия.
      </div>
      {w.quiz.map((q, i) => (
        <div key={q._key} style={S.subCard}>
          <div style={S.grid2}>
            <Field label="ID"><TextInput value={q.id} onChange={v => update(f => f.wheel.quiz[i].id = v)} mono /></Field>
            <Field label="Вопрос" full><TextInput value={q.question} onChange={v => update(f => f.wheel.quiz[i].question = v)} /></Field>
          </div>
          {q.options.map((o, j) => (
            <div key={o._key} style={S.rowCompact}>
              <input value={o.text} onChange={e => update(f => f.wheel.quiz[i].options[j].text = e.target.value)}
                     placeholder="Текст варианта" style={{ ...S.input, flex: 1 }} />
              <input
                value={JSON.stringify(o.scores || {})}
                onChange={e => {
                  try { const parsed = JSON.parse(e.target.value); update(f => f.wheel.quiz[i].options[j].scores = parsed); }
                  catch { /* keep typing */ }
                }}
                placeholder='{"camera": 5}'
                style={{ ...S.input, flex: 1, fontFamily: "'IBM Plex Mono', monospace", fontSize: 12 }}
              />
              <button onClick={() => update(f => f.wheel.quiz[i].options.splice(j, 1))} style={S.btnIconDanger}>×</button>
            </div>
          ))}
          <button
            onClick={() => update(f => f.wheel.quiz[i].options.push({ _key: cryptoLikeKey(), text: '', scores: {} }))}
            style={{ ...S.btn, ...S.btnGhost, marginRight: 6 }}>
            + вариант
          </button>
          <button onClick={() => update(f => f.wheel.quiz.splice(i, 1))} style={{ ...S.btn, ...S.btnGhost }}>
            Удалить вопрос
          </button>
        </div>
      ))}
      <button
        onClick={() => update(f => f.wheel.quiz.push({ _key: cryptoLikeKey(), id: 'wq' + (f.wheel.quiz.length + 1), question: '', options: [] }))}
        style={{ ...S.btn, ...S.btnGhost }}>
        + вопрос колеса
      </button>
    </div>
  );
}

// Segments editor — used inside AdvancedPanel.
function SegmentsEditor({ form, update }) {
  const { S } = useCMS();
  return (
    <div>
      {form.segments.map((s, i) => (
        <div key={s._key} style={S.subCardCompact}>
          <div style={S.grid3}>
            <Field label="ID"><TextInput value={s.id} onChange={v => update(f => f.segments[i].id = v)} mono placeholder="A" /></Field>
            <Field label="Название"><TextInput value={s.title} onChange={v => update(f => f.segments[i].title = v)} /></Field>
            <Field label="Одна строка"><TextInput value={s.oneLiner} onChange={v => update(f => f.segments[i].oneLiner = v)} /></Field>
          </div>
          <button onClick={() => update(f => f.segments.splice(i, 1))} style={{ ...S.btn, ...S.btnGhost }}>
            Удалить
          </button>
        </div>
      ))}
      <button onClick={() => update(f => f.segments.push({ _key: cryptoLikeKey(), id: '', title: '', oneLiner: '' }))} style={{ ...S.btn, ...S.btnGhost }}>
        + сегмент
      </button>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Preview pane — structural mirror of CohortOverviewView. Not pixel
// perfect; goal is for hosts to see roughly what users will see.
// ──────────────────────────────────────────────────────────────

function PreviewPane({ form, sampleName }) {
  const { S } = useCMS();
  const intro = form.onboarding.intro;
  const heroes = [intro.expertHeroPhoto1Url, intro.expertHeroPhoto2Url, intro.expertHeroPhoto3Url].filter(Boolean);
  return (
    <div style={S.previewBox}>
      <div style={S.previewLabel}>iOS preview · {form.locale}</div>
      <div style={S.previewPhone}>
        <div style={S.previewScroll}>
          <div style={{ textAlign: 'center', padding: '24px 16px 8px' }}>
            <div style={{
              width: 64, height: 64, borderRadius: 32, margin: '0 auto 10px',
              background: 'rgba(62,192,141,0.15)', border: '1.4px solid rgba(62,192,141,0.5)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 26,
              color: '#3EC08D',
            }}>
              {(intro.expertName || '?').slice(0, 1)}
            </div>
            <div style={{ fontFamily: "'Nunito', sans-serif", fontSize: 10, fontWeight: 700, letterSpacing: 1.6,
                          textTransform: 'uppercase', color: '#3EC08D', marginBottom: 6 }}>
              ДЕНЬ 1 ИЗ {form.durationDays || '?'}
            </div>
            <div style={{ fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 22, color: 'rgba(255,255,255,0.92)' }}>
              Марафон с {intro.expertName || '…'}
            </div>
          </div>

          {heroes.length > 0 && (
            <div style={{ position: 'relative' }}>
              <img src={heroes[0]} alt="" style={{ width: '100%', height: 220, objectFit: 'cover' }} />
              <div style={{
                position: 'absolute', inset: 0,
                background: 'linear-gradient(to bottom, transparent 50%, rgba(0,0,0,0.55))',
              }} />
              <div style={{ position: 'absolute', bottom: 14, left: 14, color: '#fff' }}>
                <div style={{ fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 22 }}>{intro.expertName}</div>
                <div style={{ fontFamily: "'Nunito', sans-serif", fontSize: 11, letterSpacing: 1.4, textTransform: 'uppercase', opacity: 0.85 }}>Reels-эксперт</div>
              </div>
            </div>
          )}

          <div style={{ padding: 16 }}>
            <div style={S.previewSectionLabel}>ВЕДУЩАЯ</div>
            {intro.expertTagline && (
              <div style={{ fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 17, lineHeight: 1.45, color: 'rgba(255,255,255,0.92)', marginBottom: 12 }}>
                {personalize(intro.expertTagline, sampleName)}
              </div>
            )}
            {(intro.expertStats || []).length > 0 && (
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginBottom: 12 }}>
                {intro.expertStats.map((s, i) => (
                  <div key={i} style={{
                    padding: '12px 8px', borderRadius: 10,
                    background: 'rgba(62,192,141,0.10)', border: '1px solid rgba(62,192,141,0.22)',
                    textAlign: 'center',
                  }}>
                    <div style={{ fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 22, color: '#3EC08D' }}>
                      {s.value || '—'}
                    </div>
                    <div style={{ fontFamily: "'Nunito', sans-serif", fontSize: 10, color: 'rgba(255,255,255,0.65)' }}>
                      {s.label}
                    </div>
                  </div>
                ))}
              </div>
            )}
            {intro.expertBio && (
              <div style={{ fontFamily: 'Lora, Georgia, serif', fontStyle: 'italic', fontSize: 13, lineHeight: 1.6, color: 'rgba(255,255,255,0.7)' }}>
                {personalize(intro.expertBio, sampleName)}
              </div>
            )}
          </div>

          {form.marathonDescription && (
            <div style={{ padding: 16 }}>
              <div style={S.previewSectionLabel}>О МАРАФОНЕ</div>
              <div style={{ fontFamily: 'Lora, Georgia, serif', fontStyle: 'italic', fontSize: 14, lineHeight: 1.65, color: 'rgba(255,255,255,0.85)' }}>
                {personalize(form.marathonDescription, sampleName)}
              </div>
            </div>
          )}

          <div style={{ padding: '0 16px 24px' }}>
            <div style={S.previewSectionLabel}>ДНИ</div>
            {form.days.map((d, i) => {
              const c = d.shared || {};
              return (
                <div key={d._key} style={{
                  padding: 12, marginBottom: 8, borderRadius: 12,
                  background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
                  display: 'flex', alignItems: 'center', gap: 12,
                }}>
                  <div style={{
                    width: 36, height: 36, borderRadius: 18,
                    background: i === 0 ? 'rgba(62,192,141,0.18)' : 'rgba(255,255,255,0.04)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 16,
                    color: 'rgba(255,255,255,0.9)',
                  }}>
                    {i + 1}
                  </div>
                  <div>
                    <div style={{ fontFamily: "'Nunito', sans-serif", fontSize: 9, fontWeight: 700, letterSpacing: 1.4, textTransform: 'uppercase', color: '#3EC08D' }}>
                      ДЕНЬ {i + 1}
                    </div>
                    <div style={{ fontFamily: "'Playfair Display', serif", fontStyle: 'italic', fontSize: 16, color: 'rgba(255,255,255,0.95)' }}>
                      {c.title || '(название)'}
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}

function personalize(template, name) {
  if (!template) return '';
  const safeName = name || 'Anna';
  return String(template)
    .replace(/\{name\}/g, safeName)
    .replace(/\{Name\}/g, safeName.charAt(0).toUpperCase() + safeName.slice(1));
}

// ──────────────────────────────────────────────────────────────
// Tiny helpers
// ──────────────────────────────────────────────────────────────

function str(v, fallback = '') { return (v == null) ? fallback : String(v); }
function int(v) { const n = Number(v); return Number.isFinite(n) ? Math.trunc(n) : 0; }
function numOr(v, fallback) { const n = Number(v); return Number.isFinite(n) ? n : fallback; }

function secondsToLocal(secondsLike) {
  const s = Number(secondsLike);
  if (!Number.isFinite(s) || s <= 0) return '';
  const d = new Date(s * 1000);
  // Format as "YYYY-MM-DDTHH:MM" in LOCAL time for <input type="datetime-local">
  const pad = n => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

function localToSeconds(localStr) {
  if (!localStr) return 0;
  const ms = new Date(localStr).getTime();
  return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
}

function cryptoLikeKey() { return 'k_' + Math.random().toString(36).slice(2, 10); }

// Storage path segments come from form state (expertId, cohortId). Strip any
// character that could escape the intended bucket-relative prefix — Firebase
// Storage's compat SDK does not reject `/`, so an `expertId` like `../foo`
// would resolve to a path the host should not be able to write.
function _safeStoragePathSegment(s) {
  return String(s || '').replace(/[^a-zA-Z0-9_\-]/g, '_').replace(/^_+|_+$/g, '');
}

function timeAgo(ts) {
  if (!ts) return '';
  const sec = Math.round((Date.now() - ts) / 1000);
  if (sec < 5) return 'только что';
  if (sec < 60) return `${sec}с назад`;
  const min = Math.round(sec / 60);
  if (min < 60) return `${min} мин назад`;
  const hr = Math.round(min / 60);
  return `${hr} ч назад`;
}

// ──────────────────────────────────────────────────────────────
// Styles — derived from the active coach-portal theme palette so
// the CMS works in dark, light, and any accent recolor (emerald,
// midnight, rose, ember). Pulled fresh on every theme change.
// All text colors come from `c.textPrimary/Secondary/Tertiary` so
// they pass the WCAG AA contrast bar regardless of mode.
// (See `design/contrast_requirement.md`.)
// ──────────────────────────────────────────────────────────────

function makeStyles(c, fonts) {
  const bodyFont = (fonts && fonts.body)    || "'Inter', system-ui, sans-serif";
  const dispFont = (fonts && fonts.display) || "'Fraunces', Georgia, serif";
  const monoFont = (fonts && fonts.mono)    || "'IBM Plex Mono', monospace";
  const accent      = c.accent       || '#3EC08D';
  const accentMuted = c.accentMuted  || _hexA(accent, 0.15);
  const border      = c.borderSubtle || _hexA(accent, 0.18);
  const borderStrong= c.borderDefault|| _hexA(accent, 0.30);
  const textPrim    = c.textPrimary  || 'rgba(255,255,255,0.92)';
  const textSec     = c.textSecondary|| 'rgba(255,255,255,0.65)';
  const textTer     = c.textTertiary || 'rgba(255,255,255,0.45)';
  const surface     = c.elevated     || 'rgba(255,255,255,0.04)';
  const surfaceSub  = c.card         || 'rgba(255,255,255,0.02)';
  const inputBg     = c.bg           || 'rgba(0,0,0,0.25)';
  const danger      = c.danger       || '#E85D5D';

  return {
    shell:      { padding: '32px 28px', maxWidth: 1400, margin: '0 auto', fontFamily: bodyFont, color: textPrim },
    header:     { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 20 },
    eyebrow:    { fontSize: 11, fontWeight: 700, letterSpacing: 1.6, textTransform: 'uppercase', color: textTer },
    h1:         { margin: '4px 0 0', fontFamily: dispFont, fontWeight: 600, fontSize: 30, letterSpacing: -0.5, color: textPrim },
    linkBack:   { color: accent, textDecoration: 'none', fontSize: 14 },

    toolbar:    { display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px', borderRadius: 12,
                  background: accentMuted, border: `1px solid ${borderStrong}` },
    select:     { background: inputBg, color: textPrim, border: `1px solid ${border}`,
                  padding: '8px 12px', borderRadius: 8, fontSize: 14 },
    previewNameLabel: { display: 'inline-flex', alignItems: 'center', fontSize: 12, color: textSec, marginRight: 12 },

    splitPane:  { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 380px', gap: 24, marginTop: 16, alignItems: 'flex-start' },
    formCol:    { display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 },
    previewCol: { position: 'sticky', top: 16 },

    card:       { background: surface, border: `1px solid ${border}`, borderRadius: 14, padding: 20 },
    cardTitle:  { fontSize: 12, textTransform: 'uppercase', letterSpacing: 1.4, color: textSec, fontWeight: 700 },
    subTitle:   { fontSize: 11, textTransform: 'uppercase', letterSpacing: 1.3, color: textSec, margin: '20px 0 10px', fontWeight: 700 },
    subCard:    { padding: 14, borderRadius: 10, background: surfaceSub, border: `1px solid ${border}`, marginBottom: 10 },
    subCardCompact: { padding: 12, borderRadius: 10, background: surfaceSub, border: `1px solid ${border}`, marginBottom: 8 },
    muted:      { color: textSec },

    grid2:      { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 },
    grid3:      { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 },

    fieldLabel: { fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1.1, color: textSec, marginBottom: 6 },
    fieldHint:  { fontSize: 11, color: textTer, marginTop: 4, lineHeight: 1.4 },

    input:      { width: '100%', padding: '8px 10px', borderRadius: 8,
                  background: inputBg, color: textPrim,
                  border: `1px solid ${border}`, fontSize: 13, outline: 'none' },

    rowCompact: { display: 'flex', gap: 6, alignItems: 'center', marginBottom: 6 },

    btn:        { padding: '8px 14px', borderRadius: 8, fontSize: 13, fontWeight: 500, cursor: 'pointer',
                  border: '1px solid transparent', background: 'transparent', color: textPrim },
    btnPrimary: { background: accent, color: c.ink || '#0C1A12', border: `1px solid ${accent}`, fontWeight: 600 },
    btnGhost:   { border: `1px solid ${border}`, color: textPrim, background: 'transparent' },
    btnIconDanger: { width: 28, height: 28, padding: 0, borderRadius: 6, fontSize: 16,
                     background: _hexA(danger, 0.10), color: danger,
                     border: `1px solid ${_hexA(danger, 0.30)}`, cursor: 'pointer' },

    dayCard:    { borderRadius: 12, background: surfaceSub, border: `1px solid ${border}`, marginBottom: 10 },
    dayHeader:  { padding: '14px 16px', cursor: 'pointer', fontFamily: dispFont, fontSize: 17,
                  color: textPrim, display: 'flex', alignItems: 'center', userSelect: 'none' },

    heroSlot:   { padding: 10, borderRadius: 12, background: surfaceSub, border: `1px dashed ${borderStrong}` },
    heroAssetBadge: { padding: '8px 10px', borderRadius: 8, background: accentMuted,
                      color: accent, fontFamily: monoFont, fontSize: 12, marginBottom: 8, height: 130,
                      display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center' },

    previewBox:        { background: surface, border: `1px solid ${border}`, borderRadius: 14, padding: 14 },
    previewLabel:      { fontSize: 11, fontWeight: 700, letterSpacing: 1.4, textTransform: 'uppercase',
                         color: textTer, marginBottom: 10, textAlign: 'center' },
    // Phone preview always shows in dark mode — that's how the iOS app actually looks.
    previewPhone:      { background: '#0C1A12', borderRadius: 22, overflow: 'hidden',
                         border: '1px solid rgba(255,255,255,0.10)', maxHeight: '78vh', overflowY: 'auto' },
    previewScroll:     { paddingBottom: 24 },
    previewSectionLabel: { fontSize: 10, fontWeight: 700, letterSpacing: 1.6, textTransform: 'uppercase',
                           color: '#3EC08D', marginBottom: 10 },
  };
}

// Fallback theme used when the CMS renders outside <LWThemeProvider>
// (e.g. a route loaded before app.html wraps the tree). Matches the
// dark-emerald defaults the rest of the portal ships with.
function _fallbackPalette() {
  return {
    accent:        '#3EC08D',
    accentMuted:   'rgba(62,192,141,0.15)',
    bg:            '#0F1316',
    bgSubtle:      '#0F1316',
    card:          'rgba(255,255,255,0.03)',
    elevated:      'rgba(255,255,255,0.05)',
    borderSubtle:  'rgba(62,192,141,0.18)',
    borderDefault: 'rgba(62,192,141,0.30)',
    textPrimary:   'rgba(255,255,255,0.92)',
    textSecondary: 'rgba(255,255,255,0.65)',
    textTertiary:  'rgba(255,255,255,0.45)',
    danger:        '#E85D5D',
    warning:       '#E8943A',
    ink:           '#0C1A12',
  };
}
function _fallbackFonts() {
  return {
    body:    "'Inter', system-ui, sans-serif",
    display: "'Fraunces', Georgia, serif",
    mono:    "'IBM Plex Mono', monospace",
  };
}
function _hexA(hex, a) {
  // Tiny hex → rgba helper so makeStyles never depends on global hexToRgba.
  if (!hex || hex[0] !== '#') return `rgba(0,0,0,${a})`;
  let h = hex.slice(1);
  if (h.length === 3) h = h.split('').map(ch => ch + ch).join('');
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r},${g},${b},${a})`;
}

window.LWCohortCMS = LWCohortCMS;
