// Dynamo Authoring — main app composition.

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "dark": false,
  "density": "regular",
  "rail": "expanded",
  "showAiBadges": true,
  "showValidationDot": true,
  "selectedLayoutType": "sequence",
  "viewAs": "author",
  "rolesStartBlank": false
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [surface, setSurface] = React.useState('dynamo-home');
  const [orgId, setOrgId] = React.useState('mohg');
  const [selectedModuleId, setSelectedModuleId] = React.useState('M2');
  const [selectedLayoutId, setSelectedLayoutId] = React.useState('M2-L3');
  const [moduleOrder, setModuleOrder] = React.useState(null); // null = use default ordering
  const [layoutOrders, setLayoutOrders] = React.useState({}); // { [moduleId]: layoutId[] }
  const [groupTitleOverrides, setGroupTitleOverrides] = React.useState({}); // { [groupId]: title }
  const [moduleOverrides, setModuleOverrides] = React.useState({}); // { [moduleId]: { title?, summary? } }
  const [layoutStatusOverrides, setLayoutStatusOverrides] = React.useState({}); // { [layoutId]: 'pending'|'proposed'|'accepted'|'issues' }
  const [layoutDrafts, setLayoutDrafts] = React.useState({}); // { [layoutId]: layoutDataObject }
  const updateDraft = React.useCallback((layoutId, patch) =>
    setLayoutDrafts(d => ({ ...d, [layoutId]: { ...(d[layoutId] || {}), ...patch } })), []);
  // Course-level settings slice — the single source of truth for the Course
  // settings surface (Surface 7 · SurfaceBrand). Lives in app state, not
  // localStorage, so edits persist across surface navigation but reset on
  // browser refresh (expected — see Course_settings_design audit #2).
  const [courseSettings, setCourseSettings] = React.useState(window.SAMPLE_COURSE_SETTINGS);
  const [addedModules, setAddedModules] = React.useState([]); // user-created modules
  const [addedGroups, setAddedGroups]   = React.useState([]); // user-created module groups
  const [addedLayouts, setAddedLayouts] = React.useState({}); // { [moduleId]: layout[] } — layouts appended to a module
  const [deletedLayoutIds, setDeletedLayoutIds] = React.useState([]); // hidden layouts
  const [deletedModuleIds, setDeletedModuleIds] = React.useState([]); // hidden modules
  const [deletedGroupIds, setDeletedGroupIds]   = React.useState([]); // hidden chapters (+ their modules)
  const [showValidation, setShowValidation] = React.useState(false);
  const [showCmdK, setShowCmdK] = React.useState(false);
  const [showPreview, setShowPreview] = React.useState(false);

  const currentOrg = window.SAMPLE_ORGS.find(o => o.id === orgId) || window.SAMPLE_ORGS[0];
  // Build a per-org course shell so the breadcrumb shows org-appropriate brand
  const liveCourse = React.useMemo(() => {
    const base = SAMPLE_COURSE;
    const allModules = [...base.modules, ...addedModules];
    const orderedModules = moduleOrder
      ? [
          ...moduleOrder.map(id => allModules.find(m => m.id === id)).filter(Boolean),
          // append any new modules not yet reflected in the saved ordering
          ...allModules.filter(m => !moduleOrder.includes(m.id)),
        ]
      : allModules;
    const modules = orderedModules
      // Drop deleted modules and any module whose chapter was deleted.
      .filter(m => !deletedModuleIds.includes(m.id) && !(m.group && deletedGroupIds.includes(m.group)))
      .map(m => {
        const ov = moduleOverrides[m.id];
        let merged = ov ? { ...m, ...ov } : m;
        // Base layouts + user-added layouts, minus deleted ones.
        let layouts = [...merged.layouts, ...(addedLayouts[m.id] || [])]
          .filter(l => !deletedLayoutIds.includes(l.id));
        // Apply per-layout status overrides set by the layout editor.
        layouts = layouts.map(l =>
          layoutStatusOverrides[l.id] ? { ...l, status: layoutStatusOverrides[l.id] } : l);
        // Apply the saved drag order, appending any layouts not yet in it.
        const order = layoutOrders[m.id];
        if (order) {
          const ordered = order.map(id => layouts.find(l => l.id === id)).filter(Boolean);
          const rest = layouts.filter(l => !order.includes(l.id));
          layouts = [...ordered, ...rest];
        }
        // Renumber for display so L-numbers stay contiguous after add/delete.
        layouts = layouts.map((l, i) => ({ ...l, n: i + 1 }));
        return { ...merged, layouts };
      });
    const moduleGroups = [...(base.moduleGroups || []), ...addedGroups]
      .filter(g => !deletedGroupIds.includes(g.id))
      .map(g => ({ ...g, title: groupTitleOverrides[g.id] || g.title }));
    return { ...base, modules, moduleGroups, brand: currentOrg.name };
  }, [currentOrg, moduleOrder, layoutOrders, groupTitleOverrides, moduleOverrides, layoutStatusOverrides, addedModules, addedGroups, addedLayouts, deletedLayoutIds, deletedModuleIds, deletedGroupIds]);

  // Expose the live course id so the media-upload widgets (VideoMediaBlock,
  // CompanionSlot, BackgroundPicker, MediaSlot) can target the same course the
  // export surface uses — without threading a courseId prop through every
  // layout-editor body. Falls back to the seeded test-course UUID.
  window.dynamoCourseId = liveCourse.id || '7bf69e2d-51e7-4856-86bb-bb2b77b47216';

  // Append a new module (and optionally a new group) created from the
  // "+ Add module" dialog. Auto-select so the user lands on it.
  const handleAddModule = React.useCallback((payload) => {
    let groupId = payload.groupId || null;
    if (payload.newGroupTitle) {
      const newGroup = {
        id: 'g_new_' + (Date.now().toString(36)),
        title: payload.newGroupTitle,
      };
      setAddedGroups(gs => [...gs, newGroup]);
      groupId = newGroup.id;
    }
    setAddedModules(ms => {
      const baseCount = SAMPLE_COURSE.modules.length;
      const n = baseCount + ms.length + 1;
      const id = 'M' + n;
      const layouts = (payload.layouts || []).map((l, i) => ({
        ...l, id: `${id}-L${i+1}`, n: i + 1,
      }));
      const totalSec = layouts.length * 90; // ~1.5 min/layout placeholder
      const mm = String(Math.floor(totalSec / 60)).padStart(2, '0');
      const ss = String(totalSec % 60).padStart(2, '0');
      const next = {
        id, n,
        title: payload.title,
        group: groupId || undefined,
        summary: payload.summary || '',
        duration: layouts.length ? `${mm}:${ss}` : '00:00',
        mandatory: false,
        layouts,
      };
      // Defer the select so the new module is in liveCourse first.
      setTimeout(() => setSelectedModuleId(id), 0);
      return [...ms, next];
    });
  }, []);

  // Append a new layout (of the chosen type) to a module. Auto-select it.
  const handleAddLayout = React.useCallback((moduleId, type) => {
    const newId = `${moduleId}-Lx${Date.now().toString(36)}`;
    const meta = (window.LAYOUT_TYPES || []).find(t => t.id === type);
    // `type` may be a display id (e.g. a hidden_items flat/360° dropdown id).
    // Store the CANONICAL type on the data (Fix 4) and seed the matching media
    // field so the variant is encoded by the URL, not the type string.
    const canon = window.canonicalLayoutType ? window.canonicalLayoutType(type) : type;
    const layout = {
      id: newId, type: canon, status: 'pending',
      summary: meta ? `New ${meta.label.toLowerCase()}` : 'New layout',
      ...(type === 'hidden_items_360_image' ? { image360Url: 'placeholder:panorama', items: [] }
        : canon === 'hidden_items' ? { imageUrl: 'placeholder:scene-new', items: [] } : {}),
    };
    setAddedLayouts(al => ({ ...al, [moduleId]: [...(al[moduleId] || []), layout] }));
    setSelectedModuleId(moduleId);
    setTimeout(() => setSelectedLayoutId(newId), 0);

    // Auto-enable the gaming-quiz flow when the FIRST quiz_gaming layout is
    // added (count 0 → 1). Adding one implies the author wants the flow on, so
    // flip the course-level master switch. We never auto-flip it OFF on removal
    // — that stays the author's explicit setting (Quiz_layouts_options §13.4
    // Fix 4: last-removed preserves intent). Count honours a draft type-switch
    // override (layoutDrafts[l.id].type) and the live (post-delete) course, so
    // the trigger fires only when there are genuinely zero existing ones.
    if (canon === 'quiz_gaming') {
      const existingGaming = liveCourse.modules.reduce((acc, m) =>
        acc + m.layouts.filter(l => {
          const t = (layoutDrafts[l.id] && layoutDrafts[l.id].type) || l.type;
          const ct = window.canonicalLayoutType ? window.canonicalLayoutType(t) : t;
          return ct === 'quiz_gaming';
        }).length, 0);
      if (existingGaming === 0) {
        // Same-reference bail-out when already on → no spurious state churn on
        // a 2nd+ add (the count guard already prevents this, but keep it safe).
        setCourseSettings(s => (s.dftiFlow && s.dftiFlow.enabled)
          ? s
          : { ...s, dftiFlow: { ...s.dftiFlow, enabled: true } });
      }
    }
  }, [liveCourse, layoutDrafts]);

  const handleDeleteLayout = React.useCallback((layoutId) => {
    setDeletedLayoutIds(ids => ids.includes(layoutId) ? ids : [...ids, layoutId]);
  }, []);

  const handleDeleteModule = React.useCallback((moduleId) => {
    setDeletedModuleIds(ids => ids.includes(moduleId) ? ids : [...ids, moduleId]);
  }, []);

  const handleDeleteGroup = React.useCallback((groupId) => {
    setDeletedGroupIds(ids => ids.includes(groupId) ? ids : [...ids, groupId]);
  }, []);

  // Theme + density
  React.useEffect(() => {
    document.documentElement.classList.toggle('dark', !!t.dark);
    const d = { compact: 0.85, regular: 1, comfy: 1.15 }[t.density] || 1;
    document.documentElement.style.setProperty('--density', d);
  }, [t.dark, t.density]);

  // Keyboard shortcuts
  React.useEffect(() => {
    const h = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setShowCmdK(o => !o); }
      else if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === 'p' || e.key === 'P')) { e.preventDefault(); setShowPreview(true); }
      else if ((e.metaKey || e.ctrlKey) && (e.key === 'p' || e.key === 'P')) { e.preventDefault(); setShowPreview(o => !o); }
      else if ((e.metaKey || e.ctrlKey) && e.key === 'e') { e.preventDefault(); setSurface('export'); }
      else if (e.key === 'Escape') { setShowCmdK(false); setShowValidation(false); setShowPreview(false); }
    };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, []);

  const goToLayoutEditor = React.useCallback((id) => {
    if (id) setSelectedLayoutId(id);
    setSurface('layout');
  }, []);

  // ── Local draft persistence (IndexedDB, single record per course) ─────────
  // The "draft" is the aggregate of every author-edit slice above. We persist
  // that snapshot (debounced) and restore it on boot. Slice setters are
  // untouched — persistence wraps the state, it doesn't restructure it.
  const DRAFT_COURSE_ID = 'mohg:difficult-interactions';
  const draftAggregate = React.useMemo(() => ({
    moduleOrder, layoutOrders, groupTitleOverrides, moduleOverrides,
    layoutStatusOverrides, layoutDrafts, addedModules, addedGroups, addedLayouts,
    deletedLayoutIds, deletedModuleIds, deletedGroupIds, courseSettings,
    // Resume context so a refresh lands back on the layout being edited.
    selectedModuleId, selectedLayoutId, surface,
  }), [moduleOrder, layoutOrders, groupTitleOverrides, moduleOverrides,
    layoutStatusOverrides, layoutDrafts, addedModules, addedGroups, addedLayouts,
    deletedLayoutIds, deletedModuleIds, deletedGroupIds, courseSettings,
    selectedModuleId, selectedLayoutId, surface]);

  // Apply a restored snapshot (or null = reset to defaults) to every slice.
  const hydrateDraft = React.useCallback((c) => {
    const d = c || {};
    setModuleOrder(d.moduleOrder ?? null);
    setLayoutOrders(d.layoutOrders || {});
    setGroupTitleOverrides(d.groupTitleOverrides || {});
    setModuleOverrides(d.moduleOverrides || {});
    setLayoutStatusOverrides(d.layoutStatusOverrides || {});
    setLayoutDrafts(d.layoutDrafts || {});
    setAddedModules(d.addedModules || []);
    setAddedGroups(d.addedGroups || []);
    setAddedLayouts(d.addedLayouts || {});
    setDeletedLayoutIds(d.deletedLayoutIds || []);
    setDeletedModuleIds(d.deletedModuleIds || []);
    setDeletedGroupIds(d.deletedGroupIds || []);
    setCourseSettings(d.courseSettings || window.SAMPLE_COURSE_SETTINGS);
    if (d.selectedModuleId) setSelectedModuleId(d.selectedModuleId);
    if (d.selectedLayoutId) setSelectedLayoutId(d.selectedLayoutId);
    // Only resume into a stable editing surface (never a transient flow screen).
    const SAFE = ['draft', 'layout', 'library', 'org-roles', 'content-mapping',
      'localisation', 'assessments', 'brand', 'export', 'components'];
    if (d.surface && SAFE.includes(d.surface)) setSurface(d.surface);
  }, []);

  const { bootStatus: draftBootStatus, saveBlocked: draftSaveBlocked, reset: resetDraft } =
    usePersistedDraft(DRAFT_COURSE_ID, draftAggregate, hydrateDraft);
  const [showResetConfirm, setShowResetConfirm] = React.useState(false);

  const currentSurface = (() => {
    switch (surface) {
      case 'dynamo-home': return <SurfaceDynamoHome
                            onPickOrg={(id) => { setOrgId(id); setSurface('home'); }} />;
      case 'home':        return <SurfaceHome orgId={orgId}
                            onOpenCourse={() => setSurface('draft')}
                            onNewCourse={() => setSurface('new-course')}
                            onSwitchOrg={() => {}} />;
      case 'new-course':  return <SurfaceNewCourse
                            onCancel={() => setSurface('home')}
                            onGenerate={() => setSurface('generating')} />;
      case 'generating':  return <SurfaceGenerating
                            courseName={liveCourse.title}
                            files={SAMPLE_SOURCES}
                            onCancel={() => setSurface('new-course')}
                            onDone={() => setSurface('draft')} />;
      case 'sources':     return <SurfaceSources sources={SAMPLE_SOURCES} />;
      case 'sources-empty': return <SurfaceSources sources={SAMPLE_SOURCES} isEmpty />;
      case 'draft':       return <SurfaceDraft course={liveCourse}
                            selectedModuleId={selectedModuleId} selectedLayoutId={selectedLayoutId}
                            onSelectModule={setSelectedModuleId} onSelectLayout={setSelectedLayoutId}
                            onReorderModules={(next) => setModuleOrder(next.map(m => m.id))}
                            onReorderLayouts={(modId, nextLayouts) =>
                              setLayoutOrders(o => ({ ...o, [modId]: nextLayouts.map(l => l.id) }))}
                            onRenameGroup={(gid, title) =>
                              setGroupTitleOverrides(o => ({ ...o, [gid]: title }))}
                            onAddModule={handleAddModule}
                            onEditModule={(mid, patch) =>
                              setModuleOverrides(o => ({ ...o, [mid]: { ...(o[mid] || {}), ...patch } }))}
                            onAddLayout={handleAddLayout}
                            onDeleteLayout={handleDeleteLayout}
                            onDeleteModule={handleDeleteModule}
                            onDeleteGroup={handleDeleteGroup}
                            sources={SAMPLE_SOURCES}
                            onOpenLayoutEditor={goToLayoutEditor} />;
      case 'layout':      return <SurfaceLayoutEditor course={liveCourse}
                            key={selectedLayoutId}
                            selectedLayoutId={selectedLayoutId}
                            onSelectLayout={setSelectedLayoutId}
                            draft={layoutDrafts[selectedLayoutId]}
                            onChangeDraft={(d) => setLayoutDrafts(o =>
                              ({ ...o, [selectedLayoutId]: d }))}
                            allDrafts={layoutDrafts}
                            gamingQuizEnabled={!!courseSettings.dftiFlow.enabled}
                            dftiFlow={courseSettings.dftiFlow}
                            onChangeDftiFlow={(next) => setCourseSettings(s =>
                              ({ ...s, dftiFlow: typeof next === 'function' ? next(s.dftiFlow) : next }))}
                            onBack={() => setSurface('draft')}
                            onChangeLayoutStatus={(lid, st) =>
                              setLayoutStatusOverrides(o => ({ ...o, [lid]: st }))} />;
      case 'library':     return <SurfaceLayoutLibrary
                            viewAs={t.viewAs}
                            onChangeViewAs={(v) => setTweak('viewAs', v)} />;
      case 'roles':
      case 'org-roles':   return <SurfaceOrgRoles roles={SAMPLE_ROLES} course={liveCourse}
                            key={t.rolesStartBlank ? 'blank' : 'seeded'}
                            startBlank={!!t.rolesStartBlank} />;
      case 'content-mapping': return <SurfaceContentMapping roles={SAMPLE_ROLES} course={liveCourse}
                            assessments={SAMPLE_ASSESSMENTS} onNavigate={setSurface} />;
      case 'localisation': return <SurfaceLocalisation course={liveCourse} roles={SAMPLE_ROLES} assessments={SAMPLE_ASSESSMENTS}
                            layoutDrafts={layoutDrafts}
                            onUpdateDrafts={updateDraft}
                            onTranslateModuleTitle={(mid, title) => setModuleOverrides(o => ({ ...o, [mid]: { ...(o[mid] || {}), title } }))}
                            onTranslateGroupTitle={(gid, title) => setGroupTitleOverrides(o => ({ ...o, [gid]: title }))} />;
      case 'languages':   return <SurfaceLocalisation course={liveCourse} roles={SAMPLE_ROLES} assessments={SAMPLE_ASSESSMENTS}
                            layoutDrafts={layoutDrafts}
                            onUpdateDrafts={updateDraft}
                            onTranslateModuleTitle={(mid, title) => setModuleOverrides(o => ({ ...o, [mid]: { ...(o[mid] || {}), title } }))}
                            onTranslateGroupTitle={(gid, title) => setGroupTitleOverrides(o => ({ ...o, [gid]: title }))} />;
      case 'subtitles':   return <SurfaceLocalisation course={liveCourse} roles={SAMPLE_ROLES} assessments={SAMPLE_ASSESSMENTS}
                            layoutDrafts={layoutDrafts}
                            onUpdateDrafts={updateDraft}
                            onTranslateModuleTitle={(mid, title) => setModuleOverrides(o => ({ ...o, [mid]: { ...(o[mid] || {}), title } }))}
                            onTranslateGroupTitle={(gid, title) => setGroupTitleOverrides(o => ({ ...o, [gid]: title }))} />;
      case 'assessments': return <SurfaceAssessments course={liveCourse} onNavigate={setSurface} />;
      case 'brand':       return <SurfaceBrand course={liveCourse}
                            settings={courseSettings} setSettings={setCourseSettings} />;
      case 'export':      return <SurfaceExport course={liveCourse}
                            settings={courseSettings}
                            layoutDrafts={layoutDrafts}
                            onOpenPreview={() => setShowPreview(true)}
                            onJump={(item) => {
                              window.dispatchEvent(new CustomEvent('dynamo:jump-to', { detail: item }));
                              if (item && item.layoutId) { setSelectedLayoutId(item.layoutId); setSurface('layout'); }
                              else if (item && item.surface && item.surface !== 'export') setSurface(item.surface);
                            }} />;
      case 'components':  return <SurfaceComponents />;
      default: return null;
    }
  })();

  const noChromeSurface = surface === 'home' || surface === 'dynamo-home' || surface === 'new-course' || surface === 'generating';

  return (
    <LocDefaultLangContext.Provider value={courseSettings?.metadata?.defaultLanguage || SAMPLE_COURSE.defaultLanguage || 'en'}>
    <SaveStatusBanner bootStatus={draftBootStatus} saveBlocked={draftSaveBlocked} />
    <div style={{
      display: 'grid',
      gridTemplateAreas: noChromeSurface
        ? '"header header" "main main"'
        : '"header header" "rail main" "status status"',
      gridTemplateColumns: (surface === 'home' || surface === 'dynamo-home') ? '0 1fr' : 'auto 1fr',
      gridTemplateRows: noChromeSurface ? '52px 1fr' : '52px 1fr 26px',
      height: '100vh',
      background: 'var(--bg)',
    }}>
      <TopHeader course={liveCourse} currentSurface={surface}
        currentOrg={currentOrg}
        onValidationClick={() => setShowValidation(o => !o)}
        onCmdK={() => setShowCmdK(true)}
        onPreview={() => setShowPreview(o => !o)}
        onLogoClick={() => setSurface('dynamo-home')}
        onSwitchOrg={(id) => { setOrgId(id); setSurface('home'); }} />
      {!noChromeSurface && (
        <LeftRail collapsed={t.rail === 'collapsed'} currentSurface={surface}
          onNavigate={setSurface}
          onToggleCollapse={() => setTweak('rail', t.rail === 'collapsed' ? 'expanded' : 'collapsed')} />
      )}
      <main style={{ gridArea: 'main', minWidth: 0, overflow: 'hidden', position: 'relative' }}>
        {currentSurface}
      </main>
      {!noChromeSurface && (
        <StatusBar course={liveCourse}
          jobs={[{ id: 'j1' }, { id: 'j2' }]}
          onValidationClick={() => setShowValidation(o => !o)} />
      )}

      {showValidation && <ValidationDrawer course={liveCourse}
        onClose={() => setShowValidation(false)}
        onJump={(item) => { setShowValidation(false); setSurface(item.surface); }} />}
      {showCmdK && <CommandPalette onClose={() => setShowCmdK(false)}
        onNavigate={setSurface} onJumpLayout={goToLayoutEditor}
        onResetDraft={() => { setShowCmdK(false); setShowResetConfirm(true); }} />}

      {showResetConfirm && window.ConfirmDialog && (
        <window.ConfirmDialog icon="Trash" tone="danger"
          title="Reset draft?"
          body="Your saved work will be permanently deleted and the course returns to its starting content. This can't be undone."
          confirmLabel="Reset draft"
          onConfirm={() => { resetDraft(); setShowResetConfirm(false); }}
          onCancel={() => setShowResetConfirm(false)} />
      )}

      {showPreview && <PreviewModal course={liveCourse}
        layoutDrafts={layoutDrafts} selectedLayoutId={selectedLayoutId}
        onClose={() => setShowPreview(false)} />}

      <DynamoTweaks t={t} setTweak={setTweak} setSurface={setSurface} />
    </div>
    </LocDefaultLangContext.Provider>
  );
}

