// Sample course: "Difficult Interactions" — leadership training
// 4 modules, 24 layouts, EN primary + IT secondary

// ─── LocalizedString helpers ────────────────────────────────────────────────
// Production schema types every author-facing text field as a LocalizedString:
// a per-language object `{ [langCode]: string }` (Zod: z.record(z.string(),
// z.string())). The prototype seeds English-only — i.e. `{ en: '…' }`.
//
// These helpers are the ONE place that knows the shape, so every read/write
// site (the controlled text widgets, the inline question editors, and the
// player preview) stays shape-agnostic. They tolerate a flat string too, so a
// not-yet-migrated value never renders blank — read auto-upgrades, write
// returns an object.
//
// Defined here in data.jsx (the earliest app script after icons) so every
// later script resolves them at render time regardless of load order.
const LOC_LANG_KEYS = new Set([
  'en','it','de','fr','es','pt','ptbr','nl','ja','jp','ko','zh','zht','zh-hant',
  'ar','th','tr','hk','ch','id','ru','pl','sv','da','fi','no','cs','el','he',
  'hi','uk','ro','hu','vi','fa','ms','fil','ca','bg','hr','sk','sl','lv','lt',
  'et','is','ga','gd','cy','af','sw','ta','bn','ur','sr','eslat',
]);

// The default language for non-localisation surfaces (architecture, the
// per-layout editor, the player preview). React context so a widget reads the
// active language without every caller threading a prop.
const LocDefaultLangContext = React.createContext('en');

// Read the active-language string out of a LocalizedString value. `value` may
// be a per-language object OR (legacy) a flat string; both resolve to a string.
// Falls back to the default language, then `en`, then any present language.
function locText(value, lang) {
  if (value == null) return '';
  if (typeof value === 'string') return value;
  if (typeof value !== 'object') return '';
  if (lang && typeof value[lang] === 'string') return value[lang];
  if (typeof value.en === 'string') return value.en;
  for (const k in value) if (typeof value[k] === 'string') return value[k];
  return '';
}

// Write `str` into one language slot, preserving every other language. Always
// returns a per-language object (auto-upgrades a legacy flat string to { en }).
function locSet(value, lang, str) {
  const base = (value && typeof value === 'object' && !Array.isArray(value))
    ? value
    : (typeof value === 'string' && value) ? { en: value } : {};
  return { ...base, [lang || 'en']: str };
}

// True when `o` is a LocalizedString object: a plain object whose keys are ALL
// language codes and whose values are ALL strings. Used to flatten arbitrary
// content trees for display (e.g. the player preview) without enumerating every
// field name. Strict by design — `{ state, color, textColor }` and the like are
// NOT mistaken for translations because their keys aren't language codes.
function isLocObject(o) {
  if (!o || typeof o !== 'object' || Array.isArray(o)) return false;
  const keys = Object.keys(o);
  if (!keys.length) return false;
  return keys.every(k => LOC_LANG_KEYS.has(k) && typeof o[k] === 'string');
}

// Deep-resolve every LocalizedString object inside `value` to its active-language
// string, recursing through arrays and plain objects. Returns a NEW structure;
// the original (the seed / draft) is never mutated.
function deepLocResolve(value, lang) {
  if (Array.isArray(value)) return value.map(v => deepLocResolve(v, lang));
  if (value && typeof value === 'object') {
    if (isLocObject(value)) return locText(value, lang);
    const out = {};
    for (const k in value) out[k] = deepLocResolve(value[k], lang);
    return out;
  }
  return value;
}

if (typeof window !== 'undefined') {
  Object.assign(window, {
    locText, locSet, isLocObject, deepLocResolve, LocDefaultLangContext, LOC_LANG_KEYS,
  });
}

const LAYOUT_TYPES = [
  { id: 'fullscreen_video',         label: 'Fullscreen video',         icon: 'Film',    group: 'Video' },
  { id: 'fullscreen_text_and_image', label: 'Fullscreen text + image', icon: 'Image',   group: 'Hero' },
  { id: 'text_and_image',           label: 'Text + image rows',        icon: 'Layers',  group: 'Content' },
  { id: 'small_video',              label: 'Small video',              icon: 'Film',    group: 'Video' },
  { id: 'sequence',                 label: 'Sequence (tabbed)',        icon: 'Tabs',    group: 'Interactive' },
  { id: 'icons_discover',           label: 'Icons discover',           icon: 'Grid',    group: 'Interactive' },
  { id: 'vertical_tabs',            label: 'Vertical tabs',            icon: 'Tabs',    group: 'Interactive' },
  { id: 'horizontal_tabs',          label: 'Horizontal tabs',          icon: 'Tabs',    group: 'Interactive' },
  { id: 'hidden_items_flat_image',  label: 'Hidden items · flat image', icon: 'Eye',    group: 'Interactive' },
  { id: 'hidden_items_360_image',   label: 'Hidden items · 360° image', icon: 'Globe',  group: 'Interactive' },
  // Canonical stored type for both hidden_items display ids (Fix 4). Hidden
  // from the dropdown (the two display ids above are what authors pick), but
  // registered so type chips / previews resolve a label + icon for the value
  // actually stored on the data.
  { id: 'hidden_items',             label: 'Hidden items',             icon: 'Eye',     group: 'Interactive', hidden: true },
  { id: 'mandatoryQuestion',        label: 'Mandatory question',       icon: 'Lock',    group: 'Assessment', hidden: true },
  { id: 'question',                 label: 'Question (non-blocking)',  icon: 'AlertCircle', group: 'Assessment', hidden: true },
  { id: 'quiz_images',              label: 'Quiz · image',             icon: 'ClipboardCheck', group: 'Assessment' },
  { id: 'quiz_gaming',              label: 'Quiz · gaming',            icon: 'Trophy',  group: 'Assessment' },
  { id: 'title',                    label: 'Title divider',            icon: 'Type',    group: 'Hero' },
  { id: 'two_columns_text',         label: 'Two columns text',         icon: 'FileText',group: 'Content' },
  { id: 'object_viewer',            label: '3D object viewer',         icon: 'Cube',    group: 'Interactive' },
];

// ── hidden_items is PURE UI SUGAR (Fix 4, locked 2026-06-04) ────────────────
// The two LAYOUT_TYPES entries above (`hidden_items_flat_image` /
// `hidden_items_360_image`) are DISPLAY ids for the dropdown only. The value
// stored at `layout.type` is ALWAYS the canonical `hidden_items`; the flat vs
// 360° variant is encoded by which media URL is populated (`imageUrl` for flat,
// `image360Url` for 360°) — the same branch the Player runtime uses. Never
// write a hidden_items display id to `layout.type`.
const LAYOUT_DISPLAY_TO_CANONICAL = {
  hidden_items_flat_image: 'hidden_items',
  hidden_items_360_image:  'hidden_items',
};
// display id (or canonical id) → the canonical id stored on the data.
function canonicalLayoutType(displayId) {
  return LAYOUT_DISPLAY_TO_CANONICAL[displayId] || displayId;
}
// a stored layout → the display id the dropdown should show (reverse map).
// hidden_items reverses to flat / 360° based on which media URL is populated.
function displayLayoutType(layout) {
  if (!layout) return undefined;
  if (layout.type === 'hidden_items') {
    return layout.image360Url ? 'hidden_items_360_image' : 'hidden_items_flat_image';
  }
  return layout.type;
}