// ── Validation drawer ──────────────────────────────────────────────────────
function ValidationDrawer({ course, onClose, onJump }) {
  return (
    <>
      <div onClick={onClose} style={{
        position: 'fixed', inset: 0, background: 'rgba(15,23,42,0.3)', zIndex: 40,
        animation: 'fadeIn 150ms',
      }} />
      <aside className="slide-in" style={{
        position: 'fixed', top: 0, right: 0, bottom: 0,
        width: 420, background: 'var(--surface)',
        borderLeft: '1px solid var(--border)', zIndex: 41,
        display: 'flex', flexDirection: 'column',
        boxShadow: 'var(--shadow-xl)',
      }}>
        <header style={{ padding: '16px 18px', borderBottom: '1px solid var(--border)',
          display: 'flex', alignItems: 'center', gap: 10 }}>
          <I.ListChecks size={16} />
          <h2 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>Validation</h2>
          <span className="pill issues" style={{ fontSize: 11 }}>
            {course.validation.items.length}
          </span>
          <div style={{ flex: 1 }} />
          <button className="btn sm ghost" onClick={onClose}><I.X size={14} /></button>
        </header>
        <div style={{ flex: 1, overflowY: 'auto', padding: '14px 18px' }}>
          <p style={{ margin: '0 0 12px', fontSize: 12.5, color: 'var(--text-muted)' }}>
            All unresolved invariants across the course. Click any item to jump.
          </p>
          <div style={{ display: 'grid', gap: 8 }}>
            {course.validation.items.map(v => (
              <button key={v.id} onClick={() => onJump(v)}
                style={{
                  display: 'grid', gridTemplateColumns: 'auto 1fr auto',
                  gap: 10, alignItems: 'flex-start', textAlign: 'left',
                  padding: '10px 12px',
                  background: v.level === 'error' ? 'var(--error-bg)' : 'var(--surface)',
                  border: '1px solid', borderColor: v.level === 'error' ? 'var(--error)' : 'var(--border)',
                  borderRadius: 'var(--radius-md)', cursor: 'default',
                  fontFamily: 'inherit', color: 'var(--text)',
                }}>
                {v.level === 'error'
                  ? <I.AlertCircle size={14} style={{ color: 'var(--error-text)', marginTop: 1 }} />
                  : <I.AlertTriangle size={14} style={{ color: 'var(--warning)', marginTop: 1 }} />}
                <div>
                  <div style={{ fontSize: 12.5, lineHeight: 1.4 }}>{v.message}</div>
                  <div style={{ fontSize: 11, color: 'var(--text-faint)',
                    fontFamily: 'var(--font-mono)', marginTop: 4 }}>
                    {v.surface}{v.moduleId ? ` · ${v.moduleId}` : ''}{v.layoutId ? ` · ${v.layoutId}` : ''}
                  </div>
                </div>
                <I.ArrowRight size={13} style={{ color: 'var(--text-faint)' }} />
              </button>
            ))}
          </div>
        </div>
      </aside>
    </>
  );
}