const SAMPLE_COURSE = {
  // Real seeded test-course UUID from the production Postgres (uuid-typed
  // course.id column) — the gateway 500s on non-UUID ids. Phase 4.
  id: '7bf69e2d-51e7-4856-86bb-bb2b77b47216',
  // Denormalised display copy of the course name (chrome breadcrumb / header).
  // NOT a LocalizedString — the canonical localised course name is
  // SAMPLE_COURSE_SETTINGS.metadata.title ({ en: … }). Left flat by design.
  title: 'Difficult Interactions',
  topic: 'Leadership · Conversational skills',
  defaultLanguage: 'en',
  languages: ['en', 'it', 'de', 'fr', 'es'],
  scormVersion: '2004',
  template: 'corporate-default',
  brand: 'MOHG',
  lastSaved: '2 minutes ago',
  aiSpend: { used: 4.82, cap: 25.00, currency: 'USD' },
  validation: {
    total: 3,
    items: [
      { id: 'v1', level: 'warning', surface: 'draft', moduleId: 'M2', layoutId: 'M2-L5', message: 'Coverage below 80% — 4 source claims unmapped' },
      { id: 'v2', level: 'warning', surface: 'languages', message: '2 layouts in Italian need review after recent EN edits' },
      { id: 'v3', level: 'error', surface: 'assessments', message: 'Post-assessment: M4 has no question group bound' },
    ],
  },
  moduleGroups: [
    { id: 'g1', title: { en: 'Preparing for difficult moments' }, color: '#7c3aed' },
    { id: 'g2', title: { en: 'Handling and recovering' },          color: '#0891b2' },
  ],
  /* Course-level brand grouping. When `selectBrand` is true, every
     role belongs to one brand; the Roles matrix renders a coloured
     super-header row above the role columns, grouping the roles
     under their brand. Mirrors the Player's <selectBrand> tag — the
     brand is the gate the learner picks before their role surfaces.
     Brand colours live with the brand definition so the super-header
     in the matrix matches what the learner sees on the brand-picker
     screen in the Player. */
  selectBrand: true,
  brands: [
    { id: 'bt', label: 'BT',        color: '#5C2D91', textColor: '#ffffff' },
    { id: 'or', label: 'Openreach', color: '#1B2A3D', textColor: '#ffffff' },
  ],
  modules: [
    {
      id: 'M1', n: 1, title: { en: 'Recognising tension early' }, group: 'g1',
      summary: { en: 'Spot the verbal and non-verbal cues that signal a conversation is sliding sideways.' },
      duration: '08:24', mandatory: true,
      layouts: [
        { id: 'M1-L1', n: 1, type: 'fullscreen_text_and_image', status: 'accepted', summary: { en: 'When does a conversation become difficult?' } },
        { id: 'M1-L2', n: 2, type: 'text_and_image',           status: 'accepted', summary: { en: 'Five everyday signals — paired examples and counter-examples' } },
        { id: 'M1-L3', n: 3, type: 'small_video',              status: 'accepted', summary: { en: 'SME interview · Dr. Priya Naidu · 02:14' } },
        { id: 'M1-L4', n: 4, type: 'sequence',                 status: 'accepted', summary: { en: '6-step scenario · the standup that went wrong' } },
        { id: 'M1-L5', n: 5, type: 'icons_discover',           status: 'proposed', summary: { en: '4 patterns that escalate quickly' } },
        { id: 'M1-L6', n: 6, type: 'quiz_images',             status: 'accepted', summary: { en: 'Knowledge check — what is the earliest signal?' } },
      ],
    },
    {
      id: 'M2', n: 2, title: { en: 'Frameworks for tough talks' }, group: 'g1',
      summary: { en: 'Two named frameworks (CLEAR and SBI) and when each one fits.' },
      duration: '12:10', mandatory: true,
      layouts: [
        { id: 'M2-L1', n: 1, type: 'title',                    status: 'accepted', summary: { en: 'Section opener · Frameworks' } },
        { id: 'M2-L2', n: 2, type: 'fullscreen_text_and_image',status: 'accepted', summary: { en: 'Why frameworks beat improvisation' } },
        { id: 'M2-L3', n: 3, type: 'sequence',                 status: 'proposed', summary: { en: 'Sarah & Jordan · a feedback scenario in 6 tabs' }, selected: true },
        { id: 'M2-L4', n: 4, type: 'fullscreen_video',         status: 'pending',  summary: { en: 'Sarah & Jordan · the full conversation, in-video drills' } },
        { id: 'M2-L5', n: 5, type: 'vertical_tabs',            status: 'accepted', summary: { en: 'CLEAR vs SBI — side-by-side' } },
        { id: 'M2-L6', n: 6, type: 'small_video',              status: 'issues',   summary: { en: 'In-video drill: identify the framework being used' } },
        { id: 'M2-L7', n: 7, type: 'horizontal_tabs',          status: 'pending',  summary: { en: 'Coaching · giving · receiving · 3-tab carousel' } },
        { id: 'M2-L8', n: 8, type: 'quiz_images',             status: 'pending',  summary: { en: 'Pick the right framework for the scenario' } },
      ],
    },
    {
      id: 'M3', n: 3, title: { en: 'De-escalation in practice' }, group: 'g2',
      summary: { en: 'When tempers rise — concrete moves to slow the conversation down.' },
      duration: '09:47', mandatory: true,
      layouts: [
        { id: 'M3-L1', n: 1, type: 'fullscreen_text_and_image',status: 'accepted', summary: { en: 'The temperature dial · setting the mental model' } },
        { id: 'M3-L2', n: 2, type: 'icons_discover',           status: 'accepted', summary: { en: '6 verbal moves to lower the temperature' } },
        { id: 'M3-L3', n: 3, type: 'sequence',                 status: 'accepted', summary: { en: '5-step scenario · the customer escalation' } },
        { id: 'M3-L4', n: 4, type: 'hidden_items',             status: 'proposed', summary: { en: 'Interactive · spot the calming signals in the photo' } },
        { id: 'M3-L5', n: 5, type: 'two_columns_text',         status: 'accepted', summary: { en: 'Do · Avoid — paired guidance' } },
        { id: 'M3-L6', n: 6, type: 'quiz_images',             status: 'accepted', summary: { en: 'Practice · select the most effective response' } },
      ],
    },
    {
      id: 'M4', n: 4, title: { en: 'After the conversation' }, group: 'g2',
      summary: { en: 'Repair, follow-through, and what to write down so the moment isn\'t lost.' },
      duration: '06:32', mandatory: false,
      layouts: [
        { id: 'M4-L1', n: 1, type: 'fullscreen_text_and_image',status: 'accepted', summary: { en: 'Why follow-through is the conversation' } },
        { id: 'M4-L2', n: 2, type: 'small_video',              status: 'accepted', summary: { en: 'Reflection prompt · 60-second written exercise' } },
        { id: 'M4-L3', n: 3, type: 'text_and_image',           status: 'proposed', summary: { en: 'Three repair moves with examples' } },
        { id: 'M4-L4', n: 4, type: 'horizontal_tabs',          status: 'pending',  summary: { en: 'Write · share · check-in · 3-tab carousel' } },
        { id: 'M4-L5', n: 5, type: 'quiz_gaming',             status: 'pending',  summary: { en: 'Final check · what should follow-through include?' } },
      ],
    },
  ],
};

// ── Sources (raw uploads being processed) ───────────────────────────────────
const SAMPLE_SOURCES = [
  { id: 's1', name: 'Difficult-Interactions_v3-final.docx', kind: 'doc', size: '847 KB',
    uploaded: '2 hrs ago', status: 'ready', classification: 'Expository', warnings: 0,
    paragraphs: 142, anecdotes: 8 },
  { id: 's2', name: 'Priya-Naidu_SME-interview.mp4', kind: 'video', size: '184 MB',
    uploaded: '2 hrs ago', status: 'ready', classification: 'Narrative', warnings: 1,
    duration: '11:42', transcriptWords: 1842 },
  { id: 's3', name: 'Customer-escalation-case_audio.mp3', kind: 'audio', size: '24 MB',
    uploaded: '38 min ago', status: 'ready', classification: 'Narrative', warnings: 2,
    duration: '08:12', transcriptWords: 1204 },
  { id: 's4', name: 'CLEAR-framework_handbook.docx', kind: 'doc', size: '312 KB',
    uploaded: '38 min ago', status: 'ingesting', classification: 'Expository', warnings: 0,
    paragraphs: 67, progress: 0.62 },
  { id: 's5', name: 'Manager-anecdotes-collected.docx', kind: 'doc', size: '198 KB',
    uploaded: '12 min ago', status: 'queued', classification: 'Narrative', warnings: 0 },
  { id: 's6', name: 'old-handout_2019.docx', kind: 'doc', size: '94 KB',
    uploaded: '5 min ago', status: 'failed', classification: 'Expository', warnings: 3 },
];

// ── Section context for the currently-selected layout (M2-L3 sequence) ──────
// AI section-analysis context (source provenance, paragraph mapping). `summary`
// here is editorial analysis, NOT learner-facing course content — not a
// LocalizedString, intentionally left flat.
const SAMPLE_SECTION_CONTEXT = {
  sectionId: 'M2.S3',
  summary: 'Sarah, a senior engineer, delivers difficult feedback to Jordan after a missed deadline. Demonstrates the SBI framework end-to-end.',
  sourceFile: 'Difficult-Interactions_v3-final.docx',
  paragraphs: [
    { id: 'p1', text: 'Sarah had been dreading the conversation all week. Jordan had missed two deadlines in a row, and the team was beginning to whisper.', kept: true },
    { id: 'p2', text: 'She thought about the SBI framework — Situation, Behavior, Impact — that her own manager had used with her three years earlier. The clarity had stung at the time, but it had also stuck.', kept: true },
    { id: 'p3', text: '"Jordan, when you missed the API spec review on Tuesday and the design sign-off on Friday, the consequence is that two engineers were blocked and we slipped the integration."', kept: true },
    { id: 'p4', text: 'Jordan was quiet for a moment. Then: "I should have told you sooner. I\'ve been struggling with the new auth layer and didn\'t want to flag it."', kept: true },
    { id: 'p5', text: 'This is the moment SBI is designed for: surface the behaviour, name the impact, then leave room for the other person to bring context.', kept: true },
  ],
  coverage: { covered: 24, total: 25, pct: 96 },
  anecdotes: [
    { id: 'a1', text: 'Three years earlier, Sarah\'s own manager used SBI with her.', status: 'verified', cite: 'Source p.2, ¶3' },
    { id: 'a2', text: 'The team was beginning to whisper about Jordan.', status: 'verified', cite: 'Source p.2, ¶1' },
    { id: 'a3', text: 'Jordan struggled silently with the new auth layer.', status: 'verifying', cite: 'Source p.3, ¶4' },
  ],
  otherSuggestions: [
    { type: 'vertical_tabs',    score: 0.81, rationale: 'Could be reframed as Sarah-view vs Jordan-view tabs' },
    { type: 'small_video',      score: 0.74, rationale: 'Could re-shoot as dramatised dialogue, but adds production time' },
    { type: 'horizontal_tabs',  score: 0.62, rationale: 'Less optimal — fragments the narrative thread' },
  ],
};

// (SAMPLE_SUGGESTIONS removed — right-hand suggestions panel deleted from Course architecture)

// ── Sequence tabs for the currently-edited layout ──────────────────────────
// Legacy standalone demo for the original sequence editor. The canonical,
// schema-shaped sequence content (with LocalizedString tabTitle/tabText) lives
// in LAYOUT_CONTENT_SAMPLES.sequence — that one is migrated. These flat
// title/body strings drive only the legacy demo path; left flat by design.
const SAMPLE_SEQUENCE_TABS = [
  { id: 't1', kind: 'text',     mobileText: 'Set-up',         title: 'The missed deadline',     body: 'Sarah notices Jordan has missed two consecutive deliverables. The team is starting to whisper.' },
  { id: 't2', kind: 'video',    mobileText: 'Prep',           title: 'Sarah prepares',          body: 'Watch how Sarah uses SBI — Situation, Behavior, Impact — to scaffold what she wants to say.', duration: '01:24' },
  { id: 't3', kind: 'questionMultiple', mobileText: 'Decide', title: 'Which opener works best?', body: 'Pick the opener that uses SBI most cleanly.', answers: 4 },
  { id: 't4', kind: 'text',     mobileText: 'Feedback ✓',     title: 'Correct — that\'s an SBI opener',   body: 'Naming the specific behaviour without judgement is the SBI core move.', branch: 'correct' },
  { id: 't5', kind: 'text',     mobileText: 'The talk',       title: 'The conversation',        body: 'See how Sarah names the behaviour, then waits. Jordan brings context unprompted.' },
  { id: 't6', kind: 'text',     mobileText: 'Wrap',           title: 'After the talk',          body: 'Both walk away with a concrete next step and a follow-up scheduled.' },
];

const LANG_FLAGS = {
  en: '🇬🇧', it: '🇮🇹', de: '🇩🇪', fr: '🇫🇷', es: '🇪🇸', jp: '🇯🇵',
  ar: '🇸🇦', hk: '🇭🇰', ch: '🇨🇳', id: '🇮🇩', th: '🇹🇭', tr: '🇹🇷', pt: '🇵🇹', nl: '🇳🇱',
};
const LANG_NAMES = {
  en: 'English', it: 'Italian', de: 'German', fr: 'French', es: 'Spanish',
  jp: 'Japanese', ar: 'Arabic', hk: 'Chinese (HK)', ch: 'Chinese (PRC)',
  id: 'Indonesian', th: 'Thai', tr: 'Turkish', pt: 'Portuguese', nl: 'Dutch',
};

const SAMPLE_ORGS = [
  { id: 'mohg',     name: 'MOHG',     full: 'Mandarin Oriental Hotel Group',
    desc: 'Hospitality · Code of Conduct, Fire awareness, GDPR',
    color: ['#0F4F44', '#0A2C28'], glyph: 'M', currentUser: 'Lisa Park · Senior Editor',
    plan: 'Enterprise', stats: { courses: 12, modules: 47, editors: 8 } },
  { id: 'deloitte', name: 'Deloitte', full: 'Deloitte Touche Tohmatsu',
    desc: 'Professional services · Onboarding, Ethics, Auditing',
    color: ['#86BC25', '#3F6F0F'], glyph: 'D', currentUser: '',
    plan: 'Enterprise', stats: { courses: 31, modules: 142, editors: 24 } },
  { id: 'bt',       name: 'BT',       full: 'BT Group',
    desc: 'Telecom · Customer Experience, Compliance',
    color: ['#5514B4', '#2D0466'], glyph: 'BT', currentUser: '',
    plan: 'Enterprise', stats: { courses: 28, modules: 168, editors: 19 } },
];