// ── Command palette ────────────────────────────────────────────────────────
function CommandPalette({ onClose, onNavigate, onJumpLayout, onResetDraft }) {
  const [q, setQ] = React.useState('');
  const commands = [
    { kind: 'nav', label: 'Go to Sources', icon: 'Upload', action: () => { onNavigate('sources'); onClose(); } },
    { kind: 'nav', label: 'Go to Course architecture', icon: 'Layers', action: () => { onNavigate('draft'); onClose(); } },
    { kind: 'nav', label: 'Go to Layout library', icon: 'Grid', action: () => { onNavigate('library'); onClose(); } },
    { kind: 'nav', label: 'Go to Organisations & roles', icon: 'Users', action: () => { onNavigate('org-roles'); onClose(); } },
    { kind: 'nav', label: 'Go to Content mapping', icon: 'Table', action: () => { onNavigate('content-mapping'); onClose(); } },
    { kind: 'nav', label: 'Go to Localisation', icon: 'Globe', action: () => { onNavigate('localisation'); onClose(); } },
    { kind: 'nav', label: 'Go to Assessments', icon: 'ClipboardCheck', action: () => { onNavigate('assessments'); onClose(); } },
    { kind: 'nav', label: 'Go to Course settings', icon: 'Palette', action: () => { onNavigate('brand'); onClose(); } },
    { kind: 'nav', label: 'Go to Export', icon: 'Package', action: () => { onNavigate('export'); onClose(); } },
    { kind: 'nav', label: 'Open Component library', icon: 'Grid', action: () => { onNavigate('components'); onClose(); } },
    { kind: 'action', label: 'Re-roll selected suggestion', icon: 'RefreshCw', shortcut: 'R' },
    { kind: 'action', label: 'Accept selected suggestion', icon: 'Check', shortcut: 'A' },
    { kind: 'action', label: 'Cycle layout type', icon: 'Tabs', shortcut: 'T' },
    { kind: 'jump', label: 'Open M2-L3 · Sarah & Jordan feedback', icon: 'PenLine',
      action: () => { onJumpLayout('M2-L3'); onClose(); } },
    { kind: 'jump', label: 'Open M1-L4 · 6-step standup scenario', icon: 'PenLine',
      action: () => { onJumpLayout('M1-L4'); onClose(); } },
    { kind: 'action', label: 'Reset local draft (clear saved work)', icon: 'Trash',
      action: () => { onResetDraft ? onResetDraft() : onClose(); } },
  ];
  const filtered = commands.filter(c => c.label.toLowerCase().includes(q.toLowerCase()));
  return (
    <>
      <div onClick={onClose} style={{
        position: 'fixed', inset: 0, background: 'rgba(15,23,42,0.4)',
        backdropFilter: 'blur(2px)', zIndex: 50,
      }} />
      <div className="slide-in" style={{
        position: 'fixed', top: '15vh', left: '50%', transform: 'translateX(-50%)',
        width: 'min(640px, 92vw)', background: 'var(--surface)',
        border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)',
        boxShadow: 'var(--shadow-xl)', zIndex: 51,
        display: 'flex', flexDirection: 'column', maxHeight: 'min(560px, 70vh)',
        overflow: 'hidden',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10,
          padding: '12px 16px', borderBottom: '1px solid var(--border)' }}>
          <I.Search size={16} style={{ color: 'var(--text-muted)' }} />
          <input value={q} autoFocus onChange={e => setQ(e.target.value)}
            placeholder="Jump to surface, layout, module · or run an action…"
            style={{ flex: 1, border: 0, outline: 'none', background: 'transparent',
              color: 'var(--text)', fontSize: 14, fontFamily: 'inherit' }} />
          <kbd>esc</kbd>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: '6px 8px 10px' }}>
          {filtered.map((c, i) => {
            const Icon = I[c.icon] || I.Hash;
            return (
              <button key={i} onClick={c.action}
                style={{
                  display: 'flex', alignItems: 'center', gap: 10, width: '100%',
                  padding: '8px 10px', textAlign: 'left',
                  background: i === 0 ? 'var(--surface-inset)' : 'transparent',
                  border: 0, borderRadius: 'var(--radius)', cursor: 'default',
                  fontFamily: 'inherit', color: 'var(--text)', fontSize: 13.5,
                }}
                onMouseOver={e => e.currentTarget.style.background = 'var(--surface-inset)'}
                onMouseOut={e => e.currentTarget.style.background = i === 0 ? 'var(--surface-inset)' : 'transparent'}>
                <Icon size={14} style={{ color: 'var(--text-muted)' }} />
                <span style={{ flex: 1 }}>{c.label}</span>
                <span style={{
                  fontSize: 10, padding: '2px 5px', borderRadius: 3,
                  background: c.kind === 'nav' ? 'var(--accent-bg)' : c.kind === 'jump' ? 'var(--ai-bg)' : 'var(--surface-inset)',
                  color: c.kind === 'nav' ? 'var(--accent-text)' : c.kind === 'jump' ? 'var(--ai-text)' : 'var(--text-muted)',
                  fontWeight: 600, letterSpacing: '.04em', textTransform: 'uppercase',
                }}>{c.kind}</span>
                {c.shortcut && <kbd>{c.shortcut}</kbd>}
              </button>
            );
          })}
        </div>
        <div style={{ padding: '8px 16px', borderTop: '1px solid var(--border)',
          fontSize: 11, color: 'var(--text-faint)',
          display: 'flex', alignItems: 'center', gap: 12, background: 'var(--surface-2)' }}>
          <span><kbd>↑↓</kbd> navigate</span>
          <span><kbd>↵</kbd> run</span>
          <span><kbd>esc</kbd> close</span>
          <div style={{ flex: 1 }} />
          <span>powered by <kbd>⌘K</kbd></span>
        </div>
      </div>
    </>
  );
}