// Org-level course catalogue (home screen cards). `title` here is the
// catalogue display name across many courses — NOT the in-course module
// hierarchy Fix D migrates. Left flat by design.
const SAMPLE_COURSES_LIST = [
  // MOHG
  { id: 'c1', org: 'mohg', title: 'Difficult Interactions', topic: 'Leadership',
    modules: 4, layouts: 24, langs: ['en','it'], lastSaved: '2 min ago',
    editor: 'Lisa Park', validation: { level: 'warning', count: 3 }, status: 'in-progress',
    coverImage: 'difficult', current: true },
  { id: 'c2', org: 'mohg', title: 'Code of Conduct', topic: 'Compliance',
    modules: 6, layouts: 41, langs: ['en','it','jp','hk','ch'], lastSaved: '2 days ago',
    editor: 'Marco Rossi', validation: { level: 'success', count: 0 }, status: 'shipped',
    coverImage: 'conduct' },
  { id: 'c3', org: 'mohg', title: 'Fire Awareness', topic: 'Safety',
    modules: 3, layouts: 18, langs: ['en','it','jp','hk','ch','ar','fr'], lastSaved: '1 week ago',
    editor: 'Lisa Park', validation: { level: 'success', count: 0 }, status: 'shipped',
    coverImage: 'fire' },
  { id: 'c4', org: 'mohg', title: 'Anti-Bribery', topic: 'Compliance',
    modules: 5, layouts: 32, langs: ['en','it','jp'], lastSaved: '3 weeks ago',
    editor: 'Akira Sato', validation: { level: 'success', count: 0 }, status: 'shipped',
    coverImage: 'compliance' },
  { id: 'c5', org: 'mohg', title: 'Cybersecurity Foundations', topic: 'Security',
    modules: 4, layouts: 26, langs: ['en'], lastSaved: '32 min ago',
    editor: 'Marco Rossi', validation: { level: 'error', count: 2 }, status: 'in-progress',
    coverImage: 'security' },
  { id: 'c6', org: 'mohg', title: 'New Hire Welcome', topic: 'Onboarding',
    modules: 2, layouts: 14, langs: ['en','it','jp','hk','ch','ar','fr','es','de'], lastSaved: '5 days ago',
    editor: 'Lisa Park', validation: { level: 'success', count: 0 }, status: 'shipped',
    coverImage: 'welcome' },
  { id: 'c7', org: 'mohg', title: 'Guest Privacy & GDPR', topic: 'Compliance',
    modules: 4, layouts: 22, langs: ['en','it'], lastSaved: '1 hr ago',
    editor: 'Lisa Park', validation: { level: 'warning', count: 1 }, status: 'in-progress',
    coverImage: 'privacy' },
  { id: 'c8', org: 'mohg', title: 'Service Excellence', topic: 'Hospitality',
    modules: 5, layouts: 30, langs: ['en','jp','hk','ch'], lastSaved: '6 weeks ago',
    editor: 'Akira Sato', validation: { level: 'success', count: 0 }, status: 'archived',
    coverImage: 'service' },
];

const SAMPLE_ROLES = [
  /* `label` is the single role name shown everywhere (column header,
     Player picker). `code` is the internal id used for keying and
     <UI_${code}>.xml file lookup — never shown in the authoring UI.
     `brand` ties the role to a course brand; only consumed when
     `course.selectBrand` is true (§25.8). */
  { code: 'manager',  label: 'Manager',                brand: 'bt',  modules: ['M1','M2','M3','M4'], skipPreAssessment: false, uiOverride: false },
  { code: 'ic',       label: 'Individual contributor', brand: 'bt',  modules: ['M1','M2','M3'],      skipPreAssessment: false, uiOverride: false },
  { code: 'newhire',  label: 'New hire',               brand: 'or',  modules: ['M1','M2'],           skipPreAssessment: true,  uiOverride: true },
  { code: 'exec',     label: 'Executive',              brand: 'or',  modules: ['M2','M3','M4'],      skipPreAssessment: true,  uiOverride: false },
];

// Pre/post-assessment question groups, each bound to a subset of roles.
// Single source of truth so the Assessments surface and the Roles matrix
// stay in sync; the Roles surface flips role↔group bindings in place,
// the Assessments surface edits group + question text.
//
// Per-question role bindings are stored on the question itself so an
// author can include a group for a role but skip one of its questions
// for that role (rare but supported — usually the question inherits
// from the group's `roles` set).
const SAMPLE_ASSESSMENTS = {
  pre: {
    groups: [
      { id: 'pg1', mod: 'M1', label: 'Tension signals',     roles: ['manager','ic','newhire','exec'],
        questions: [
          { id: 'pq1', prompt: 'What is the earliest signal that a conversation is becoming difficult?', roles: ['manager','ic','newhire','exec'] },
          { id: 'pq2', prompt: 'Which scenario shows a "stockpiling" pattern most clearly?',             roles: ['manager','ic','newhire','exec'] },
          { id: 'pq3', prompt: 'Pair the verbal cue with its underlying meaning.',                       roles: ['manager','ic','exec'] },
        ] },
      { id: 'pg2', mod: 'M2', label: 'Framework recall',    roles: ['manager','ic','exec'],
        questions: [
          { id: 'pq4', prompt: 'Which framework fits a 1:1 feedback moment best?',                       roles: ['manager','ic','exec'] },
          { id: 'pq5', prompt: 'Match the framework to the situation.',                                   roles: ['manager','exec'] },
        ] },
      { id: 'pg3', mod: 'M3', label: 'De-escalation moves', roles: ['manager','ic','exec'],
        questions: [
          { id: 'pq6', prompt: 'Identify the verbal move that lowers temperature fastest.',              roles: ['manager','ic','exec'] },
          { id: 'pq7', prompt: 'Pick the response that re-grounds the conversation.',                    roles: ['manager','exec'] },
        ] },
    ],
  },
  post: {
    groups: [
      { id: 'pog1', mod: 'M1', label: 'Recognising tension early', roles: ['manager','ic','newhire','exec'],
        questions: [
          { id: 'poq1', prompt: 'Spot the earliest signal in this conversation snippet.', roles: ['manager','ic','newhire','exec'] },
          { id: 'poq2', prompt: 'Rank the four signals from weakest to strongest.',       roles: ['manager','ic','exec'] },
        ] },
      { id: 'pog2', mod: 'M2', label: 'Frameworks for tough talks', roles: ['manager','ic','exec'],
        questions: [
          { id: 'poq3', prompt: 'Apply CLEAR to a scenario.',                              roles: ['manager','ic','exec'] },
          { id: 'poq4', prompt: 'Compare CLEAR and SBI in a one-paragraph response.',      roles: ['manager','exec'] },
        ] },
      { id: 'pog3', mod: 'M3', label: 'De-escalation in practice', roles: ['manager','exec'],
        questions: [
          { id: 'poq5', prompt: 'Select the most effective response to this escalation.',  roles: ['manager','exec'] },
        ] },
      // M4 intentionally missing — the validation error on the Assessments
      // surface (and the matrix's coverage chip) calls this out.
    ],
  },
};

// ── Assessment question seed, keyed by MODULE ID ────────────────────────────
// Single source of truth for the Assessments surface's starter questions.
// Keyed by `module.id` (NOT a module-code literal) so renaming a module's
// title never breaks the lookup. The surface reads ASSESSMENT_SEED[module.id]
// and falls back to one empty question when a module has no seed.
const ASSESSMENT_SEED = {
  M1: [
    { prompt: 'What is the earliest signal that a conversation is becoming difficult?',
      verified: true,
      answers: [
        { id: 'a1', text: 'Volume rises in the room or voices flatten.', isCorrect: true,
          feedback: 'Right — a shift in vocal energy is the earliest tell.' },
        { id: 'a2', text: 'The other person crosses their arms.', isCorrect: false,
          feedback: 'A later cue — body language usually follows the vocal shift.' },
        { id: 'a3', text: 'A pause that lasts longer than two seconds.', isCorrect: false,
          feedback: 'Pauses happen for many reasons; not a reliable early signal.' },
      ] },
    { prompt: "Which scenario shows a 'stockpiling' pattern most clearly?", verified: true,
      answers: [
        { id: 'a1', text: 'Raising six months of saved-up grievances at once.', isCorrect: true,
          feedback: 'Right — stockpiling is holding issues back until they spill out together.' },
        { id: 'a2', text: 'Giving feedback on the same day.', isCorrect: false,
          feedback: 'That is timely feedback — the opposite of stockpiling.' },
      ] },
  ],
  M2: [
    { prompt: 'Which framework fits a behaviour-and-impact conversation best?', verified: true,
      answers: [
        { id: 'a1', text: 'SBI — Situation, Behavior, Impact.', isCorrect: true,
          feedback: 'Right — SBI keeps the focus on observable behaviour.' },
        { id: 'a2', text: 'Improvise and read the room.', isCorrect: false,
          feedback: 'A named framework gives the conversation structure.' },
      ] },
  ],
  M3: [
    { prompt: 'What is the first move when a conversation overheats?', verified: true,
      answers: [
        { id: 'a1', text: 'Lower your own volume and slow the pace.', isCorrect: true,
          feedback: 'Right — regulating your own delivery lowers the temperature fastest.' },
        { id: 'a2', text: 'Match their energy to show you care.', isCorrect: false,
          feedback: 'Matching heat usually escalates rather than calms.' },
      ] },
  ],
  M4: [
    { prompt: 'What should effective follow-through include?', verified: false,
      answers: [
        { id: 'a1', text: 'A written note of the agreed next step and a check-in date.', isCorrect: true,
          feedback: 'Right — writing it down and scheduling a check-in is what makes it stick.' },
        { id: 'a2', text: 'Nothing — the conversation itself is enough.', isCorrect: false,
          feedback: 'Without follow-through the moment is easily lost.' },
      ] },
  ],
};