// ── Preview pane ───────────────────────────────────────────────────────────
// Renders the actual selected layout (or one of the seven framing screens)
// at native 1280×720 and scales it to fit the slide-out width — so what you
// see here matches what the Dynamo Player will render at runtime.
function PreviewPane({ onClose, course, selectedLayoutId, layoutDrafts, surface }) {
  // Default to the framing screen that matches the current surface, if any.
  const surfaceToFraming = {
    'brand': 'cover', 'languages': 'language', 'roles': 'role',
    'assessments': 'preAssessment', 'home': 'home',
  };
  const [mode, setMode] = React.useState(
    surfaceToFraming[surface] ? `frame:${surfaceToFraming[surface]}` : 'layout');

  // Resolve the layout from the selectedLayoutId.
  let layout = null;
  (course?.modules || []).forEach(m => {
    (m.layouts || []).forEach(l => { if (l.id === selectedLayoutId) layout = l; });
  });
  const layoutType = layout?.type || 'sequence';
  const draft = layoutDrafts?.[selectedLayoutId];
  const content = window.LAYOUT_CONTENT_SAMPLES?.[layoutType] || {};

  const isFrame = mode.startsWith('frame:');
  const frameKey = isFrame ? mode.slice(6) : null;
  const W = 1280, H = 720;

  return (
    <aside className="slide-in" style={{
      position: 'absolute', top: 0, right: 0, bottom: 0,
      width: 540, background: 'var(--surface)',
      borderLeft: '1px solid var(--border)',
      boxShadow: 'var(--shadow-xl)',
      display: 'flex', flexDirection: 'column', zIndex: 20,
    }}>
      <header style={{ padding: '12px 14px', borderBottom: '1px solid var(--border)',
        display: 'flex', alignItems: 'center', gap: 8 }}>
        <I.Eye size={14} />
        <h3 style={{ margin: 0, fontSize: 13, fontWeight: 600 }}>Live preview</h3>
        <div style={{ flex: 1 }} />
        <kbd>⌘P</kbd>
        <button className="btn sm ghost" onClick={onClose}><I.X size={13} /></button>
      </header>

      {/* Mode picker */}
      <div style={{ padding: '8px 14px', borderBottom: '1px solid var(--border)',
        display: 'flex', flexDirection: 'column', gap: 6,
        background: 'var(--surface-2)' }}>
        <div style={{ fontSize: 10.5, color: 'var(--text-faint)', fontWeight: 600,
          letterSpacing: '.06em', textTransform: 'uppercase' }}>The selected layout</div>
        <button onClick={() => setMode('layout')}
          style={{
            display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px',
            background: mode === 'layout' ? 'var(--accent-bg)' : 'transparent',
            color: mode === 'layout' ? 'var(--accent-text)' : 'var(--text)',
            border: '1px solid', borderColor: mode === 'layout' ? 'var(--accent-border)' : 'transparent',
            borderRadius: 'var(--radius)', cursor: 'default', fontFamily: 'inherit',
            fontSize: 12.5, textAlign: 'left',
          }}>
          <LayoutTypeChip type={layoutType} />
          <span className="truncate" style={{ flex: 1 }}>{selectedLayoutId || 'no layout selected'}</span>
        </button>
        <div style={{ fontSize: 10.5, color: 'var(--text-faint)', fontWeight: 600,
          letterSpacing: '.06em', textTransform: 'uppercase', marginTop: 6 }}>Course framing</div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
          {[
            ['cover',           'Cover',         'Image'],
            ['language',        'Language',      'Globe'],
            ['brand',           'Business',      'Layers'],
            ['role',            'Role',          'Users'],
            ['home',            'Home',          'Grid'],
            ['preAssessment',   'Pre-assess.',   'ClipboardCheck'],
            ['postAssessment',  'Post-assess.',  'ClipboardCheck'],
          ].map(([k, label, ic]) => {
            const Ic = I[ic];
            const sel = frameKey === k;
            return (
              <button key={k} onClick={() => setMode(`frame:${k}`)}
                style={{
                  display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px',
                  fontSize: 11.5, fontFamily: 'inherit', cursor: 'default',
                  background: sel ? 'var(--accent-bg)' : 'var(--surface-inset)',
                  color: sel ? 'var(--accent-text)' : 'var(--text-muted)',
                  border: '1px solid', borderColor: sel ? 'var(--accent-border)' : 'transparent',
                  borderRadius: 'var(--radius-sm)', textAlign: 'left',
                }}>
                <Ic size={11} /><span style={{ flex: 1, whiteSpace: 'nowrap' }}>{label}</span>
              </button>
            );
          })}
        </div>
      </div>

      <div style={{ flex: 1, padding: 16, overflowY: 'auto', background: 'var(--bg-deep)' }}>
        <PlayerFrame width={W} height={H} maxWidth={508}>
          <PlayerPreview
            layout={isFrame ? null : { ...content, ...(layout || {}), ...(draft || {}), type: layoutType }}
            frame={isFrame ? frameKey : 'default'} />
        </PlayerFrame>
        <div style={{ marginTop: 10, fontSize: 11, color: 'var(--text-faint)',
          textAlign: 'center', fontFamily: 'var(--font-mono)' }}>
          {isFrame
            ? `framing-screen · ${frameKey}`
            : `${selectedLayoutId || ''} · ${layoutType}`}
        </div>
      </div>
    </aside>
  );
}