// ── Course-level settings slice ─────────────────────────────────────────────
// The single source of truth for the Course settings surface (Surface 7).
// Mirrors §3 of the Course_settings_design spec. Every control on
// surface-brand.jsx reads from + writes to this object via app-level state.
// Default values match what surface-brand.jsx rendered statically before the
// wiring pass. Localised fields are { [lang]: string } maps so a later
// localisation pass can fill secondary languages without a schema change.
const SAMPLE_COURSE_SETTINGS = {
  metadata: {
    // Course title — localised. The Cover "Title overlay" (cover.title) is a
    // separate display string; this is the canonical course name.
    title: { en: 'Difficult Interactions' },
    topic: 'Leadership · Conversational skills',
    // Validated against the course's enabled languages on the Localisation
    // surface. Until that slice is shared, the picker offers SAMPLE_COURSE.languages.
    defaultLanguage: 'en',
    scormVersion: '2004',        // '1.2' | '2004'
    // NOTE: "template" is a UX-level convenience only — it seeds defaults in
    // the authoring tool and is NOT emitted to config.xml / any production
    // schema. No XML emission downstream.
    template: 'corporate-default',
  },

  // One row per organisation. `logo` is the active variant; the other three
  // catalogue variants (§4.6) live under logoVariants behind a per-row
  // "More logo variants" disclosure. Exactly one org carries isPrimary.
  organisations: [
    { id: 'org-acme-talent', label: { en: 'Acme Talent' }, isPrimary: true,
      logo: 'placeholder:acme-talent_logo.svg',
      logoVariants: { inactive: null, big: null, activeBig: null } },
    { id: 'org-acme-hosp', label: { en: 'Acme Hospitality' }, isPrimary: false,
      logo: null,
      logoVariants: { inactive: null, big: null, activeBig: null } },
  ],

  // Pre-assessment is a course-level feature. learnerChoice + skipReturnees
  // only take effect when `pre` is on (enforced as disabled state in the UI).
  assessmentFlow: { pre: true, learnerChoice: true, skipReturnees: false },

  cover: {
    image: 'placeholder:cover_difficult-interactions.jpg',
    title: { en: 'Difficult Interactions' },
    intro: { en: 'A short course on the conversations that matter most. 35 minutes. Earn your completion certificate.' },
    ctaColor: '#3b82f6',
    ctaLabel: { en: 'Start course' },
  },

  // The 12 screen-level feature flags. Each maps to a <flag> in config.xml.
  // The UI groups them into core (always visible) + advanced (disclosure).
  featureFlags: {
    haveNewToCompany: true,
    haveNewsScreen: false,
    havePeopleManager: false,
    fancyAssessment: true,
    quizFrequentSave: true,
    scormSuspendDataCompression: true,
    sequentialModules: false,
    blockingByDefault: false,
    analyticsEvents: true,
    altTextRequired: true,
    transcriptDownload: true,
    accessibilityWidget: true,
  },

  // Gaming-quiz course-level slice. Two jobs:
  //   1. `enabled` gates whether `quiz_gaming` is offered as a layout type
  //      (see surface-layout-editor.jsx) — the master switch.
  //   2. The profile-setup form fields below are SHARED across every
  //      quiz_gaming layout in the course. The gamified quiz collects three
  //      hardcoded learner inputs (first name / last name / email) on a
  //      one-time setup screen; authors can relabel those inputs and edit the
  //      surrounding intro copy/imagery here, but cannot add/remove fields.
  //      Every quiz_gaming editor's "Profile form" tab binds to THIS slice, so
  //      an edit on one propagates to all.
  // (Author-facing copy never says "DFTI" — CLAUDE.md §23.2; the `dfti*` data
  // keys are historical/internal only and never shown to authors.)
  dftiFlow: {
    enabled: true,
    profileSetupDescription: { en: 'Please fill the fields below to set up your account and move on with your learning. These details will be used to enhance your experience. This is NOT a phishing attempt.' },
    profileSetupFirstName: { en: 'First name' },
    profileSetupLastName:  { en: 'Last name' },
    profileSetupEmail:     { en: 'Email address' },
    dftiInfoTitle:         { en: "Don't feed the 'ish" },
    dftiInfoLeftText:      { en: 'Helpful systems work for you.' },
    dftiInfoRightText:     { en: 'Attackers try to trick you into acting.' },
    dftiLeftImg:  'content/dfti/images/robot_icon_big.png',
    dftiRightImg: 'content/dfti/images/hacker_icon_big.png',
  },
};

// Metadata for the 12 feature flags — the human label, the config.xml flag
// name surfaced in the tooltip, a one-line behaviour note, and whether the
// flag is "core" (shown by default) vs "advanced" (behind the disclosure).
const FEATURE_FLAG_META = [
  { key: 'sequentialModules',  core: true,
    label: 'Modules must complete in order',
    desc: 'Locks each module until the previous one is complete.' },
  { key: 'blockingByDefault',  core: true,
    label: 'Block progression by default',
    desc: 'New layouts gate the learner until finished unless individually set otherwise.' },
  { key: 'haveNewToCompany',   core: true,
    label: 'Show "new to company" intro',
    desc: 'Offers a tailored intro path for learners who are new to the organisation.' },
  { key: 'haveNewsScreen',     core: true,
    label: 'Enable News screen',
    desc: 'Adds an organisation News screen to the course home.' },

  // Always-on / hidden from the author UI. `accessibilityWidget` is a
  // compliance must-have — kept permanently true and never surfaced as a
  // toggle (filtered out via `alwaysOn`).
  { key: 'accessibilityWidget', core: true, alwaysOn: true,
    label: 'Enable accessibility widget',
    desc: 'Shows the in-player accessibility controls (text size, contrast, motion).' },
  { key: 'havePeopleManager',  core: false,
    label: 'People Manager directory',
    desc: 'Shows a directory of people-manager contacts inside the course.' },
  { key: 'fancyAssessment',    core: false,
    label: 'Fancy assessment animations',
    desc: 'Enables the richer transition animations on assessment screens.' },
  { key: 'quizFrequentSave',   core: false,
    label: 'Save after each question',
    desc: 'Commits SCORM progress after every answered question, not just at quiz end.' },
  { key: 'scormSuspendDataCompression', core: false,
    label: 'Compress SCORM suspend data',
    desc: 'Compresses suspend_data to stay under LMS size limits.' },
  { key: 'analyticsEvents',    core: false,
    label: 'Emit xAPI events',
    desc: 'Sends xAPI statements for fine-grained analytics alongside SCORM.' },
  { key: 'altTextRequired',    core: false,
    label: 'Require alt text on images',
    desc: 'Blocks export until every image has alt text — accessibility gate.' },
  { key: 'transcriptDownload', core: false,
    label: 'Allow transcript download',
    desc: 'Lets learners download a transcript of every video in the course.' },
];

// ── HTML media samples (catalogue) ──────────────────────────────────────────
// Registry of the embedded HTML-document samples an author can attach to a
// quiz_images / hidden_items mini-quiz question (nested content.html, mirroring
// content.image / content.video).
//
// These are ORIGINAL, brand-free awareness samples authored for the prototype —
// no real organisation, product, or logo is reproduced. They live as real,
// self-contained HTML files under uploads/html-samples/ (each keeps the
// Player's required <div class="quiz"> wrapper and loads a shared stylesheet
// via relative ../ paths). The UI lists them in the MediaSlot "Pick from
// samples" picker; selecting one writes its htmlUrl onto the field.
//
// htmlUrl is pre-prefixed in the data (project-relative) — the field stores it
// verbatim, matching the per-field path-prefix convention.
const HTML_SAMPLES = [
  { id: 'module_4_03', label: 'Phishy email — shipping invoice',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_4/03.html',
    htmlAlt: 'A suspicious email from a "Freight Express" billing address asking you to download an invoice attachment and reply with your full name, address and phone number to release a parcel.',
    scenarioCategory: 'phishing-email', complexity: 'med',
    tokens: ['user-address'], tooltipCount: 5,
    notes: 'Most typical phishy email — light token use, moderate cues. Default sample.' },
  { id: 'module_4_05', label: 'Phishy email — password reset',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_4/05.html',
    htmlAlt: 'A fake "account security" email warning of an unusual sign-in and pushing a time-limited password-reset link.',
    scenarioCategory: 'phishing-email', complexity: 'med',
    tokens: [], tooltipCount: 5,
    notes: 'Credential-reset lure built on alarm + a 30-minute deadline.' },
  { id: 'module_4_09', label: 'Phishy email — payroll update',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_4/09.html',
    htmlAlt: 'An email posing as HR/payroll asking you to re-enter your bank account and sort code on a new portal before a same-day cut-off.',
    scenarioCategory: 'phishing-email', complexity: 'high',
    tokens: [], tooltipCount: 4,
    notes: 'Business-email-compromise style — financial detail harvest framed as a system migration.' },
  { id: 'module_4_01', label: 'Phishy email — gift card reward',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_4/01.html',
    htmlAlt: 'A prize email claiming you won a £100 gift card and asking for a small delivery fee paid by card.',
    scenarioCategory: 'phishing-email', complexity: 'low',
    tokens: [], tooltipCount: 5,
    notes: 'Too-good-to-be-true reward with a small upfront fee and a 2-hour countdown.' },
  { id: 'module_3_05A_01', label: 'Social post — oversharing',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_3/05A/01.html',
    htmlAlt: 'A public social media post announcing a two-week holiday, the home address, and where a spare key is hidden.',
    scenarioCategory: 'social-media', complexity: 'low',
    tokens: [], tooltipCount: 3,
    notes: 'Physical-security oversharing rather than a link lure.' },
  { id: 'module_3_05A_04', label: 'Chat — urgent gift-card request',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_3/05A/04.html',
    htmlAlt: 'A chat thread where someone impersonating a director secretly asks the recipient to buy gift cards and send the codes within 20 minutes.',
    scenarioCategory: 'messaging', complexity: 'med',
    tokens: [], tooltipCount: 4,
    notes: 'Authority + urgency + secrecy pretext over an unverified channel.' },
  { id: 'module_2_02A_01', label: 'Fake login screen',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_2/02A/01.html',
    htmlAlt: 'A spoofed sign-in page on an unfamiliar domain asking you to re-enter and confirm your email and password.',
    scenarioCategory: 'credential-harvest', complexity: 'high',
    tokens: [], tooltipCount: 2,
    notes: 'The tell is the address bar — a credential-harvesting page rather than an email.' },
  { id: 'module_2_02A_06', label: 'Smishing text — missed delivery',
    htmlUrl: 'uploads/html-samples/content/en/modules/module_2/02A/06.html',
    htmlAlt: 'A text message claiming a parcel could not be delivered and demanding a small redelivery fee via a shortened link before the parcel is returned today.',
    scenarioCategory: 'smishing', complexity: 'low',
    tokens: [], tooltipCount: 4,
    notes: 'SMS / smishing variant — fee + unfamiliar link + same-day threat.' },
];

Object.assign(window, {
  LAYOUT_TYPES, canonicalLayoutType, displayLayoutType, SAMPLE_COURSE, SAMPLE_COURSE_SETTINGS, FEATURE_FLAG_META,
  SAMPLE_SOURCES, SAMPLE_SECTION_CONTEXT, SAMPLE_SEQUENCE_TABS, SAMPLE_ROLES,
  SAMPLE_ASSESSMENTS, ASSESSMENT_SEED,
  SAMPLE_ORGS, SAMPLE_COURSES_LIST,
  LANG_FLAGS, LANG_NAMES,
  HTML_SAMPLES,
});

// reorder so SAMPLE_ROLES exists before being referenced above
;