// PlayerFrame — renders children inside a fixed 1280×720 box, scaled to fit
// the given maxWidth. This is the device-frame equivalent for the Dynamo Player.
function PlayerFrame({ children, width = 1280, height = 720, maxWidth = 480 }) {
  const scale = maxWidth / width;
  const scaledH = height * scale;
  return (
    <div style={{
      width: maxWidth, height: scaledH,
      background: '#000', borderRadius: 'var(--radius-md)',
      overflow: 'hidden', boxShadow: 'var(--shadow-lg)',
      position: 'relative', margin: '0 auto',
    }}>
      <div style={{
        width, height,
        transform: `scale(${scale})`, transformOrigin: 'top left',
        position: 'absolute', top: 0, left: 0,
      }}>
        {children}
      </div>
    </div>
  );
}

// ── Tweaks panel ───────────────────────────────────────────────────────────
function DynamoTweaks({ t, setTweak, setSurface }) {
  return (
    <TweaksPanel>
      <TweakSection label="Appearance" />
      <TweakToggle label="Dark mode" value={t.dark} onChange={(v) => setTweak('dark', v)} />
      <TweakRadio label="Density" value={t.density} options={['compact','regular','comfy']}
        onChange={(v) => setTweak('density', v)} />
      <TweakRadio label="Left rail" value={t.rail} options={['collapsed','expanded']}
        onChange={(v) => setTweak('rail', v)} />

      <TweakSection label="AI presence" />
      <TweakToggle label="Show AI badges" value={t.showAiBadges}
        onChange={(v) => setTweak('showAiBadges', v)} />
      <TweakToggle label="Show validation dots" value={t.showValidationDot}
        onChange={(v) => setTweak('showValidationDot', v)} />

      <TweakSection label="Persona" />
      <TweakRadio label="View as" value={t.viewAs || 'author'} options={['author','admin']}
        onChange={(v) => setTweak('viewAs', v)} />

      <TweakSection label="Roles surface" />
      <TweakToggle label="Start from blank (preview)" value={!!t.rolesStartBlank}
        onChange={(v) => { setTweak('rolesStartBlank', v); setSurface('roles'); }} />

      <TweakSection label="Jump to surface" />
      <TweakButton label="New course flow" onClick={() => setSurface('new-course')} />
      <TweakButton label="Generation in progress" onClick={() => setSurface('generating')} />
      <TweakButton label="Layout editor (M2-L3 sequence)" onClick={() => setSurface('layout')} />
      <TweakButton label="Layout library" onClick={() => setSurface('library')} />
      <TweakButton label="Component library" onClick={() => setSurface('components')} />
      <TweakButton label="Empty sources state" secondary onClick={() => setSurface('sources-empty')} />
    </TweaksPanel>
  );
}

// Splash gate (src/auth.jsx) — the editor only mounts once signed in.
ReactDOM.createRoot(document.getElementById('root')).render(<AuthGate><App /></AuthGate>);
