import { PROJECT_MODE_CATALOG, runnableSnippetModes, snippetStarterForMode } from '@learnitive/project-runtime';
import { adminRunLearnCode, adminUploadMedia, adminUploadMediaWithProgress } from '../api';
import { populateLearnMediaPicker } from './media-picker';
import { normalizeDirectVideoUrl, normalizeWistiaMediaId, normalizeYouTubeVideoId } from '../video-utils';
import { createWistiaPlayerElement, ensureWistiaPlayerAssets } from '../wistia-player';
import type { InteractiveContentBlock, InteractiveContentBlockType } from '../../interactive-content/types';
import { interactiveContentProfileForContext, type InteractiveContentCapabilityProfile } from '../../interactive-content/profiles';
import { clearMediaReference, learnMediaAsInteractive, mediaReferenceUrl, setMediaReference, type InteractiveContentEditorAdapter, type InteractiveContentEditorMedia } from '../../interactive-content/editor-adapter';

type FieldKind='text'|'textarea'|'number'|'select'|'checkbox'|'lines'|'json'|'csvNumbers'|'rich';
type FieldSpec={key:string;label:string;kind:FieldKind;options?:Array<{value:string;label:string}>;placeholder?:string};
type ToolSpec={title:string;icon:string;fields:FieldSpec[];defaults?:Record<string,unknown>};
type ToolApi={blocks:{insert:(type:string,data?:Record<string,unknown>,config?:Record<string,unknown>,index?:number,needToFocus?:boolean,replace?:boolean)=>unknown;getCurrentBlockIndex?:()=>number}};
type ToolArgs={data?:Record<string,unknown>;readOnly?:boolean;config?:InteractiveContentEditorAdapter;api?:ToolApi};
type LearnToolClass=new(args:ToolArgs)=>{render():HTMLElement;save():Record<string,unknown>};
type EditorToolEntry=LearnToolClass|{class:LearnToolClass;config:InteractiveContentEditorAdapter};

const icon=(glyph:string)=>`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="${glyph}"/></svg>`;
const ICONS={
  heading:icon('M4 6v12 M12 6v12 M4 12h8 M17 8h3v10 M17 18h6'),
  list:icon('M8 6h13 M8 12h13 M8 18h13 M3 6h.01 M3 12h.01 M3 18h.01'),
  quote:icon('M6 17h4l2-5V7H5v5h4 M15 17h4l2-5V7h-7v5h4'),
  table:icon('M3 5h18v14H3z M3 10h18 M8 5v14 M15 5v14'),
  callout:icon('M12 3a7 7 0 0 0-4 12.7V19h8v-3.3A7 7 0 0 0 12 3 M9 22h6'),
  image:icon('M4 5h16v14H4z M8 11l2-2 7 7 M15 10h.01'),
  video:icon('M4 6h12v12H4z M16 10l4-2v8l-4-2'),
  code:icon('M8 9l-3 3 3 3 M16 9l3 3-3 3 M14 5l-4 14'),
  terminal:icon('M4 5h16v14H4z M7 9l3 3-3 3 M12 15h5'),
  check:icon('M4 12l5 5L20 6'),
  divider:icon('M4 12h16'),
};

const EXECUTABLE_MODES=PROJECT_MODE_CATALOG.filter(mode=>Boolean(mode.runtime?.provider&&mode.runtime?.operation)&&mode.id!=='latex');
const RUNNABLE_SNIPPET_MODES=runnableSnippetModes();
const COMMON_CODE_MODE_IDS=['javascript','typescript','python','shell','c','cpp','java','go','rust','php','ruby','r'] as const;
const recentCodeModeIds:string[]=[];
function rememberRecentCodeMode(id:string){const normalized=id.trim();if(!normalized)return;const current=recentCodeModeIds.indexOf(normalized);if(current>=0)recentCodeModeIds.splice(current,1);recentCodeModeIds.unshift(normalized);if(recentCodeModeIds.length>4)recentCodeModeIds.length=4;}
function orderedCodeModes(){const rank=[...recentCodeModeIds,...COMMON_CODE_MODE_IDS];return [...RUNNABLE_SNIPPET_MODES].sort((a,b)=>{const ai=rank.indexOf(a.id);const bi=rank.indexOf(b.id);if(ai>=0||bi>=0)return(ai<0?999:ai)-(bi<0?999:bi);return a.label.localeCompare(b.label,'en');});}
function cloneData(value:Record<string,unknown>|undefined,defaults:Record<string,unknown>={}){return{...defaults,...(value||{})};}
function inputClass(kind:FieldKind){return kind==='textarea'||kind==='json'||kind==='lines'?'interactive-learn-editor-tool-textarea':'interactive-learn-editor-tool-input';}
function toJson(value:unknown){try{return JSON.stringify(value??{},null,2)}catch{return'{}'}}
function labelled(captionText:string,control:HTMLElement){const label=document.createElement('label');label.className='interactive-learn-editor-tool-field';const caption=document.createElement('span');caption.textContent=captionText;label.append(caption,control);return label;}
function textInput(value:string,placeholder=''){const control=document.createElement('input');control.className='interactive-learn-editor-tool-input';control.value=value;control.placeholder=placeholder;return control;}
function textArea(value:string,placeholder=''){const control=document.createElement('textarea');control.className='interactive-learn-editor-tool-textarea';control.value=value;control.placeholder=placeholder;return control;}
const learnEditorAdapter:InteractiveContentEditorAdapter={
  context:'learn',runtimeDefaultLabel:'Use lesson/course default',requiredLabel:'Required for lesson completion',
  uploadMedia:async(kind,file,onProgress,signal)=>learnMediaAsInteractive(onProgress?await adminUploadMediaWithProgress(kind,file,onProgress,signal):await adminUploadMedia(kind,file)),
  populateMediaPicker:async(host,kind,onPick)=>{await populateLearnMediaPicker(host,kind,media=>onPick(learnMediaAsInteractive(media)))},
  runCodePreview:input=>adminRunLearnCode(input),
};
function activeAdapter(config?:InteractiveContentEditorAdapter){return config||learnEditorAdapter;}
function runtimeSelect(value:string,adapter:InteractiveContentEditorAdapter=learnEditorAdapter){const select=document.createElement('select');select.className='interactive-learn-editor-tool-input';const auto=document.createElement('option');auto.value='';auto.textContent=adapter.runtimeDefaultLabel||'Use content default';select.append(auto);for(const mode of EXECUTABLE_MODES){const option=document.createElement('option');option.value=mode.id;option.textContent=mode.label;select.append(option)}select.value=value;return select;}
function mediaDisplayName(media:InteractiveContentEditorMedia){return media.originalName||media.url.split('/').pop()||'media';}
function mediaReady(data:Record<string,unknown>,idKey='mediaId',urlKey='mediaUrl'){return Boolean(mediaReferenceUrl(data,idKey,urlKey));}

function createTool(spec:ToolSpec):LearnToolClass{
  return class LearnEditorTool{
    static get toolbox(){return{title:spec.title,icon:spec.icon};}
    static get isReadOnlySupported(){return true;}
    data:Record<string,unknown>;
    readOnly:boolean;
    constructor({data,readOnly}:ToolArgs){this.data=cloneData(data,spec.defaults);this.readOnly=Boolean(readOnly);}
    render(){
      const root=document.createElement('div');root.className='interactive-learn-editor-tool';
      if(!spec.fields.length){const p=document.createElement('p');p.className='interactive-muted';p.textContent=spec.title;root.append(p);return root;}
      for(const field of spec.fields){
        const label=document.createElement('label');label.className='interactive-learn-editor-tool-field';
        const caption=document.createElement('span');caption.textContent=field.label;label.append(caption);
        const current=this.data[field.key];
        if(field.kind==='checkbox'){
          const row=document.createElement('span');row.className='interactive-learn-editor-tool-check';
          const control=document.createElement('input');control.type='checkbox';control.checked=Boolean(current);control.disabled=this.readOnly;control.addEventListener('change',()=>{this.data[field.key]=control.checked});row.append(control,document.createTextNode(' Enabled'));label.append(row);
        }else if(field.kind==='select'){
          const control=document.createElement('select');control.className=inputClass(field.kind);control.disabled=this.readOnly;
          for(const option of field.options||[]){const el=document.createElement('option');el.value=option.value;el.textContent=option.label;control.append(el)}
          control.value=String(current??field.options?.[0]?.value??'');control.addEventListener('change',()=>{this.data[field.key]=control.value});label.append(control);
        }else if(field.kind==='rich'){
          const control=document.createElement('div');control.className='interactive-learn-editor-tool-rich';control.contentEditable=String(!this.readOnly);control.innerHTML=String(current??'');control.dataset.placeholder=field.placeholder||'Write…';control.addEventListener('input',()=>{this.data[field.key]=control.innerHTML});label.append(control);
        }else{
          const control=(field.kind==='textarea'||field.kind==='json'||field.kind==='lines')?document.createElement('textarea'):document.createElement('input');
          control.className=inputClass(field.kind);control.disabled=this.readOnly;control.placeholder=field.placeholder||'';
          if(control instanceof HTMLInputElement&&field.kind==='number')control.type='number';
          if(field.kind==='json')control.value=toJson(current);else if(field.kind==='lines')control.value=Array.isArray(current)?current.map(String).join('\n'):String(current??'');else if(field.kind==='csvNumbers')control.value=Array.isArray(current)?current.join(','):String(current??'');else control.value=String(current??'');
          control.addEventListener('input',()=>{if(field.kind==='number')this.data[field.key]=Number(control.value)||0;else if(field.kind==='lines')this.data[field.key]=control.value.split('\n').map(v=>v.trim()).filter(Boolean);else if(field.kind==='csvNumbers')this.data[field.key]=control.value.split(',').map(v=>Number(v.trim())).filter(Number.isInteger);else if(field.kind==='json'){try{this.data[field.key]=JSON.parse(control.value)}catch{/* Keep last valid JSON. */}}else this.data[field.key]=control.value;});
          label.append(control);
        }
        root.append(label);
      }
      return root;
    }
    save(){return this.data;}
  };
}

class LegacyImportTool {
  static get toolbox(){return{title:'Legacy import warning',icon:ICONS.callout};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;
  constructor({data}:ToolArgs){this.data=cloneData(data,{sourceType:'unknown',reason:'legacy_import_requires_admin_review',originalData:{}});}
  render(){
    const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-legacy-import';
    const title=document.createElement('strong');title.textContent='Legacy content requires administrator review';
    const message=document.createElement('p');message.textContent=`Source block: ${String(this.data.sourceType||'unknown')} · ${String(this.data.reason||'legacy import warning')}`;
    const help=document.createElement('p');help.className='interactive-muted';help.textContent='The importer preserved this unsupported source block instead of discarding it. Recreate the content with a supported Learn block, then remove this warning block.';
    const details=document.createElement('details');const summary=document.createElement('summary');summary.textContent='View preserved source data';const pre=document.createElement('pre');pre.className='interactive-learn-editor-code-output';pre.textContent=toJson(this.data.originalData);details.append(summary,pre);root.append(title,message,help,details);return root;
  }
  save(){return this.data;}
}

class ImageTool {
  static get toolbox(){return{title:'Image',icon:ICONS.image};}
  static get isReadOnlySupported(){return true;}
  static get pasteConfig(){return{files:{mimeTypes:['image/*']}};}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;preview?:HTMLImageElement;status?:HTMLElement;
  constructor({data,readOnly,config}:ToolArgs){this.data=cloneData(data,{mediaId:0,widthPercent:100,alignment:'center',alt:'',caption:''});this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  async upload(file:File){if(!file.type.startsWith('image/'))return;this.status&&(this.status.textContent='Uploading image…');try{const media=await this.adapter.uploadMedia('image',file);setMediaReference(this.data,media);if(!this.data.alt)this.data.alt=file.name;if(this.preview){this.preview.src=media.url;this.preview.hidden=false;this.applyPreviewLayout();}this.status&&(this.status.textContent=`Uploaded · ${mediaDisplayName(media)}${media.width&&media.height?` · ${media.width}×${media.height}`:''}`);}catch(reason){this.status&&(this.status.textContent=reason instanceof Error?reason.message:String(reason));}}
  applyPreviewLayout(){if(!this.preview)return;const width=Math.min(100,Math.max(25,Number(this.data.widthPercent||100)));const alignment=['left','center','right'].includes(String(this.data.alignment))?String(this.data.alignment):'center';this.preview.style.width=`${width}%`;this.preview.style.marginLeft=alignment==='left'?'0':alignment==='center'?'auto':'auto';this.preview.style.marginRight=alignment==='right'?'0':alignment==='center'?'auto':'auto';}
  selectMedia(media:InteractiveContentEditorMedia){setMediaReference(this.data,media);if(!this.data.alt)this.data.alt=media.altText||mediaDisplayName(media);if(this.preview){this.preview.src=media.url;this.preview.hidden=false;this.applyPreviewLayout();}this.status&&(this.status.textContent=`Selected · ${mediaDisplayName(media)}`);}
  onPaste(event:{detail?:{file?:File}}){const file=event.detail?.file;if(file)void this.upload(file);}
  render(){
    const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-image-tool';
    const preview=document.createElement('img');preview.className='interactive-learn-editor-image-preview';preview.alt='';const url=mediaReferenceUrl(this.data,'mediaId','mediaUrl',this.adapter);if(url)preview.src=url;else preview.hidden=true;this.preview=preview;root.append(preview);this.applyPreviewLayout();
    const caption=textInput(String(this.data.caption||''),'Add a caption…');caption.classList.add('interactive-learn-editor-image-caption');caption.disabled=this.readOnly;caption.addEventListener('input',()=>{this.data.caption=caption.value});root.append(caption);
    if(this.readOnly)return root;
    const details=document.createElement('details');details.className='interactive-learn-editor-block-settings';const summary=document.createElement('summary');summary.textContent=url?'Image settings':'Add image';details.append(summary);
    const status=document.createElement('p');status.className='interactive-muted';status.textContent=url?'Image ready':'Paste an image, drag a screenshot into the editor, or choose a file.';this.status=status;details.append(status);
    const controls=document.createElement('div');controls.className='interactive-learn-editor-image-controls';const widths:[[number,string],[number,string],[number,string],[number,string]]=[[25,'Small'],[50,'Medium'],[75,'Large'],[100,'Full']];for(const [width,labelText] of widths){const button=document.createElement('button');button.type='button';button.textContent=labelText;button.title=`${width}% width`;if(Number(this.data.widthPercent||100)===width)button.classList.add('is-active');button.addEventListener('click',()=>{this.data.widthPercent=width;for(const candidate of controls.querySelectorAll('button'))candidate.classList.remove('is-active');button.classList.add('is-active');this.applyPreviewLayout()});controls.append(button)}const align=document.createElement('select');align.setAttribute('aria-label','Image alignment');for(const value of ['left','center','right']){const option=document.createElement('option');option.value=value;option.textContent=value[0].toUpperCase()+value.slice(1);align.append(option)}align.value=String(this.data.alignment||'center');align.addEventListener('change',()=>{this.data.alignment=align.value;this.applyPreviewLayout()});controls.append(align);details.append(controls);
    const actions=document.createElement('div');actions.className='interactive-learn-editor-file-actions';const label=document.createElement('label');label.className='interactive-button interactive-button-secondary interactive-learn-editor-upload';label.textContent=url?'Replace image':'Choose image';const input=document.createElement('input');input.type='file';input.accept='image/png,image/jpeg,image/webp';input.hidden=true;input.addEventListener('change',()=>{const file=input.files?.[0];if(file)void this.upload(file);input.value=''});label.append(input);actions.append(label);
    let library:HTMLElement|undefined;if(this.adapter.populateMediaPicker){const libraryButton=document.createElement('button');libraryButton.type='button';libraryButton.className='interactive-button interactive-button-secondary';libraryButton.textContent='Media library';library=document.createElement('div');library.hidden=true;libraryButton.addEventListener('click',()=>{if(!library)return;library.hidden=!library.hidden;if(!library.hidden)void this.adapter.populateMediaPicker?.(library,'image',media=>{this.selectMedia(media);library!.hidden=true})});actions.append(libraryButton)}details.append(actions);if(library)details.append(library);
    const alt=textInput(String(this.data.alt||''),'Describe the image for screen readers');alt.addEventListener('input',()=>{this.data.alt=alt.value});details.append(labelled('Alt text',alt));root.append(details);return root;
  }
  save(){return this.data;}
}

class ChecklistTool {
  static get toolbox(){return{title:'Checklist',icon:ICONS.check};}
  static get isReadOnlySupported(){return true;}
  data:{items:Array<{text:string;checked:boolean}>};readOnly:boolean;host?:HTMLElement;
  constructor({data,readOnly}:ToolArgs){const source=cloneData(data,{items:[]});const raw=Array.isArray(source.items)?source.items:[];this.data={items:raw.slice(0,200).map(item=>{if(typeof item==='string')return{text:item,checked:false};const value=(item&&typeof item==='object'&&!Array.isArray(item)?item:{}) as Record<string,unknown>;return{text:String(value.text??value.content??''),checked:Boolean(value.checked)}})};this.readOnly=Boolean(readOnly);}
  rerender(){if(!this.host)return;this.host.replaceChildren();this.data.items.forEach((item,index)=>{const row=document.createElement('div');row.className='interactive-learn-editor-checklist-row';const checked=document.createElement('input');checked.type='checkbox';checked.checked=item.checked;checked.disabled=this.readOnly;checked.addEventListener('change',()=>{item.checked=checked.checked});const input=textInput(item.text,'Checklist item');input.disabled=this.readOnly;input.addEventListener('input',()=>{item.text=input.value});row.append(checked,input);if(!this.readOnly){for(const [caption,action,disabled] of [['↑',()=>{if(index>0){[this.data.items[index-1],this.data.items[index]]=[this.data.items[index],this.data.items[index-1]];this.rerender()}},index===0],['↓',()=>{if(index<this.data.items.length-1){[this.data.items[index+1],this.data.items[index]]=[this.data.items[index],this.data.items[index+1]];this.rerender()}},index===this.data.items.length-1],['×',()=>{this.data.items.splice(index,1);this.rerender()},false]] as Array<[string,()=>void,boolean]>){const button=document.createElement('button');button.type='button';button.textContent=caption;button.disabled=disabled;button.addEventListener('click',action);row.append(button)}}this.host.append(row)});if(!this.readOnly){const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary';add.textContent='+ Add checklist item';add.addEventListener('click',()=>{this.data.items.push({text:'',checked:false});this.rerender()});this.host.append(add)}}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-checklist-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}

class GalleryTool {
  static get toolbox(){return{title:'Gallery',icon:ICONS.image};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;host?:HTMLElement;picker?:HTMLElement;
  constructor({data,readOnly,config}:ToolArgs){this.data=cloneData(data,{mediaIds:[],mediaUrls:[],alts:[],caption:''});this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  refs():Array<{id?:number;url:string}>{const urls=(Array.isArray(this.data.mediaUrls)?this.data.mediaUrls:[]).map(String).filter(Boolean);if(urls.length)return urls.slice(0,24).map(url=>({url}));return (Array.isArray(this.data.mediaIds)?this.data.mediaIds:[]).map(Number).filter(Number.isInteger).filter(Boolean).slice(0,24).map(id=>({id,url:`/api/learn/media/${id}`}));}
  setRefs(refs:Array<{id?:number;url:string}>){if(refs.every(ref=>Number.isInteger(ref.id)&&Number(ref.id)>0)){this.data.mediaIds=refs.map(ref=>Number(ref.id));delete this.data.mediaUrls;}else{this.data.mediaUrls=refs.map(ref=>ref.url);delete this.data.mediaIds;}}
  alts(){return (Array.isArray(this.data.alts)?this.data.alts:[]).map(String).slice(0,24);}
  setAlts(alts:string[]){this.data.alts=alts.slice(0,24);}
  addMedia(media:InteractiveContentEditorMedia){const refs=this.refs();if(refs.length>=24||refs.some(ref=>ref.url===(media.persistedUrl||media.url)))return;const alts=this.alts();while(alts.length<refs.length)alts.push('');refs.push({id:media.id,url:media.persistedUrl||media.url});alts.push(media.altText||mediaDisplayName(media));this.setRefs(refs);this.setAlts(alts);}
  rerender(){if(!this.host)return;this.host.replaceChildren();const refs=this.refs();const alts=this.alts();while(alts.length<refs.length)alts.push('');if(alts.length>refs.length)alts.length=refs.length;this.setAlts(alts);const grid=document.createElement('div');grid.className='interactive-learn-editor-gallery-grid';refs.forEach((ref,index)=>{const card=document.createElement('div');card.className='interactive-learn-editor-gallery-item';if(!this.readOnly){card.draggable=true;card.title='Drag to reorder';card.addEventListener('dragstart',(event)=>{event.dataTransfer?.setData('text/plain',String(index));if(event.dataTransfer)event.dataTransfer.effectAllowed='move'});card.addEventListener('dragover',(event)=>{event.preventDefault();if(event.dataTransfer)event.dataTransfer.dropEffect='move';card.classList.add('is-drag-over')});card.addEventListener('dragleave',()=>card.classList.remove('is-drag-over'));card.addEventListener('drop',(event)=>{event.preventDefault();card.classList.remove('is-drag-over');const from=Number(event.dataTransfer?.getData('text/plain'));if(!Number.isInteger(from)||from<0||from>=refs.length||from===index)return;const [moved]=refs.splice(from,1);const [movedAlt]=alts.splice(from,1);refs.splice(index,0,moved);alts.splice(index,0,movedAlt||'');this.setRefs(refs);this.setAlts(alts);this.rerender()})}const img=document.createElement('img');img.src=this.adapter.resolveMediaUrl?.(ref.url)||ref.url;img.alt=alts[index]||`Gallery image ${index+1}`;card.append(img);if(!this.readOnly){const alt=textInput(alts[index]||'',`Alt text for image ${index+1}`);alt.classList.add('interactive-learn-editor-gallery-alt');alt.addEventListener('input',()=>{alts[index]=alt.value;this.setAlts(alts);img.alt=alt.value||`Gallery image ${index+1}`});card.append(alt);const actions=document.createElement('div');for(const [caption,action,disabled] of [['←',()=>{if(index>0){[refs[index-1],refs[index]]=[refs[index],refs[index-1]];[alts[index-1],alts[index]]=[alts[index],alts[index-1]];this.setRefs(refs);this.setAlts(alts);this.rerender()}},index===0],['→',()=>{if(index<refs.length-1){[refs[index+1],refs[index]]=[refs[index],refs[index+1]];[alts[index+1],alts[index]]=[alts[index],alts[index+1]];this.setRefs(refs);this.setAlts(alts);this.rerender()}},index===refs.length-1],['×',()=>{refs.splice(index,1);alts.splice(index,1);this.setRefs(refs);this.setAlts(alts);this.rerender()},false]] as Array<[string,()=>void,boolean]>){const b=document.createElement('button');b.type='button';b.textContent=caption;b.disabled=disabled;b.addEventListener('click',action);actions.append(b)}card.append(actions)}grid.append(card)});this.host.append(grid);const caption=textInput(String(this.data.caption||''),'Optional gallery caption');caption.disabled=this.readOnly;caption.addEventListener('input',()=>{this.data.caption=caption.value});this.host.append(labelled('Caption',caption));if(!this.readOnly){const controls=document.createElement('div');controls.className='interactive-learn-editor-file-actions';const upload=document.createElement('label');upload.className='interactive-button interactive-button-secondary interactive-learn-editor-upload';upload.textContent='Upload image';const input=document.createElement('input');input.type='file';input.accept='image/*';input.hidden=true;input.multiple=true;input.addEventListener('change',async()=>{for(const file of Array.from(input.files||[]).slice(0,24-this.refs().length))this.addMedia(await this.adapter.uploadMedia('image',file));input.value='';this.rerender()});upload.append(input);controls.append(upload);if(this.adapter.populateMediaPicker){const library=document.createElement('button');library.type='button';library.className='interactive-button interactive-button-secondary';library.textContent='Media library';library.addEventListener('click',()=>{if(!this.picker)return;this.picker.hidden=!this.picker.hidden;if(!this.picker.hidden)void this.adapter.populateMediaPicker?.(this.picker,'image',media=>{this.addMedia(media);this.rerender()})});controls.append(library)}this.host.append(controls);if(this.adapter.populateMediaPicker){const picker=document.createElement('div');picker.hidden=true;this.picker=picker;this.host.append(picker)}}}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-gallery-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}


class VideoTool {
  static get toolbox(){return{title:'Video',icon:ICONS.video};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;host?:HTMLElement;picker?:HTMLElement;uploadAbort?:AbortController;
  constructor({data,readOnly,config}:ToolArgs){this.data=cloneData(data,{source:'upload',mediaId:0,externalId:'',directUrl:'',title:'',requiredWatchPercent:0,captionMediaId:0,posterMediaId:0,credit:'',creditUrl:''});this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  destroyPreview(){}
  chooseMedia(media:InteractiveContentEditorMedia){setMediaReference(this.data,media);if(!this.data.title)this.data.title=mediaDisplayName(media);this.rerender();}
  addUploadPreview(url:string){if(!url)return;const frame=document.createElement('div');frame.className='interactive-learn-editor-video-frame';const preview=document.createElement('video');preview.controls=true;preview.playsInline=true;preview.preload='metadata';preview.src=url;preview.className='interactive-learn-editor-video-preview';frame.append(preview);this.host?.append(frame);}
  addExternalPreview(source:string,id:string){if(!id||!this.host)return;const frame=document.createElement('div');frame.className='interactive-learn-editor-video-frame';if(source==='youtube'){const iframe=document.createElement('iframe');iframe.allow='autoplay; fullscreen; picture-in-picture';iframe.allowFullscreen=true;iframe.loading='lazy';iframe.title='YouTube preview';iframe.referrerPolicy='strict-origin-when-cross-origin';iframe.src=`https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}?rel=0&origin=${encodeURIComponent(window.location.origin)}`;frame.append(iframe)}else{const player=createWistiaPlayerElement(id,{title:'Wistia preview'});frame.append(player);void ensureWistiaPlayerAssets(id).catch(()=>{if(!frame.isConnected)return;frame.replaceChildren();const error=document.createElement('p');error.className='interactive-error-copy';error.textContent='The Wistia preview could not be loaded.';frame.append(error)})}this.host.append(frame);}
  rerender(){if(!this.host)return;this.destroyPreview();this.host.replaceChildren();const source=String(this.data.source||'upload');if(!this.readOnly){const quick=textInput('','Paste YouTube, Wistia or HTTPS video URL…');quick.classList.add('interactive-learn-editor-video-url-first');const apply=()=>{const raw=quick.value.trim();if(!raw)return;const youtube=normalizeYouTubeVideoId(raw);if(youtube){this.data.source='youtube';this.data.externalId=youtube;this.rerender();return}const wistia=normalizeWistiaMediaId(raw);if(wistia&&/wistia/iu.test(raw)){this.data.source='wistia';this.data.externalId=wistia;this.rerender();return}const direct=normalizeDirectVideoUrl(raw);if(direct){this.data.source='direct';this.data.directUrl=direct;this.rerender();return}quick.setCustomValidity('Paste a supported YouTube, Wistia or HTTPS video URL.');quick.reportValidity();};quick.addEventListener('change',apply);quick.addEventListener('paste',()=>requestAnimationFrame(apply));this.host.append(quick)}const sourceSelect=document.createElement('select');sourceSelect.className='interactive-learn-editor-tool-input';for(const [value,label] of [['upload','Uploaded video'],['youtube','YouTube'],['wistia','Wistia'],['direct','Direct HTTPS URL']]){const option=document.createElement('option');option.value=value;option.textContent=label;sourceSelect.append(option)}sourceSelect.value=source;sourceSelect.disabled=this.readOnly;sourceSelect.addEventListener('change',()=>{this.data.source=sourceSelect.value;this.rerender()});this.host.append(labelled('Source',sourceSelect));
    if(source==='upload'){const mediaUrl=mediaReferenceUrl(this.data,'mediaId','mediaUrl',this.adapter);if(mediaUrl)this.addUploadPreview(mediaUrl);if(!this.readOnly){const controls=document.createElement('div');controls.className='interactive-learn-editor-file-actions';const upload=document.createElement('label');upload.className='interactive-button interactive-button-secondary interactive-learn-editor-upload';upload.textContent=mediaUrl?'Replace video':'Upload video';const file=document.createElement('input');file.type='file';file.accept='video/mp4,video/webm';file.hidden=true;const progress=document.createElement('div');progress.className='interactive-learn-editor-upload-progress';progress.hidden=true;progress.setAttribute('role','progressbar');progress.setAttribute('aria-valuemin','0');progress.setAttribute('aria-valuemax','100');const progressCopy=document.createElement('div');const progressName=document.createElement('strong');const progressText=document.createElement('span');progressCopy.append(progressName,progressText);const track=document.createElement('div');const fill=document.createElement('i');track.append(fill);const cancel=document.createElement('button');cancel.type='button';cancel.className='interactive-button interactive-button-secondary';cancel.textContent='Cancel upload';cancel.addEventListener('click',()=>this.uploadAbort?.abort());progress.append(progressCopy,track,cancel);file.addEventListener('change',async()=>{const selected=file.files?.[0];file.value='';if(!selected)return;this.uploadAbort?.abort();const controller=new AbortController();this.uploadAbort=controller;progress.hidden=false;progressName.textContent=selected.name;progressText.textContent='Starting upload…';progress.setAttribute('aria-valuenow','0');fill.style.width='0%';upload.classList.add('is-disabled');try{const media=await this.adapter.uploadMedia('video',selected,value=>{progress.setAttribute('aria-valuenow',String(value.percent));fill.style.width=`${value.percent}%`;const mb=(bytes:number)=>`${(bytes/1024/1024).toFixed(1)} MB`;progressText.textContent=`Uploading… ${value.percent}% · ${mb(value.loaded)} / ${mb(value.total)}`;},controller.signal);progressText.textContent='Upload complete. Preparing preview…';this.chooseMedia(media)}catch(reason){if(controller.signal.aborted){progressText.textContent='Upload cancelled. The existing video was kept.';}else{progressText.textContent=reason instanceof Error?reason.message:String(reason);progress.dataset.status='error';}upload.classList.remove('is-disabled')}finally{if(this.uploadAbort===controller)this.uploadAbort=undefined}});upload.append(file);controls.append(upload);if(this.adapter.populateMediaPicker){const library=document.createElement('button');library.type='button';library.className='interactive-button interactive-button-secondary';library.textContent='Media library';library.addEventListener('click',()=>{if(!this.picker)return;this.picker.hidden=!this.picker.hidden;if(!this.picker.hidden)void this.adapter.populateMediaPicker?.(this.picker,'video',media=>this.chooseMedia(media))});controls.append(library)}this.host.append(controls,progress);if(this.adapter.populateMediaPicker){const picker=document.createElement('div');picker.hidden=true;this.picker=picker;this.host.append(picker)}}}
    else if(source==='youtube'||source==='wistia'){const input=textInput(String(this.data.externalId||''),source==='youtube'?'YouTube URL or video ID':'Wistia URL or media ID');input.disabled=this.readOnly;const status=document.createElement('small');status.className='interactive-muted';const preview=document.createElement('button');preview.type='button';preview.className='interactive-button interactive-button-secondary';preview.textContent='Preview video';const sync=()=>{const normalized=source==='youtube'?normalizeYouTubeVideoId(input.value):normalizeWistiaMediaId(input.value);this.data.externalId=normalized;status.textContent=normalized?`Video ID: ${normalized}`:'Enter a valid video URL or ID.';status.dataset.status=normalized?'ok':'error';preview.disabled=!normalized};input.addEventListener('input',sync);preview.addEventListener('click',()=>{const id=String(this.data.externalId||'');if(id){this.host?.querySelector('.interactive-learn-editor-video-frame')?.remove();this.addExternalPreview(source,id)}});sync();this.host.append(labelled(source==='youtube'?'YouTube video':'Wistia video',input),status,preview)}
    else{const input=textInput(String(this.data.directUrl||''),'https://cdn.example.com/video.mp4');input.disabled=this.readOnly;const status=document.createElement('small');status.className='interactive-muted';const sync=()=>{const normalized=normalizeDirectVideoUrl(input.value);this.data.directUrl=normalized;status.textContent=normalized?'Secure direct URL ready':'Only HTTPS video URLs are accepted.';status.dataset.status=normalized?'ok':'error'};input.addEventListener('input',sync);sync();this.host.append(labelled('Direct video URL',input),status)}
    const title=textInput(String(this.data.title||''),'Video title');title.disabled=this.readOnly;title.addEventListener('input',()=>{this.data.title=title.value});this.host.append(labelled('Title',title));const threshold=document.createElement('input');threshold.type='number';threshold.className='interactive-learn-editor-tool-input';threshold.min='0';threshold.max='100';threshold.value=String(this.data.requiredWatchPercent||0);threshold.disabled=this.readOnly;threshold.addEventListener('input',()=>{this.data.requiredWatchPercent=Math.min(100,Math.max(0,Number(threshold.value)||0))});this.host.append(labelled(this.adapter.context==='learn'?'Completion % (0 = manual)':'Optional completion threshold',threshold));
    const credit=textInput(String(this.data.credit||''),'e.g. Red Hat');credit.disabled=this.readOnly;credit.addEventListener('input',()=>{this.data.credit=credit.value});const creditUrl=textInput(String(this.data.creditUrl||''),'https://…');creditUrl.disabled=this.readOnly;creditUrl.addEventListener('input',()=>{this.data.creditUrl=normalizeDirectVideoUrl(creditUrl.value)});this.host.append(labelled('Video credit',credit),labelled('Credit URL',creditUrl));
    if(!this.readOnly&&(source==='upload'||source==='direct')){const caption=document.createElement('label');caption.className='interactive-button interactive-button-secondary interactive-learn-editor-upload';caption.textContent=mediaReady(this.data,'captionMediaId','captionMediaUrl')?'Replace captions':'Add WebVTT captions';const file=document.createElement('input');file.type='file';file.accept='.vtt,text/vtt';file.hidden=true;file.addEventListener('change',async()=>{const selected=file.files?.[0];if(selected){const media=await this.adapter.uploadMedia('caption',selected);setMediaReference(this.data,media,'captionMediaId','captionMediaUrl');this.rerender()}file.value=''});caption.append(file);this.host.append(caption)}
  }
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-video-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}

class AudioTool {
  static get toolbox(){return{title:'Audio',icon:ICONS.video};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;host?:HTMLElement;picker?:HTMLElement;
  constructor({data,readOnly,config}:ToolArgs){this.data=cloneData(data,{mediaId:0,title:'',requiredListenPercent:0});this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  select(media:InteractiveContentEditorMedia){setMediaReference(this.data,media);if(!this.data.title)this.data.title=mediaDisplayName(media);this.rerender();}
  rerender(){if(!this.host)return;this.host.replaceChildren();const url=mediaReferenceUrl(this.data,'mediaId','mediaUrl',this.adapter);if(url){const audio=document.createElement('audio');audio.controls=true;audio.preload='metadata';audio.src=url;this.host.append(audio)}const title=textInput(String(this.data.title||''),'Audio title');title.disabled=this.readOnly;title.addEventListener('input',()=>{this.data.title=title.value});this.host.append(labelled('Title',title));const threshold=document.createElement('input');threshold.type='number';threshold.className='interactive-learn-editor-tool-input';threshold.min='0';threshold.max='100';threshold.value=String(this.data.requiredListenPercent||0);threshold.disabled=this.readOnly;threshold.addEventListener('input',()=>{this.data.requiredListenPercent=Math.min(100,Math.max(0,Number(threshold.value)||0))});this.host.append(labelled(this.adapter.context==='learn'?'Completion % (0 = manual)':'Optional completion threshold',threshold));if(!this.readOnly){const controls=document.createElement('div');controls.className='interactive-learn-editor-file-actions';const upload=document.createElement('label');upload.className='interactive-button interactive-button-secondary interactive-learn-editor-upload';upload.textContent=url?'Replace audio':'Upload audio';const input=document.createElement('input');input.type='file';input.accept='audio/*';input.hidden=true;input.addEventListener('change',async()=>{const file=input.files?.[0];if(file)this.select(await this.adapter.uploadMedia('audio',file));input.value=''});upload.append(input);controls.append(upload);if(this.adapter.populateMediaPicker){const library=document.createElement('button');library.type='button';library.className='interactive-button interactive-button-secondary';library.textContent='Media library';library.addEventListener('click',()=>{if(!this.picker)return;this.picker.hidden=!this.picker.hidden;if(!this.picker.hidden)void this.adapter.populateMediaPicker?.(this.picker,'audio',media=>this.select(media))});controls.append(library)}const remove=document.createElement('button');remove.type='button';remove.className='interactive-button interactive-button-secondary';remove.textContent='Remove';remove.disabled=!url;remove.addEventListener('click',()=>{clearMediaReference(this.data);this.rerender()});controls.append(remove);this.host.append(controls);if(this.adapter.populateMediaPicker){const picker=document.createElement('div');picker.hidden=true;this.picker=picker;this.host.append(picker)}}}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-audio-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}

class FileTool {
  static get toolbox(){return{title:'File',icon:ICONS.image};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;card?:HTMLElement;status?:HTMLElement;
  constructor({data,readOnly,config}:ToolArgs){this.data=cloneData(data,{mediaId:0,label:'',description:'',filename:'',mimeType:'',sizeBytes:0});this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  applyMedia(media:InteractiveContentEditorMedia){setMediaReference(this.data,media);this.data.filename=mediaDisplayName(media);this.data.mimeType=media.mimeType||'';this.data.sizeBytes=media.sizeBytes||0;if(!this.data.label)this.data.label=mediaDisplayName(media);this.renderCard();this.status&&(this.status.textContent=`Selected · ${mediaDisplayName(media)}`);}
  async upload(file:File){this.status&&(this.status.textContent='Uploading file…');try{this.applyMedia(await this.adapter.uploadMedia('file',file));}catch(reason){this.status&&(this.status.textContent=reason instanceof Error?reason.message:String(reason));}}
  renderCard(){if(!this.card)return;this.card.replaceChildren();if(!mediaReady(this.data)){this.card.textContent='No file selected';return;}const icon=document.createElement('span');icon.className='interactive-learn-editor-file-icon';icon.textContent='↧';const copy=document.createElement('span');const strong=document.createElement('strong');strong.textContent=String(this.data.filename||this.data.label||'Attachment');const small=document.createElement('small');const bytes=Number(this.data.sizeBytes||0);small.textContent=`${String(this.data.mimeType||'file')}${bytes?` · ${(bytes/1024).toFixed(bytes>1024*1024?0:1)} ${bytes>1024*1024?'MB':'KB'}`:''}`;copy.append(strong,small);this.card.append(icon,copy);}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-file-tool';const card=document.createElement('div');card.className='interactive-learn-editor-file-card';this.card=card;root.append(card);this.renderCard();const status=document.createElement('p');status.className='interactive-muted';status.textContent=mediaReady(this.data)?'Attachment ready':`Upload a file${this.adapter.populateMediaPicker?' or choose one from the media library':''}.`;this.status=status;root.append(status);if(!this.readOnly){const actions=document.createElement('div');actions.className='interactive-learn-editor-file-actions';const upload=document.createElement('label');upload.className='interactive-button interactive-button-secondary';upload.textContent=mediaReady(this.data)?'Replace file':'Upload file';const input=document.createElement('input');input.type='file';input.accept='.pdf,.zip,.txt,.md,.json,application/pdf,application/zip,text/plain,text/markdown,application/json';input.hidden=true;input.addEventListener('change',()=>{const file=input.files?.[0];if(file)void this.upload(file);input.value=''});upload.append(input);actions.append(upload);let library:HTMLElement|undefined;if(this.adapter.populateMediaPicker){const libraryButton=document.createElement('button');libraryButton.type='button';libraryButton.className='interactive-button interactive-button-secondary';libraryButton.textContent='Media library';actions.append(libraryButton);library=document.createElement('div');library.hidden=true;libraryButton.addEventListener('click',()=>{if(!library)return;library.hidden=!library.hidden;if(!library.hidden)void this.adapter.populateMediaPicker?.(library,'file',media=>{this.applyMedia(media);library!.hidden=true})})}const remove=document.createElement('button');remove.type='button';remove.className='interactive-button interactive-button-secondary';remove.textContent='Remove';remove.addEventListener('click',()=>{clearMediaReference(this.data);this.data.filename='';this.data.mimeType='';this.data.sizeBytes=0;this.renderCard();});actions.append(remove);root.append(actions);if(library)root.append(library)}const label=textInput(String(this.data.label||''),'Attachment label');label.disabled=this.readOnly;label.addEventListener('input',()=>{this.data.label=label.value});root.append(labelled('Label',label));const description=textInput(String(this.data.description||''),'Optional description');description.disabled=this.readOnly;description.addEventListener('input',()=>{this.data.description=description.value});root.append(labelled('Description',description));return root;}
  save(){return this.data;}
}

class DirectHeadingTool {
  static get toolbox(){return{title:'Heading',icon:ICONS.heading};}
  static get isReadOnlySupported(){return true;}
  static get conversionConfig(){return{export:(data:Record<string,unknown>)=>String(data.text||''),import:(text:string)=>({text,level:2})};}
  data:{text:string;level:2|3|4};readOnly:boolean;host?:HTMLElement;heading?:HTMLHeadingElement;
  constructor({data,readOnly}:ToolArgs){const source=cloneData(data,{text:'',level:2});const level=Number(source.level);this.data={text:String(source.text||''),level:level===3||level===4?level:2};this.readOnly=Boolean(readOnly);}
  renderHeading(){if(!this.host)return;const previous=this.heading;const heading=document.createElement(`h${this.data.level}`) as HTMLHeadingElement;heading.className='interactive-content-direct-heading';heading.contentEditable=String(!this.readOnly);heading.dataset.placeholder='Heading';heading.innerHTML=previous?.innerHTML??this.data.text;heading.addEventListener('input',()=>{this.data.text=heading.innerHTML});this.heading=heading;this.host.replaceChildren(heading);}
  render(){const root=document.createElement('div');root.className='interactive-content-direct-heading-wrap';this.host=root;this.renderHeading();return root;}
  renderSettings(){const menu=document.createElement('div');menu.className='interactive-content-heading-settings';for(const level of [2,3,4] as const){const button=document.createElement('button');button.type='button';button.textContent=`H${level}`;button.title=`Heading ${level}`;button.className=this.data.level===level?'is-active':'';button.addEventListener('click',()=>{this.data.level=level;this.renderHeading();for(const item of menu.querySelectorAll('button'))item.classList.toggle('is-active',item===button);this.heading?.focus()});menu.append(button)}return menu;}
  save(){if(this.heading)this.data.text=this.heading.innerHTML;return this.data;}
}

type NestedListItem={content:string;items:NestedListItem[];key?:string};
function listItemKey(){return `li-${globalThis.crypto?.randomUUID?.()||`${Date.now()}-${Math.random().toString(36).slice(2,8)}`}`;}
function normalizeNestedListItems(value:unknown):NestedListItem[]{if(!Array.isArray(value))return[];return value.map(raw=>typeof raw==='string'?{content:raw,items:[],key:listItemKey()}:{content:String((raw as Record<string,unknown>)?.content||(raw as Record<string,unknown>)?.text||''),items:normalizeNestedListItems((raw as Record<string,unknown>)?.items),key:listItemKey()});}
function listPlainText(value:string):string{if(typeof document==='undefined')return value.replace(/<[^>]*>/gu,' ').replace(/\s+/gu,' ').trim();const host=document.createElement('div');host.innerHTML=value;return host.textContent||'';}
function escapeListText(value:string):string{return value.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');}
class StructuredListTool {
  static get toolbox(){return{title:'List',icon:ICONS.list};}
  static get isReadOnlySupported(){return true;}
  static get conversionConfig(){return{export:(data:Record<string,unknown>)=>normalizeNestedListItems(data.items).map(item=>listPlainText(item.content)).join('\n'),import:(text:string)=>({style:'unordered',items:String(text).split(/\n/gu).filter(Boolean).map(content=>({content:escapeListText(content),items:[]}))})};}
  data:{style:'unordered'|'ordered'|'checklist';items:NestedListItem[]};readOnly:boolean;host?:HTMLElement;
  constructor({data,readOnly}:ToolArgs){const source=cloneData(data,{style:'unordered',items:[]});const style=String(source.style||'unordered');this.data={style:style==='ordered'||style==='checklist'?style:'unordered',items:normalizeNestedListItems(source.items)};if(!this.data.items.length)this.data.items=[{content:'',items:[],key:listItemKey()}];this.readOnly=Boolean(readOnly);}
  findParent(target:NestedListItem,items=this.data.items,parent?:NestedListItem):{items:NestedListItem[];index:number;parent?:NestedListItem}|undefined{const index=items.indexOf(target);if(index>=0)return{items,index,parent};for(const item of items){const found=this.findParent(target,item.items,item);if(found)return found}return undefined;}
  addAfter(item?:NestedListItem){const next:NestedListItem={content:'',items:[],key:listItemKey()};if(!item)this.data.items.push(next);else{const position=this.findParent(item);if(position)position.items.splice(position.index+1,0,next);else this.data.items.push(next)}this.rerender();this.focus(next.key);}
  remove(item:NestedListItem){const position=this.findParent(item);if(!position)return;position.items.splice(position.index,1);if(!this.data.items.length)this.data.items.push({content:'',items:[],key:listItemKey()});this.rerender();}
  indent(item:NestedListItem){const position=this.findParent(item);if(!position||position.index===0)return;const previous=position.items[position.index-1];position.items.splice(position.index,1);previous.items.push(item);this.rerender();this.focus(item.key);}
  outdent(item:NestedListItem){const position=this.findParent(item);if(!position?.parent)return;const parentPosition=this.findParent(position.parent);if(!parentPosition)return;position.items.splice(position.index,1);parentPosition.items.splice(parentPosition.index+1,0,item);this.rerender();this.focus(item.key);}
  move(item:NestedListItem,delta:-1|1){const position=this.findParent(item);if(!position)return;const target=position.index+delta;if(target<0||target>=position.items.length)return;position.items.splice(position.index,1);position.items.splice(target,0,item);this.rerender();this.focus(item.key);}
  focus(key?:string){if(!key||!this.host)return;requestAnimationFrame(()=>this.host?.querySelector<HTMLInputElement>(`[data-list-key="${CSS.escape(key)}"] input`)?.focus());}
  setStyle(style:'unordered'|'ordered'){this.data.style=style;this.rerender();}
  renderItems(items:NestedListItem[],depth=0):HTMLElement{const group=document.createElement('div');group.className='interactive-content-list-items';group.dataset.depth=String(depth);for(const item of items){item.key ||= listItemKey();const itemWrap=document.createElement('div');itemWrap.className='interactive-content-list-item';itemWrap.dataset.listKey=item.key;const row=document.createElement('div');row.className='interactive-content-list-row';const input=textInput(listPlainText(item.content),'List item');input.disabled=this.readOnly;input.addEventListener('input',()=>{item.content=escapeListText(input.value)});input.addEventListener('keydown',(event)=>{if(this.readOnly)return;if(event.key==='Enter'){event.preventDefault();this.addAfter(item)}});row.append(input);if(!this.readOnly){const controls:Array<[string,string,()=>void,boolean]>=[['↑','Move item up',()=>this.move(item,-1),false],['↓','Move item down',()=>this.move(item,1),false],['→','Indent item',()=>this.indent(item),false],['←','Outdent item',()=>this.outdent(item),!this.findParent(item)?.parent],['×','Remove item',()=>this.remove(item),false]];for(const [label,title,action,disabled] of controls){const button=document.createElement('button');button.type='button';button.className='interactive-content-list-control';button.textContent=label;button.title=title;button.setAttribute('aria-label',title);button.disabled=disabled;button.addEventListener('click',action);row.append(button)}}itemWrap.append(row);if(item.items.length)itemWrap.append(this.renderItems(item.items,depth+1));group.append(itemWrap)}return group;}
  rerender(){if(!this.host)return;this.host.replaceChildren();const styleRow=document.createElement('label');styleRow.className='interactive-content-list-style';const label=document.createElement('span');label.textContent='Style';const select=document.createElement('select');select.disabled=this.readOnly;for(const [value,text] of [['unordered','Bullets'],['ordered','Numbers']] as const){const option=document.createElement('option');option.value=value;option.textContent=text;select.append(option)}if(this.data.style==='checklist'){const legacy=document.createElement('option');legacy.value='checklist';legacy.textContent='Checklist (legacy — use Checklist block)';legacy.disabled=true;select.append(legacy)}select.value=this.data.style;select.addEventListener('change',()=>this.setStyle(select.value==='ordered'?'ordered':'unordered'));styleRow.append(label,select);this.host.append(styleRow,this.renderItems(this.data.items));if(!this.readOnly){const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary interactive-content-list-add';add.textContent='+ Add item';add.addEventListener('click',()=>this.addAfter());this.host.append(add)}}
  render(){const root=document.createElement('div');root.className='interactive-content-structured-list';this.host=root;this.rerender();return root;}
  renderSettings(){const menu=document.createElement('div');menu.className='interactive-content-list-settings';for(const [value,label] of [['unordered','Bullets'],['ordered','Numbers']] as const){const button=document.createElement('button');button.type='button';button.textContent=label;button.className=this.data.style===value?'is-active':'';button.addEventListener('click',()=>{this.setStyle(value);for(const item of menu.querySelectorAll('button'))item.classList.toggle('is-active',item===button)});menu.append(button)}return menu;}
  save(){type SavedNestedListItem={content:string;items:SavedNestedListItem[]};const clean=(items:NestedListItem[]):SavedNestedListItem[]=>items.map(item=>({content:item.content,items:clean(item.items)}));return{style:this.data.style,items:clean(this.data.items)};}
}

class VisualTableTool {
  static get toolbox(){return{title:'Table',icon:ICONS.table};}
  static get isReadOnlySupported(){return true;}
  data:{content:string[][];withHeadings:boolean};readOnly:boolean;host?:HTMLElement;
  constructor({data,readOnly}:ToolArgs){const source=cloneData(data,{content:[['Heading','Column 2'],['Value','']],withHeadings:true});const content=Array.isArray(source.content)?source.content.map(row=>Array.isArray(row)?row.map(cell=>String(cell??'')):[]):[];this.data={content:content.length?content:[['','']],withHeadings:source.withHeadings!==false};this.readOnly=Boolean(readOnly);}
  columns(){return Math.max(1,...this.data.content.map(row=>row.length));}
  normalize(){const count=this.columns();for(const row of this.data.content)while(row.length<count)row.push('');}
  rerender(){if(!this.host)return;this.normalize();this.host.replaceChildren();const controls=document.createElement('div');controls.className='interactive-learn-editor-table-controls';const header=document.createElement('label');header.className='interactive-learn-editor-tool-check';const check=document.createElement('input');check.type='checkbox';check.checked=this.data.withHeadings;check.disabled=this.readOnly;check.addEventListener('change',()=>{this.data.withHeadings=check.checked});header.append(check,document.createTextNode(' First row is header'));controls.append(header);if(!this.readOnly){for(const [label,action] of [['+ Row',()=>{this.data.content.push(Array(this.columns()).fill(''));this.rerender()}],['− Row',()=>{if(this.data.content.length>1)this.data.content.pop();this.rerender()}],['+ Column',()=>{for(const row of this.data.content)row.push('');this.rerender()}],['− Column',()=>{if(this.columns()>1)for(const row of this.data.content)row.pop();this.rerender()}]] as Array<[string,()=>void]>){const button=document.createElement('button');button.type='button';button.textContent=label;button.addEventListener('click',action);controls.append(button)}}this.host.append(controls);const table=document.createElement('table');table.className='interactive-learn-editor-table';this.data.content.forEach((row,rowIndex)=>{const tr=document.createElement('tr');row.forEach((value,columnIndex)=>{const cell=document.createElement(rowIndex===0&&this.data.withHeadings?'th':'td');cell.contentEditable=String(!this.readOnly);cell.textContent=value;cell.addEventListener('input',()=>{this.data.content[rowIndex][columnIndex]=cell.textContent||''});tr.append(cell)});table.append(tr)});this.host.append(table)}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}


type NestedEditorBlock={id:string;type:'paragraph'|'header'|'quote'|'callout'|'code';data:Record<string,unknown>};
type VisualSection={id:string;title:string;blocks:NestedEditorBlock[]};
const nestedId=(prefix='nested')=>`${prefix}-${globalThis.crypto?.randomUUID?.()||`${Date.now()}-${Math.random().toString(36).slice(2,8)}`}`;
function normalizeNestedEditorBlocks(value:unknown,legacyContent=''):NestedEditorBlock[]{
  const source=Array.isArray(value)?value:[];const blocks:NestedEditorBlock[]=[];
  for(const raw of source.slice(0,100)){if(!raw||typeof raw!=='object'||Array.isArray(raw))continue;const item=raw as {id?:unknown;type?:unknown;data?:unknown};const data=(item.data&&typeof item.data==='object'&&!Array.isArray(item.data)?item.data:{}) as Record<string,unknown>;const type=String(item.type||'paragraph');if(!['paragraph','header','quote','callout','code'].includes(type))continue;blocks.push({id:String(item.id||nestedId('block')).slice(0,100),type:type as NestedEditorBlock['type'],data:{...data}})}
  if(!blocks.length&&legacyContent)blocks.push({id:nestedId('legacy'),type:'paragraph',data:{text:legacyContent}});
  return blocks;
}
function nestedBlockEditor(block:NestedEditorBlock,readOnly:boolean,onChange:()=>void){
  const body=document.createElement('div');body.className='interactive-learn-editor-nested-block-body';
  if(block.type==='paragraph'||block.type==='quote'){const area=textArea(String(block.data.text||''),block.type==='quote'?'Quote':'Paragraph');area.disabled=readOnly;area.addEventListener('input',()=>{block.data.text=area.value;onChange()});body.append(area);if(block.type==='quote'){const caption=textInput(String(block.data.caption||''),'Caption');caption.disabled=readOnly;caption.addEventListener('input',()=>{block.data.caption=caption.value;onChange()});body.append(caption)}}
  else if(block.type==='header'){const input=textInput(String(block.data.text||''),'Heading');input.disabled=readOnly;input.addEventListener('input',()=>{block.data.text=input.value;onChange()});const level=document.createElement('select');level.className='interactive-learn-editor-tool-input';for(const n of [2,3,4]){const o=document.createElement('option');o.value=String(n);o.textContent=`H${n}`;level.append(o)}level.value=String(block.data.level||3);level.disabled=readOnly;level.addEventListener('change',()=>{block.data.level=Number(level.value);onChange()});body.append(input,level)}
  else if(block.type==='callout'){const title=textInput(String(block.data.title||''),'Callout title');title.disabled=readOnly;title.addEventListener('input',()=>{block.data.title=title.value;onChange()});const area=textArea(String(block.data.text||''),'Callout text');area.disabled=readOnly;area.addEventListener('input',()=>{block.data.text=area.value;onChange()});body.append(title,area)}
  else if(block.type==='code'){const filename=textInput(String(block.data.filename||''),'Filename');filename.disabled=readOnly;filename.addEventListener('input',()=>{block.data.filename=filename.value;onChange()});const area=textArea(String(block.data.code||''),'Code');area.disabled=readOnly;area.spellcheck=false;area.addEventListener('input',()=>{block.data.code=area.value;onChange()});body.append(filename,area)}
  return body;
}
function createVisualSectionTool(kind:'tabs'|'accordion'):LearnToolClass{
  const key=kind==='tabs'?'tabs':'items';const singular=kind==='tabs'?'Tab':'Section';
  return class VisualSectionTool{
    static get toolbox(){return{title:kind==='tabs'?'Tabs':'Accordion',icon:ICONS.list};}
    static get isReadOnlySupported(){return true;}
    data:Record<string,unknown>;readOnly:boolean;host?:HTMLElement;active=0;
    constructor({data,readOnly}:ToolArgs){this.readOnly=Boolean(readOnly);const source=cloneData(data,{});const raw=Array.isArray(source[key])?source[key] as unknown[]:[];const sections:VisualSection[]=raw.slice(0,kind==='tabs'?20:50).map((entry,index)=>{const item=(entry&&typeof entry==='object'&&!Array.isArray(entry)?entry:{}) as Record<string,unknown>;return{id:String(item.id||nestedId(kind==='tabs'?'tab':'section')).slice(0,100),title:String(item.title||`${singular} ${index+1}`).slice(0,500),blocks:normalizeNestedEditorBlocks(item.blocks,String(item.content||''))}});if(!sections.length)sections.push({id:nestedId(kind==='tabs'?'tab':'section'),title:`${singular} 1`,blocks:[]});this.data={[key]:sections};}
    sections(){return this.data[key] as VisualSection[];}
    move(index:number,delta:number){const list=this.sections();const target=index+delta;if(target<0||target>=list.length)return;[list[index],list[target]]=[list[target],list[index]];this.active=target;this.rerender();}
    renderNested(section:VisualSection){const wrap=document.createElement('div');wrap.className='interactive-learn-editor-nested-blocks';section.blocks.forEach((block,index)=>{const card=document.createElement('div');card.className='interactive-learn-editor-nested-block';const head=document.createElement('div');head.className='interactive-learn-editor-nested-block-head';const label=document.createElement('strong');label.textContent=block.type;head.append(label);if(!this.readOnly){for(const [caption,action,disabled] of [['↑',()=>{if(index>0){[section.blocks[index-1],section.blocks[index]]=[section.blocks[index],section.blocks[index-1]];this.rerender()}},index===0],['↓',()=>{if(index<section.blocks.length-1){[section.blocks[index+1],section.blocks[index]]=[section.blocks[index],section.blocks[index+1]];this.rerender()}},index===section.blocks.length-1],['×',()=>{section.blocks.splice(index,1);this.rerender()},false]] as Array<[string,()=>void,boolean]>){const b=document.createElement('button');b.type='button';b.textContent=caption;b.disabled=disabled;b.addEventListener('click',action);head.append(b)}}card.append(head,nestedBlockEditor(block,this.readOnly,()=>{}));wrap.append(card)});if(!this.readOnly){const addRow=document.createElement('div');addRow.className='interactive-learn-editor-nested-add';const select=document.createElement('select');select.className='interactive-learn-editor-tool-input';for(const type of ['paragraph','header','quote','callout','code']){const o=document.createElement('option');o.value=type;o.textContent=type[0].toUpperCase()+type.slice(1);select.append(o)}const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary';add.textContent='+ Add block';add.addEventListener('click',()=>{const type=select.value as NestedEditorBlock['type'];const defaults:Record<string,unknown>=type==='header'?{text:'',level:3}:type==='callout'?{tone:'info',title:'',text:''}:type==='code'?{language:'text',filename:'',code:'',stdin:'',runnable:false}:{text:''};section.blocks.push({id:nestedId('block'),type,data:defaults});this.rerender()});addRow.append(select,add);wrap.append(addRow)}return wrap;}
    rerender(){if(!this.host)return;const list=this.sections();this.active=kind==='accordion'?Math.max(-1,Math.min(this.active,list.length-1)):Math.max(0,Math.min(this.active,list.length-1));this.host.replaceChildren();const nav=document.createElement('div');nav.className=kind==='tabs'?'interactive-learn-editor-tabs-nav':'interactive-learn-editor-accordion-nav';list.forEach((section,index)=>{const row=document.createElement('div');row.className='interactive-learn-editor-section-tab';if(index===this.active)row.classList.add('is-active');const title=textInput(section.title,`${singular} ${index+1}`);title.disabled=this.readOnly;title.addEventListener('input',()=>{section.title=title.value});title.addEventListener('focus',()=>{this.active=index});row.append(title);const open=document.createElement('button');open.type='button';open.textContent=kind==='tabs'?'Open':index===this.active?'Collapse':'Edit';open.addEventListener('click',()=>{this.active=this.active===index&&kind==='accordion'?-1:index;this.rerender()});row.append(open);if(!this.readOnly){const up=document.createElement('button');up.type='button';up.textContent='↑';up.title=`Move ${singular.toLowerCase()} up`;up.disabled=index===0;up.addEventListener('click',()=>this.move(index,-1));const down=document.createElement('button');down.type='button';down.textContent='↓';down.title=`Move ${singular.toLowerCase()} down`;down.disabled=index===list.length-1;down.addEventListener('click',()=>this.move(index,1));const remove=document.createElement('button');remove.type='button';remove.textContent='×';remove.title=`Delete ${singular.toLowerCase()}`;remove.disabled=list.length===1;remove.addEventListener('click',()=>{list.splice(index,1);this.active=Math.max(0,index-1);this.rerender()});row.append(up,down,remove)}nav.append(row)});this.host.append(nav);const current=list[this.active];if(current)this.host.append(this.renderNested(current));if(!this.readOnly){const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary';add.textContent=`+ Add ${singular.toLowerCase()}`;add.addEventListener('click',()=>{list.push({id:nestedId(kind==='tabs'?'tab':'section'),title:`${singular} ${list.length+1}`,blocks:[]});this.active=list.length-1;this.rerender()});this.host.append(add)}}
    render(){const root=document.createElement('div');root.className=`interactive-learn-editor-tool interactive-learn-editor-${kind}-tool`;this.host=root;this.rerender();return root;}
    save(){return this.data;}
  } as unknown as LearnToolClass;
}

class RunnableCodeTool {
  static get toolbox(){return{title:'Code',icon:ICONS.code};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;output?:HTMLElement;
  constructor({data,readOnly,config}:ToolArgs){
    const requested=String(data?.projectModeId||data?.language||recentCodeModeIds[0]||'javascript').trim().toLowerCase();
    const aliases:Record<string,string>={js:'javascript',ts:'typescript',py:'python',sh:'shell',bash:'shell','c++':'cpp'};
    const starter=snippetStarterForMode(aliases[requested]||requested)||snippetStarterForMode('javascript');
    const source=cloneData(data,{
      language:starter?.mode.primaryLanguageId||'javascript',projectModeId:starter?.mode.id||'javascript',filename:starter?.filename||'main.js',
      code:starter?.code||'console.log("Hello, Learnitive!");',stdin:'',allowStdin:false,runnable:false,lineNumbers:true,
    });
    if(data?.allowStdin===undefined&&String(data?.stdin||''))source.allowStdin=true;
    const normalized=snippetStarterForMode(String(source.projectModeId||source.language||''));
    if(normalized){source.projectModeId=normalized.mode.id;source.language=normalized.mode.primaryLanguageId;if(!String(source.filename||'').trim())source.filename=normalized.filename;if(data?.code===undefined)source.code=normalized.code;}
    this.data=source;this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);rememberRecentCodeMode(String(source.projectModeId||''));
  }
  render(){
    const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-code-tool';
    const primary=document.createElement('div');primary.className='interactive-learn-editor-code-primary';
    let codeControl:HTMLTextAreaElement|undefined;let filenameControl:HTMLInputElement|undefined;
    const runtimeWrap=document.createElement('label');runtimeWrap.className='interactive-learn-editor-code-runtime-search';const runtimeLabel=document.createElement('span');runtimeLabel.textContent='Language';
    const runtime=document.createElement('select');runtime.className='interactive-learn-editor-tool-input';runtime.setAttribute('aria-label','Code language');runtime.disabled=this.readOnly;
    for(const mode of orderedCodeModes()){const option=document.createElement('option');option.value=mode.id;option.textContent=recentCodeModeIds.includes(mode.id)?`${mode.label} · Recent`:mode.label;runtime.append(option)}
    const currentStarter=snippetStarterForMode(String(this.data.projectModeId||''))||snippetStarterForMode('javascript');runtime.value=currentStarter?.mode.id||'javascript';
    runtime.addEventListener('change',()=>{
      const previous=snippetStarterForMode(String(this.data.projectModeId||''));const next=snippetStarterForMode(runtime.value);if(!next)return;
      const existingCode=String(this.data.code||'');const sourceIsStarter=!existingCode.trim()||Boolean(previous&&existingCode===previous.code);
      this.data.projectModeId=next.mode.id;this.data.language=next.mode.primaryLanguageId;rememberRecentCodeMode(next.mode.id);
      const existingFilename=String(this.data.filename||'');if(!existingFilename.trim()||Boolean(previous&&existingFilename===previous.filename)){this.data.filename=next.filename;if(filenameControl)filenameControl.value=next.filename;}
      if(sourceIsStarter){this.data.code=next.code;if(codeControl)codeControl.value=next.code;}
    });
    runtimeWrap.append(runtimeLabel,runtime);primary.append(runtimeWrap);
    const badge=document.createElement('span');badge.className='interactive-learn-block-label';badge.textContent=this.data.runnable===true?'Runnable Code':'Code · display only';primary.append(badge);root.append(primary);
    const code=textArea(String(this.data.code||''),'Paste code or start typing…');codeControl=code;code.classList.add('interactive-learn-editor-code-input');code.spellcheck=false;code.disabled=this.readOnly;code.addEventListener('input',()=>{this.data.code=code.value});root.append(code);
    const actions=document.createElement('div');actions.className='interactive-learn-editor-code-actions';
    const output=document.createElement('pre');output.className='interactive-learn-editor-code-output';output.textContent=this.data.runnable===true?'Ready to run in the Learnitive sandbox.':'Display-only code. Enable Runnable Code in settings if readers should execute it.';this.output=output;
    let runButton:HTMLButtonElement|undefined;let sandbox:HTMLElement|undefined;if(!this.readOnly){const run=document.createElement('button');run.type='button';run.className='interactive-button interactive-button-primary';run.textContent='▶ Run';run.hidden=this.data.runnable!==true;run.addEventListener('click',async()=>{const selected=snippetStarterForMode(String(this.data.projectModeId||''));if(!selected){output.textContent='This language cannot be run as a standalone code block.';output.dataset.status='error';return}const source=String(this.data.code||'');if(!source.trim()){output.textContent='Add code before running this block.';output.dataset.status='error';return}run.disabled=true;run.textContent='Running…';output.textContent='Queued…';output.dataset.status='running';try{const result=await this.adapter.runCodePreview({projectModeId:selected.mode.id,language:selected.mode.primaryLanguageId,filename:String(this.data.filename||selected.filename),code:source,stdin:Boolean(this.data.allowStdin)?String(this.data.stdin||''):''});output.textContent=[result.stdout,result.stderr].filter(Boolean).join('\n')||`Process exited with code ${result.exitCode}`;output.dataset.status=result.exitCode===0?'success':'error';}catch(reason){output.textContent=reason instanceof Error?reason.message:String(reason);output.dataset.status='error';}finally{run.disabled=false;run.textContent='▶ Run'}});runButton=run;actions.append(run)}
    sandbox=document.createElement('span');sandbox.className='interactive-learn-block-label';sandbox.textContent='Runs in the Learnitive sandbox';sandbox.hidden=this.data.runnable!==true;actions.append(sandbox);root.append(actions,output);
    const advanced=document.createElement('details');advanced.className='interactive-learn-editor-code-advanced';const summary=document.createElement('summary');summary.textContent='Code settings';advanced.append(summary);
    const filename=textInput(String(this.data.filename||''),'Filename');filenameControl=filename;filename.disabled=this.readOnly;filename.addEventListener('input',()=>{this.data.filename=filename.value});advanced.append(labelled('Filename',filename));
    const runnable=document.createElement('input');runnable.type='checkbox';runnable.checked=this.data.runnable===true;runnable.disabled=this.readOnly;const runnableRow=document.createElement('label');runnableRow.className='interactive-learn-editor-tool-check';runnableRow.append(runnable,document.createTextNode(' Runnable Code — readers can execute this block'));runnable.addEventListener('change',()=>{this.data.runnable=runnable.checked;badge.textContent=runnable.checked?'Runnable Code':'Code · display only';output.textContent=runnable.checked?'Ready to run in the Learnitive sandbox.':'Display-only code. Enable Runnable Code in settings if readers should execute it.';if(runButton)runButton.hidden=!runnable.checked;if(sandbox)sandbox.hidden=!runnable.checked});advanced.append(runnableRow);
    const allow=document.createElement('input');allow.type='checkbox';allow.checked=Boolean(this.data.allowStdin);allow.disabled=this.readOnly;const allowRow=document.createElement('label');allowRow.className='interactive-learn-editor-tool-check interactive-learn-editor-code-stdin-toggle';allowRow.append(allow,document.createTextNode(this.adapter.context==='learn'?' Allow learner standard input':' Allow reader standard input'));advanced.append(allowRow);const stdin=textArea(String(this.data.stdin||''),'Optional default standard input');stdin.classList.add('interactive-learn-editor-code-stdin');stdin.disabled=this.readOnly;stdin.addEventListener('input',()=>{this.data.stdin=stdin.value});const stdinField=labelled('Default standard input',stdin);stdinField.hidden=!Boolean(this.data.allowStdin);allow.addEventListener('change',()=>{this.data.allowStdin=allow.checked;stdinField.hidden=!allow.checked});advanced.append(stdinField);root.append(advanced);
    return root;
  }
  save(){return this.data;}
}

class TerminalTool {
  static get toolbox(){return{title:'Terminal',icon:ICONS.terminal};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;
  readOnly:boolean;adapter:InteractiveContentEditorAdapter;
  constructor({data,readOnly,config}:ToolArgs){this.data=cloneData(data,{title:'Terminal',instructions:'',projectModeId:'shell',starter:{files:{'README.md':'# Start here\n'},entryFile:'README.md'},workingDirectory:'',setupCommand:'',checkCommand:'',required:false});this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  render(){
    const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-terminal-tool';
    const title=textInput(String(this.data.title||'Terminal'),'Terminal title');title.disabled=this.readOnly;title.addEventListener('input',()=>{this.data.title=title.value});root.append(labelled('Title',title));
    const instructions=textArea(String(this.data.instructions||''),'What should the learner do in the terminal?');instructions.disabled=this.readOnly;instructions.addEventListener('input',()=>{this.data.instructions=instructions.value});root.append(labelled('Instructions',instructions));
    const select=runtimeSelect(String(this.data.projectModeId||'shell'),this.adapter);select.disabled=this.readOnly;select.addEventListener('change',()=>{this.data.projectModeId=select.value||'shell'});root.append(labelled('Runtime',select));
    const working=textInput(String(this.data.workingDirectory||''),'/workspace');working.disabled=this.readOnly;working.addEventListener('input',()=>{this.data.workingDirectory=working.value});root.append(labelled('Working directory',working));
    const setup=textInput(String(this.data.setupCommand||''),'Optional command to run when the terminal opens');setup.disabled=this.readOnly;setup.addEventListener('input',()=>{this.data.setupCommand=setup.value});root.append(labelled('Setup command',setup));
    const starter=(this.data.starter&&typeof this.data.starter==='object'?this.data.starter:{files:{}}) as {files?:Record<string,string>;entryFile?:string};const initialPath=starter.entryFile||Object.keys(starter.files||{})[0]||'README.md';
    const starterPath=textInput(initialPath,'README.md');starterPath.disabled=this.readOnly;const starterContent=textArea(String(starter.files?.[initialPath]||''),'Optional starter file contents');starterContent.disabled=this.readOnly;
    const syncStarter=()=>{const path=starterPath.value.trim()||'README.md';this.data.starter={files:{[path]:starterContent.value},entryFile:path};};starterPath.addEventListener('input',syncStarter);starterContent.addEventListener('input',syncStarter);root.append(labelled('Starter file',starterPath),labelled('Starter content',starterContent));
    const required=document.createElement('input');required.type='checkbox';required.checked=Boolean(this.data.required);required.disabled=this.readOnly;required.addEventListener('change',()=>{this.data.required=required.checked});const requiredRow=document.createElement('label');requiredRow.className='interactive-learn-editor-tool-check';requiredRow.append(required,document.createTextNode(` ${this.adapter.requiredLabel||'Required activity'}`));root.append(requiredRow);
    const preview=document.createElement('div');preview.className='interactive-learn-editor-terminal-preview';preview.innerHTML='<span>Learnitive Terminal</span><code>student@learnitive:~$</code>';root.append(preview);
    return root;
  }
  save(){return this.data;}
}


type TerminalRuleEditor={type:string;label:string;path:string;value:string;command:string;expected:string;match:string;exitCode:number;name:string;namespace:string;kind:string;field:string;port:number;running:boolean};
const TERMINAL_RULE_OPTIONS=[['file-exists','File exists'],['directory-exists','Directory exists'],['file-contains','File contains'],['file-equals','File equals'],['command-exit','Command exit status'],['command-output','Command output'],['package-installed','Package installed'],['process-running','Process running'],['port-listening','Port listening'],['environment-variable','Environment variable'],['docker-container','Docker container'],['kubernetes-resource','Kubernetes resource'],['git-branch','Git branch'],['git-ref','Git commit / tag / ref'],['ansible-check','Ansible desired-state check']] as const;
function terminalRuleDefault(index=0):TerminalRuleEditor{return{type:'file-exists',label:`Check ${index+1}`,path:'',value:'',command:'',expected:'',match:'equals',exitCode:0,name:'',namespace:'default',kind:'',field:'',port:8080,running:true};}
function normalizeTerminalRuleEditor(raw:unknown,index:number):TerminalRuleEditor{const value=(raw&&typeof raw==='object'&&!Array.isArray(raw)?raw:{}) as Record<string,unknown>;const base=terminalRuleDefault(index);return{...base,...value,type:String(value.type||base.type),label:String(value.label||base.label),path:String(value.path||''),value:String(value.value??''),command:String(value.command??''),expected:String(value.expected??''),match:String(value.match||'equals'),exitCode:Number(value.exitCode||0),name:String(value.name||''),namespace:String(value.namespace||'default'),kind:String(value.kind||''),field:String(value.field||''),port:Number(value.port||8080),running:value.running!==false};}
function normalizeTerminalTaskAuthorPath(value:string){let out=String(value||'').trim().replace(/\\/gu,'/');if(out==='/workspace')return'.';if(out.startsWith('/workspace/'))out=out.slice('/workspace/'.length);out=out.replace(/^\.\//u,'');return out||'.';}
function terminalTaskExample(kind:'linux'|'docker'|'kubernetes'|'git'|'ansible'|'networking'){
  if(kind==='linux')return{title:'Create backup status',instructions:'Create a directory named backups, create backups/status.txt, and put exactly "ready" in the file. Then verify the task.',projectModeId:'shell',starter:{files:{'README.md':'# Backup status task\n\nCreate `backups/status.txt` containing exactly `ready`.\n'},entryFile:'README.md'},workingDirectory:'.',setupCommand:'',feedbackMode:'detailed',verificationRules:[{...terminalRuleDefault(0),type:'directory-exists',label:'backups directory exists',path:'backups'},{...terminalRuleDefault(1),type:'file-equals',label:'status file contains ready',path:'backups/status.txt',value:'ready'}]};
  if(kind==='docker')return{title:'Run an nginx container',instructions:'Create and run a Docker container named web using the nginx image. Leave it running, then verify the task.',projectModeId:'shell',starter:{files:{'README.md':'# Docker task\n\nRun an nginx container named `web`.\n'},entryFile:'README.md'},workingDirectory:'.',setupCommand:'',feedbackMode:'detailed',verificationRules:[{...terminalRuleDefault(0),type:'docker-container',label:'web container is running',name:'web',running:true}]};
  if(kind==='kubernetes')return{title:'Scale nginx deployment',instructions:'Apply the supplied Deployment and change Deployment/default/nginx so it has exactly 3 replicas.',projectModeId:'shell',starter:{files:{'deployment.yaml':'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:alpine\n'},entryFile:'deployment.yaml'},workingDirectory:'.',setupCommand:'',feedbackMode:'detailed',verificationRules:[{...terminalRuleDefault(0),type:'kubernetes-resource',label:'nginx has 3 replicas',kind:'deployment',name:'nginx',namespace:'default',field:'{.spec.replicas}',value:'3'}]};
  if(kind==='git')return{title:'Create release branch',instructions:'The workspace contains an initialized Git repository. Create a branch named release and create RELEASE.md containing the word ready.',projectModeId:'shell',starter:{files:{'README.md':'# Git release task\n\nCreate the `release` branch and add `RELEASE.md` containing `ready`.\n'},entryFile:'README.md'},workingDirectory:'.',setupCommand:'git init -q && git config user.email learner@example.invalid && git config user.name Learner && git add README.md && git commit -qm "Initial task state"',feedbackMode:'detailed',verificationRules:[{...terminalRuleDefault(0),type:'git-branch',label:'release branch exists',name:'release'},{...terminalRuleDefault(1),type:'file-contains',label:'RELEASE.md contains ready',path:'RELEASE.md',value:'ready'}]};
  if(kind==='ansible')return{title:'Apply desired state',instructions:'Complete site.yml so the playbook passes Ansible check mode without changes failing, then verify the task.',projectModeId:'shell',starter:{files:{'site.yml':'---\n- name: Local desired state\n  hosts: localhost\n  connection: local\n  gather_facts: false\n  tasks:\n    # Add your task here\n'},entryFile:'site.yml'},workingDirectory:'.',setupCommand:'',feedbackMode:'detailed',verificationRules:[{...terminalRuleDefault(0),type:'ansible-check',label:'Ansible desired state passes',command:'ansible-playbook --check site.yml',exitCode:0}]};
  return{title:'Listen on port 8080',instructions:'Use the supplied server.py or another program to start a service that listens on TCP port 8080. Keep it running while you verify the task.',projectModeId:'shell',starter:{files:{'server.py':'from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler\n\nThreadingHTTPServer(("0.0.0.0", 8080), SimpleHTTPRequestHandler).serve_forever()\n','README.md':'# Networking task\n\nStart a TCP service on port 8080 and leave it running.\n'},entryFile:'server.py'},workingDirectory:'.',setupCommand:'',feedbackMode:'detailed',verificationRules:[{...terminalRuleDefault(0),type:'port-listening',label:'TCP 8080 is listening',port:8080}]};
}

class TerminalTaskTool {
  static get toolbox(){return{title:'Terminal Task',icon:ICONS.terminal};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;host?:HTMLElement;
  constructor({data,readOnly,config}:ToolArgs){const source=cloneData(data,{title:'Create backup status',instructions:'Create a directory named backups, create backups/status.txt, and put "ready" in the file.',projectModeId:'shell',starter:{files:{'README.md':'Complete the terminal task.\n'},entryFile:'README.md'},workingDirectory:'.',setupCommand:'',required:true,resetPolicy:'allowed',attemptPolicy:'unlimited',feedbackMode:'detailed',verifierTimeoutSeconds:8,verificationRules:terminalTaskExample('linux').verificationRules,advancedVerifier:''});this.data={...source,verificationRules:Array.isArray(source.verificationRules)?source.verificationRules.map(normalizeTerminalRuleEditor):terminalTaskExample('linux').verificationRules};this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  rules(){return this.data.verificationRules as TerminalRuleEditor[];}
  applyExample(kind:'linux'|'docker'|'kubernetes'|'git'|'ansible'|'networking'){const example=terminalTaskExample(kind);this.data={...this.data,...example};this.rerender();}
  ruleEditor(rule:TerminalRuleEditor,index:number){const card=document.createElement('div');card.className='interactive-learn-editor-terminal-rule';const type=document.createElement('select');type.className='interactive-learn-editor-tool-input';for(const [value,label] of TERMINAL_RULE_OPTIONS){const option=document.createElement('option');option.value=value;option.textContent=label;type.append(option)}type.value=rule.type;type.disabled=this.readOnly;type.addEventListener('change',()=>{rule.type=type.value;this.rerender()});const label=textInput(rule.label,`Check ${index+1}`);label.disabled=this.readOnly;label.addEventListener('input',()=>{rule.label=label.value});card.append(labelled('Check type',type),labelled('Feedback label',label));const addText=(caption:string,key:keyof TerminalRuleEditor,placeholder='')=>{const input=textInput(String(rule[key]??''),placeholder);input.disabled=this.readOnly;input.addEventListener('input',()=>{(rule as unknown as Record<string,unknown>)[key]=input.value});if(key==='path')input.addEventListener('blur',()=>{const normalized=normalizeTerminalTaskAuthorPath(input.value);input.value=normalized;(rule as unknown as Record<string,unknown>)[key]=normalized});card.append(labelled(caption,input));};if(['file-exists','directory-exists','file-contains','file-equals'].includes(rule.type))addText('Path','path','backups/status.txt');if(['file-contains','file-equals','environment-variable'].includes(rule.type))addText('Expected value','value','ready');if(['command-exit','command-output','ansible-check'].includes(rule.type))addText('Command','command','test -f example');if(rule.type==='command-output'){addText('Expected output','expected','ready');const match=document.createElement('select');match.className='interactive-learn-editor-tool-input';for(const value of ['equals','contains','regex']){const option=document.createElement('option');option.value=value;option.textContent=value;match.append(option)}match.value=rule.match;match.disabled=this.readOnly;match.addEventListener('change',()=>{rule.match=match.value});card.append(labelled('Output comparison',match));}if(['package-installed','process-running','environment-variable','docker-container','git-branch','git-ref'].includes(rule.type))addText(rule.type==='environment-variable'?'Variable name':'Name / pattern','name','');if(rule.type==='process-running')addText('Process pattern','value','nginx');if(rule.type==='kubernetes-resource'){addText('Kind','kind','deployment');addText('Resource name','name','nginx');addText('Namespace','namespace','default');addText('Optional JSONPath field','field','{.spec.replicas}');addText('Expected field value','value','3');}if(rule.type==='port-listening'){const port=document.createElement('input');port.type='number';port.min='1';port.max='65535';port.className='interactive-learn-editor-tool-input';port.value=String(rule.port||8080);port.disabled=this.readOnly;port.addEventListener('input',()=>{rule.port=Math.min(65535,Math.max(1,Number(port.value)||1))});card.append(labelled('TCP port',port));}if(['command-exit','ansible-check'].includes(rule.type)){const exit=document.createElement('input');exit.type='number';exit.min='0';exit.max='255';exit.className='interactive-learn-editor-tool-input';exit.value=String(rule.exitCode||0);exit.disabled=this.readOnly;exit.addEventListener('input',()=>{rule.exitCode=Math.min(255,Math.max(0,Number(exit.value)||0))});card.append(labelled('Expected exit code',exit));}if(!this.readOnly){const remove=document.createElement('button');remove.type='button';remove.className='interactive-button interactive-button-secondary';remove.textContent='Remove check';remove.addEventListener('click',()=>{this.rules().splice(index,1);this.rerender()});card.append(remove)}return card;}
  rerender(){if(!this.host)return;const root=this.host;root.replaceChildren();const help=document.createElement('div');help.className='interactive-learn-editor-exercise-help';help.innerHTML='<strong>Terminal Task workflow</strong><ol><li>Describe the desired end state.</li><li>Choose the environment.</li><li>Configure starter state.</li><li>Add protected verification rules.</li><li>Test the idea in an author sandbox.</li><li>Save.</li><li>Learners make real sandbox changes.</li><li>Verify Task evaluates protected state.</li></ol><p>Verifier rules and the advanced checker are stored separately from public lesson JSON and never sent to learners.</p><p><strong>Workspace paths:</strong> Paths are relative to this Terminal Task’s isolated workspace. Use <code>backups/status.txt</code>, not <code>/workspace/backups/status.txt</code>.</p>';root.append(help);if(!this.readOnly){const examples=document.createElement('div');examples.className='interactive-learn-editor-exercise-templates';const label=document.createElement('strong');label.textContent='Load example';examples.append(label);for(const [kind,text] of [['linux','Linux'],['ansible','Ansible'],['docker','Docker'],['kubernetes','Kubernetes'],['git','Git'],['networking','Networking']] as const){const button=document.createElement('button');button.type='button';button.className='interactive-button interactive-button-secondary';button.textContent=text;button.addEventListener('click',()=>this.applyExample(kind));examples.append(button)}root.append(examples)}const title=textInput(String(this.data.title||'Terminal task'),'Task title');title.disabled=this.readOnly;title.addEventListener('input',()=>{this.data.title=title.value});const instructions=textArea(String(this.data.instructions||''),'Describe the required end state.');instructions.disabled=this.readOnly;instructions.addEventListener('input',()=>{this.data.instructions=instructions.value});const runtime=runtimeSelect(String(this.data.projectModeId||'shell'),this.adapter);runtime.disabled=this.readOnly;runtime.addEventListener('change',()=>{this.data.projectModeId=runtime.value||'shell'});const working=textInput(String(this.data.workingDirectory||'.'),'.');working.disabled=this.readOnly;working.addEventListener('input',()=>{this.data.workingDirectory=working.value});working.addEventListener('blur',()=>{const normalized=normalizeTerminalTaskAuthorPath(working.value);working.value=normalized;this.data.workingDirectory=normalized});const setup=textArea(String(this.data.setupCommand||''),'Optional setup command executed once inside the task sandbox.');setup.disabled=this.readOnly;setup.addEventListener('input',()=>{this.data.setupCommand=setup.value});root.append(labelled('Task title',title),labelled('Instructions',instructions),labelled('Runtime / environment',runtime),labelled('Working directory',working),labelled('Optional setup command',setup));const starter=(this.data.starter&&typeof this.data.starter==='object'?this.data.starter:{files:{}}) as {files?:Record<string,string>;entryFile?:string};const starterPath=starter.entryFile||Object.keys(starter.files||{})[0]||'README.md';const pathInput=textInput(starterPath,'README.md');pathInput.disabled=this.readOnly;const content=textArea(String(starter.files?.[starterPath]||''),'Starter file contents');content.disabled=this.readOnly;const sync=()=>{const path=pathInput.value.trim()||'README.md';this.data.starter={files:{[path]:content.value},entryFile:path};};pathInput.addEventListener('input',sync);content.addEventListener('input',sync);root.append(labelled('Starter file',pathInput),labelled('Starter contents',content));const verificationDetails=document.createElement('details');verificationDetails.className='interactive-learn-editor-exercise-advanced';const verificationSummary=document.createElement('summary');verificationSummary.textContent='Verification — private, not sent to readers';verificationDetails.append(verificationSummary);root.append(verificationDetails);const section=document.createElement('div');section.className='interactive-learn-editor-terminal-rules';const head=document.createElement('div');head.className='interactive-learn-editor-exercise-section-head';head.innerHTML='<strong>Protected verification</strong><small>These checks run inside the learner task sandbox and are never serialized to the browser.</small>';section.append(head);this.rules().forEach((rule,index)=>section.append(this.ruleEditor(rule,index)));if(!this.readOnly){const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary';add.textContent='+ Add verification check';add.disabled=this.rules().length>=40;add.addEventListener('click',()=>{this.rules().push(terminalRuleDefault(this.rules().length));this.rerender()});section.append(add)}verificationDetails.append(section);const advanced=textArea(String(this.data.advancedVerifier||''),'Advanced server-side checker script (optional)');advanced.disabled=this.readOnly;advanced.addEventListener('input',()=>{this.data.advancedVerifier=advanced.value});verificationDetails.append(labelled('Advanced verifier',advanced));const timeout=document.createElement('input');timeout.type='number';timeout.min='1';timeout.max='30';timeout.className='interactive-learn-editor-tool-input';timeout.value=String(this.data.verifierTimeoutSeconds||8);timeout.disabled=this.readOnly;timeout.addEventListener('input',()=>{this.data.verifierTimeoutSeconds=Math.min(30,Math.max(1,Number(timeout.value)||8))});verificationDetails.append(labelled('Verifier timeout (seconds per check)',timeout));const feedback=document.createElement('select');feedback.className='interactive-learn-editor-tool-input';for(const [value,label] of [['detailed','Detailed — show each check'],['summary','Summary — task not complete yet'],['exam','Exam — verification failed']] as const){const option=document.createElement('option');option.value=value;option.textContent=label;feedback.append(option)}feedback.value=String(this.data.feedbackMode||'detailed');feedback.disabled=this.readOnly;feedback.addEventListener('change',()=>{this.data.feedbackMode=feedback.value});const reset=document.createElement('select');reset.className='interactive-learn-editor-tool-input';for(const [value,label] of [['allowed','Allow environment reset'],['disabled','Do not allow reset']] as const){const option=document.createElement('option');option.value=value;option.textContent=label;reset.append(option)}reset.value=String(this.data.resetPolicy||'allowed');reset.disabled=this.readOnly;reset.addEventListener('change',()=>{this.data.resetPolicy=reset.value});const attempts=document.createElement('select');attempts.className='interactive-learn-editor-tool-input';for(const [value,label] of [['unlimited','Unlimited attempts'],['single','Single attempt']] as const){const option=document.createElement('option');option.value=value;option.textContent=label;attempts.append(option)}attempts.value=String(this.data.attemptPolicy||'unlimited');attempts.disabled=this.readOnly;attempts.addEventListener('change',()=>{this.data.attemptPolicy=attempts.value});verificationDetails.append(labelled('Feedback mode',feedback),labelled('Reset policy',reset),labelled('Attempt policy',attempts));const required=document.createElement('input');required.type='checkbox';required.checked=this.data.required!==false;required.disabled=this.readOnly;required.addEventListener('change',()=>{this.data.required=required.checked});const requiredRow=document.createElement('label');requiredRow.className='interactive-learn-editor-tool-check';requiredRow.append(required,document.createTextNode(` ${this.adapter.requiredLabel||'Required activity'}`));root.append(requiredRow);}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-terminal-task-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}


type ExerciseEditorTest={label:string;stdin:string;expectedStdout:string;stdoutMatch:'equals'|'contains'|'ignore';expectedStderr:string;stderrMatch:'equals'|'contains'|'empty'|'ignore';expectedExitCode:number;checkExitCode:boolean};
function normalizeExerciseEditorTest(raw:unknown,index:number):ExerciseEditorTest{const value=(raw&&typeof raw==='object'&&!Array.isArray(raw)?raw:{}) as Record<string,unknown>;return{label:String(value.label||`Test ${index+1}`),stdin:String(value.stdin||''),expectedStdout:String(value.expectedStdout??''),stdoutMatch:['equals','contains','ignore'].includes(String(value.stdoutMatch))?String(value.stdoutMatch) as ExerciseEditorTest['stdoutMatch']:'equals',expectedStderr:String(value.expectedStderr??''),stderrMatch:['equals','contains','empty','ignore'].includes(String(value.stderrMatch))?String(value.stderrMatch) as ExerciseEditorTest['stderrMatch']:'ignore',expectedExitCode:Number.isInteger(Number(value.expectedExitCode))?Number(value.expectedExitCode):0,checkExitCode:value.checkExitCode!==false};}
function pythonExerciseExample(){return{title:'Hello from stdin',instructions:'Write a Python program that reads a name from stdin and prints "Hello, <name>!"',language:'python',projectModeId:'python',starter:{files:{'main.py':'name = input()\n# TODO\n'},entryFile:'main.py'},entryFile:'main.py',exampleStdin:'Ada\n',visibleTests:[{label:'Ada',stdin:'Ada\n',expectedStdout:'Hello, Ada!',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true},{label:'Linus',stdin:'Linus\n',expectedStdout:'Hello, Linus!',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hiddenTests:[{label:'Hidden name',stdin:'Grace\n',expectedStdout:'Hello, Grace!',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hints:['Read the name with input().','Use an f-string or string formatting to build the greeting.'],solution:{files:{'main.py':'name = input()\nprint(f"Hello, {name}!")\n'},entryFile:'main.py'},required:true};}
function exerciseTemplate(kind:'python'|'javascript'|'c'|'shell'){if(kind==='python')return pythonExerciseExample();if(kind==='javascript')return{title:'JavaScript stdin exercise',instructions:'Read one line from stdin and print it back.',language:'javascript',projectModeId:'node-javascript',starter:{files:{'index.js':"const fs = require('fs');\nconst input = fs.readFileSync(0, 'utf8').trim();\n// TODO\n"},entryFile:'index.js'},entryFile:'index.js',exampleStdin:'hello\n',visibleTests:[{label:'Echo hello',stdin:'hello\n',expectedStdout:'hello',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hiddenTests:[{label:'Hidden echo',stdin:'world\n',expectedStdout:'world',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hints:[],solution:{files:{'index.js':"const fs = require('fs');\nconst input = fs.readFileSync(0, 'utf8').trim();\nconsole.log(input);\n"},entryFile:'index.js'},required:true};if(kind==='c')return{title:'C stdin exercise',instructions:'Read a word from stdin and print it.',language:'c',projectModeId:'c',starter:{files:{'main.c':'#include <stdio.h>\n\nint main(void) {\n    char value[128];\n    // TODO\n    return 0;\n}\n'},entryFile:'main.c'},entryFile:'main.c',exampleStdin:'hello\n',visibleTests:[{label:'Print hello',stdin:'hello\n',expectedStdout:'hello',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hiddenTests:[{label:'Hidden word',stdin:'world\n',expectedStdout:'world',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hints:[],solution:{files:{'main.c':'#include <stdio.h>\n\nint main(void) {\n    char value[128];\n    if (scanf("%127s", value) == 1) printf("%s\\n", value);\n    return 0;\n}\n'},entryFile:'main.c'},required:true};return{title:'Shell stdin exercise',instructions:'Read a line from stdin and print it.',language:'shell',projectModeId:'shell',starter:{files:{'main.sh':'#!/usr/bin/env bash\nread -r value\n# TODO\n'},entryFile:'main.sh'},entryFile:'main.sh',exampleStdin:'hello\n',visibleTests:[{label:'Echo hello',stdin:'hello\n',expectedStdout:'hello',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hiddenTests:[{label:'Hidden line',stdin:'world\n',expectedStdout:'world',stdoutMatch:'equals',expectedStderr:'',stderrMatch:'ignore',expectedExitCode:0,checkExitCode:true}],hints:[],solution:{files:{'main.sh':'#!/usr/bin/env bash\nread -r value\nprintf "%s\\n" "$value"\n'},entryFile:'main.sh'},required:true};}

class CodeExerciseTool {
  static get toolbox(){return{title:'Code exercise',icon:ICONS.code};}
  static get isReadOnlySupported(){return true;}
  data:Record<string,unknown>;readOnly:boolean;adapter:InteractiveContentEditorAdapter;host?:HTMLElement;
  constructor({data,readOnly,config}:ToolArgs){const source=cloneData(data,pythonExerciseExample());this.data={...pythonExerciseExample(),...source,starter:(source.starter&&typeof source.starter==='object'?source.starter:pythonExerciseExample().starter),visibleTests:Array.isArray(source.visibleTests)?source.visibleTests.map(normalizeExerciseEditorTest):pythonExerciseExample().visibleTests,hiddenTests:Array.isArray(source.hiddenTests)?source.hiddenTests.map(normalizeExerciseEditorTest):pythonExerciseExample().hiddenTests,solution:(source.solution&&typeof source.solution==='object'?source.solution:pythonExerciseExample().solution),hints:Array.isArray(source.hints)?source.hints.map(String):[]};this.readOnly=Boolean(readOnly);this.adapter=activeAdapter(config);}
  applyTemplate(kind:'python'|'javascript'|'c'|'shell'){this.data={...exerciseTemplate(kind)};this.rerender();}
  starter(){const raw=(this.data.starter&&typeof this.data.starter==='object'?this.data.starter:{files:{}}) as {files?:Record<string,string>;entryFile?:string};if(!raw.files||typeof raw.files!=='object')raw.files={};return raw;}
  solution(){const raw=(this.data.solution&&typeof this.data.solution==='object'?this.data.solution:{files:{}}) as {files?:Record<string,string>;entryFile?:string};if(!raw.files||typeof raw.files!=='object')raw.files={};return raw;}
  tests(key:'visibleTests'|'hiddenTests'){const value=this.data[key];return(Array.isArray(value)?value:[]) as ExerciseEditorTest[];}
  renderFiles(){const starter=this.starter();const wrap=document.createElement('div');wrap.className='interactive-learn-editor-exercise-files';const heading=document.createElement('div');heading.className='interactive-learn-editor-exercise-section-head';heading.innerHTML='<strong>Starter code and files</strong><small>These files are sent to the learner as the editable starting project.</small>';wrap.append(heading);Object.entries(starter.files||{}).forEach(([path,content],index)=>{const card=document.createElement('div');card.className='interactive-learn-editor-exercise-file';const pathInput=textInput(path,'main.py');pathInput.disabled=this.readOnly;const code=textArea(String(content),'Starter code');code.spellcheck=false;code.disabled=this.readOnly;pathInput.addEventListener('change',()=>{const next=pathInput.value.trim();if(!next||next===path)return;delete starter.files![path];starter.files![next]=code.value;if(starter.entryFile===path)starter.entryFile=next;this.data.entryFile=starter.entryFile||next;this.rerender()});code.addEventListener('input',()=>{starter.files![pathInput.value.trim()||path]=code.value});card.append(labelled('Filename',pathInput),labelled('Starter code',code));if(!this.readOnly){const actions=document.createElement('div');actions.className='interactive-learn-editor-file-actions';const entry=document.createElement('button');entry.type='button';entry.textContent=starter.entryFile===path?'Entry file ✓':'Set entry';entry.addEventListener('click',()=>{starter.entryFile=path;this.data.entryFile=path;this.rerender()});const remove=document.createElement('button');remove.type='button';remove.textContent='Remove';remove.disabled=Object.keys(starter.files||{}).length<=1;remove.addEventListener('click',()=>{delete starter.files![path];if(starter.entryFile===path)starter.entryFile=Object.keys(starter.files||{})[0];this.data.entryFile=starter.entryFile||'';this.rerender()});actions.append(entry,remove);card.append(actions)}wrap.append(card)});if(!this.readOnly){const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary';add.textContent='+ Add starter file';add.addEventListener('click',()=>{let i=Object.keys(starter.files||{}).length+1,name=`file${i}.txt`;while(Object.prototype.hasOwnProperty.call(starter.files,name)){i++;name=`file${i}.txt`}starter.files![name]='';if(!starter.entryFile)starter.entryFile=name;this.rerender()});wrap.append(add)}return wrap;}
  renderTests(key:'visibleTests'|'hiddenTests',title:string,description:string){const tests=this.tests(key);const wrap=document.createElement('div');wrap.className=`interactive-learn-editor-exercise-tests ${key==='hiddenTests'?'is-protected':''}`;const heading=document.createElement('div');heading.className='interactive-learn-editor-exercise-section-head';const strong=document.createElement('strong');strong.textContent=title;const small=document.createElement('small');small.textContent=description;heading.append(strong,small);wrap.append(heading);tests.forEach((test,index)=>{const card=document.createElement('div');card.className='interactive-learn-editor-exercise-test';const label=textInput(test.label,`Test ${index+1}`);label.disabled=this.readOnly;label.addEventListener('input',()=>{test.label=label.value});const stdin=textArea(test.stdin,'stdin for this test');stdin.disabled=this.readOnly;stdin.addEventListener('input',()=>{test.stdin=stdin.value});const expected=textArea(test.expectedStdout,'Expected stdout');expected.disabled=this.readOnly;expected.addEventListener('input',()=>{test.expectedStdout=expected.value});const mode=document.createElement('select');mode.className='interactive-learn-editor-tool-input';mode.disabled=this.readOnly;for(const value of ['equals','contains','ignore']){const option=document.createElement('option');option.value=value;option.textContent=value==='equals'?'Equals expected stdout':value==='contains'?'Contains expected text':'Ignore stdout';mode.append(option)}mode.value=test.stdoutMatch;mode.addEventListener('change',()=>{test.stdoutMatch=mode.value as ExerciseEditorTest['stdoutMatch']});const exit=document.createElement('input');exit.type='number';exit.min='0';exit.max='255';exit.className='interactive-learn-editor-tool-input';exit.value=String(test.expectedExitCode);exit.disabled=this.readOnly;exit.addEventListener('input',()=>{test.expectedExitCode=Math.min(255,Math.max(0,Number(exit.value)||0))});const checkExit=document.createElement('input');checkExit.type='checkbox';checkExit.checked=test.checkExitCode;checkExit.disabled=this.readOnly;checkExit.addEventListener('change',()=>{test.checkExitCode=checkExit.checked});const exitRow=document.createElement('label');exitRow.className='interactive-learn-editor-tool-check';exitRow.append(checkExit,document.createTextNode(' Check exit code'));card.append(labelled('Label',label),labelled('stdin',stdin),labelled('Expected stdout',expected),labelled('Stdout comparison',mode),labelled('Expected exit code',exit),exitRow);if(!this.readOnly){const remove=document.createElement('button');remove.type='button';remove.className='interactive-button interactive-button-secondary';remove.textContent='Remove test';remove.addEventListener('click',()=>{tests.splice(index,1);this.rerender()});card.append(remove)}wrap.append(card)});if(!this.readOnly){const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary';add.textContent='+ Add test';add.disabled=tests.length>=(key==='visibleTests'?10:20);add.addEventListener('click',()=>{tests.push(normalizeExerciseEditorTest({},tests.length));this.rerender()});wrap.append(add)}return wrap;}
  rerender(){if(!this.host)return;const root=this.host;root.replaceChildren();const intro=document.createElement('div');intro.className='interactive-learn-editor-exercise-help';intro.innerHTML='<strong>How Code Exercise works</strong><p><b>Run</b> executes student code with practice stdin. <b>Check</b> runs every visible test and protected hidden test independently. Visible tests are shown to learners. Hidden tests and the solution never enter the public lesson response.</p>';root.append(intro);if(!this.readOnly){const templates=document.createElement('div');templates.className='interactive-learn-editor-exercise-templates';const label=document.createElement('strong');label.textContent='Load starter template';templates.append(label);for(const [kind,text] of [['python','Python stdin'],['javascript','JavaScript'],['c','C'],['shell','Shell']] as const){const button=document.createElement('button');button.type='button';button.className='interactive-button interactive-button-secondary';button.textContent=text;button.addEventListener('click',()=>this.applyTemplate(kind));templates.append(button)}root.append(templates)}const title=textInput(String(this.data.title||'Code exercise'),'Exercise title');title.disabled=this.readOnly;title.addEventListener('input',()=>{this.data.title=title.value});const instructions=textArea(String(this.data.instructions||''),'Explain the task clearly.');instructions.disabled=this.readOnly;instructions.addEventListener('input',()=>{this.data.instructions=instructions.value});const runtime=runtimeSelect(String(this.data.projectModeId||'python'),this.adapter);runtime.disabled=this.readOnly;runtime.addEventListener('change',()=>{this.data.projectModeId=runtime.value||'python';const mode=PROJECT_MODE_CATALOG.find(item=>item.id===runtime.value);if(mode)this.data.language=mode.primaryLanguageId});root.append(labelled('Title',title),labelled('Instructions',instructions),labelled('Language / runtime',runtime),this.renderFiles());const advanced=document.createElement('details');advanced.className='interactive-learn-editor-exercise-advanced';const advancedSummary=document.createElement('summary');advancedSummary.textContent='Tests, hints & protected solution';advanced.append(advancedSummary);root.append(advanced);const exampleStdin=textArea(String(this.data.exampleStdin||''),'Practice stdin used by the learner Run button');exampleStdin.disabled=this.readOnly;exampleStdin.addEventListener('input',()=>{this.data.exampleStdin=exampleStdin.value});advanced.append(labelled('Example stdin',exampleStdin),this.renderTests('visibleTests','Visible tests','Learners can inspect these test inputs and expected outputs.'),this.renderTests('hiddenTests','Protected hidden tests','Private verification — not sent to readers.'));const hints=textArea(Array.isArray(this.data.hints)?this.data.hints.map(String).join('\n'):'','One hint per line');hints.disabled=this.readOnly;hints.addEventListener('input',()=>{this.data.hints=hints.value.split('\n').map(value=>value.trim()).filter(Boolean)});advanced.append(labelled('Hints',hints));const solution=this.solution();const solutionPath=solution.entryFile||Object.keys(solution.files||{})[0]||this.starter().entryFile||'main.py';if(!solution.files![solutionPath])solution.files![solutionPath]='';const solutionCode=textArea(String(solution.files![solutionPath]||''),'Protected reference solution');solutionCode.spellcheck=false;solutionCode.disabled=this.readOnly;solutionCode.addEventListener('input',()=>{solution.files![solutionPath]=solutionCode.value});const solutionPathInput=textInput(solutionPath,'main.py');solutionPathInput.disabled=this.readOnly;solutionPathInput.addEventListener('change',()=>{const next=solutionPathInput.value.trim();if(!next||next===solutionPath)return;const value=solution.files![solutionPath]||'';delete solution.files![solutionPath];solution.files![next]=value;solution.entryFile=next;this.rerender()});advanced.append(labelled('Solution filename',solutionPathInput),labelled('Protected solution',solutionCode));const required=document.createElement('input');required.type='checkbox';required.checked=this.data.required!==false;required.disabled=this.readOnly;required.addEventListener('change',()=>{this.data.required=required.checked});const requiredRow=document.createElement('label');requiredRow.className='interactive-learn-editor-tool-check';requiredRow.append(required,document.createTextNode(` ${this.adapter.requiredLabel||'Required activity'}`));root.append(requiredRow);if(!this.readOnly){const actions=document.createElement('div');actions.className='interactive-learn-editor-code-actions';const run=document.createElement('button');run.type='button';run.className='interactive-button interactive-button-secondary';run.textContent='▶ Run starter with example stdin';const output=document.createElement('pre');output.className='interactive-learn-editor-code-output';output.textContent='Use this preview to confirm the starter program launches. Assessment tests remain server-side.';run.addEventListener('click',async()=>{const starter=this.starter();const entry=starter.entryFile||Object.keys(starter.files||{})[0]||'';const code=String(starter.files?.[entry]||'');if(!entry||!code.trim()){output.textContent='Add starter code first.';return}run.disabled=true;run.textContent='Running…';try{const result=await this.adapter.runCodePreview({projectModeId:String(this.data.projectModeId||''),language:String(this.data.language||''),filename:entry,code,stdin:String(this.data.exampleStdin||'')});output.textContent=[result.stdout,result.stderr].filter(Boolean).join('\n')||`Process exited with code ${result.exitCode}`;output.dataset.status=result.exitCode===0?'success':'error'}catch(reason){output.textContent=reason instanceof Error?reason.message:String(reason);output.dataset.status='error'}finally{run.disabled=false;run.textContent='▶ Run starter with example stdin'}});actions.append(run);root.append(actions,output)}}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-editor-code-exercise-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}

class CheckpointTool {
  static get toolbox(){return{title:'Checkpoint',icon:ICONS.check};}
  static get isReadOnlySupported(){return true;}
  data:{question:string;kind:'single'|'multiple';options:string[];correct:number[];explanation:string;required:boolean};readOnly:boolean;host?:HTMLElement;dragIndex?:number;
  constructor({data,readOnly}:ToolArgs){const source=cloneData(data,{question:'',kind:'single',options:['Option 1','Option 2'],correct:[0],explanation:'',required:true});const options=Array.isArray(source.options)?source.options.map(String).slice(0,20):['Option 1','Option 2'];const correct=Array.isArray(source.correct)?source.correct.map(Number).filter(Number.isInteger).filter(index=>index>=0&&index<options.length):[0];this.data={question:String(source.question||''),kind:source.kind==='multiple'?'multiple':'single',options:options.length?options:['Option 1','Option 2'],correct:correct.length?correct:[0],explanation:String(source.explanation||''),required:source.required!==false};this.readOnly=Boolean(readOnly);}
  moveOption(from:number,to:number){if(from===to||from<0||to<0||from>=this.data.options.length||to>=this.data.options.length)return;const [value]=this.data.options.splice(from,1);this.data.options.splice(to,0,value);this.data.correct=this.data.correct.map(index=>index===from?to:from<to&&index>from&&index<=to?index-1:from>to&&index>=to&&index<from?index+1:index).sort((a,b)=>a-b);this.rerender();}
  toggleCorrect(index:number,checked:boolean){if(this.data.kind==='single'){this.data.correct=checked?[index]:this.data.correct.filter(value=>value!==index);return}const next=new Set(this.data.correct);if(checked)next.add(index);else next.delete(index);this.data.correct=[...next].sort((a,b)=>a-b);}
  rerender(){if(!this.host)return;this.host.replaceChildren();const question=textArea(this.data.question,'Which command lists files?');question.classList.add('interactive-learn-checkpoint-question');question.disabled=this.readOnly;question.addEventListener('input',()=>{this.data.question=question.value});this.host.append(labelled('Question',question));const kind=document.createElement('div');kind.className='interactive-learn-checkpoint-kind';for(const [value,label] of [['single','Single choice'],['multiple','Multiple choice']] as const){const button=document.createElement('button');button.type='button';button.className=this.data.kind===value?'is-active':'';button.textContent=label;button.disabled=this.readOnly;button.addEventListener('click',()=>{this.data.kind=value;if(value==='single'&&this.data.correct.length>1)this.data.correct=[this.data.correct[0]??0];this.rerender()});kind.append(button)}this.host.append(kind);const list=document.createElement('div');list.className='interactive-learn-checkpoint-options';this.data.options.forEach((value,index)=>{const row=document.createElement('div');row.className='interactive-learn-checkpoint-option';row.draggable=!this.readOnly;row.addEventListener('dragstart',()=>{this.dragIndex=index;row.classList.add('is-dragging')});row.addEventListener('dragend',()=>{this.dragIndex=undefined;row.classList.remove('is-dragging')});row.addEventListener('dragover',(event)=>{if(!this.readOnly)event.preventDefault()});row.addEventListener('drop',(event)=>{event.preventDefault();if(this.dragIndex!==undefined)this.moveOption(this.dragIndex,index)});const handle=document.createElement('span');handle.className='interactive-learn-checkpoint-drag';handle.textContent='⋮⋮';handle.title='Drag to reorder';const correct=document.createElement('input');correct.type=this.data.kind==='single'?'radio':'checkbox';correct.name=this.data.kind==='single'?'checkpoint-correct':'';correct.checked=this.data.correct.includes(index);correct.disabled=this.readOnly;correct.setAttribute('aria-label',`Mark option ${index+1} correct`);correct.addEventListener('change',()=>{this.toggleCorrect(index,correct.checked);if(this.data.kind==='single')this.rerender()});const input=textInput(value,`Option ${index+1}`);input.disabled=this.readOnly;input.addEventListener('input',()=>{this.data.options[index]=input.value});row.append(handle,correct,input);if(!this.readOnly){const remove=document.createElement('button');remove.type='button';remove.textContent='×';remove.title='Remove option';remove.disabled=this.data.options.length<=2;remove.addEventListener('click',()=>{this.data.options.splice(index,1);this.data.correct=this.data.correct.filter(value=>value!==index).map(value=>value>index?value-1:value);if(!this.data.correct.length)this.data.correct=[0];this.rerender()});row.append(remove)}list.append(row)});this.host.append(list);if(!this.readOnly){const add=document.createElement('button');add.type='button';add.className='interactive-button interactive-button-secondary';add.textContent='+ Add option';add.disabled=this.data.options.length>=20;add.addEventListener('click',()=>{this.data.options.push(`Option ${this.data.options.length+1}`);this.rerender()});this.host.append(add)}const advanced=document.createElement('details');advanced.className='interactive-learn-checkpoint-advanced';const summary=document.createElement('summary');summary.textContent='Checkpoint settings';advanced.append(summary);const explanation=textArea(this.data.explanation,'Explanation shown after answering');explanation.disabled=this.readOnly;explanation.addEventListener('input',()=>{this.data.explanation=explanation.value});advanced.append(labelled('Explanation',explanation));const required=document.createElement('input');required.type='checkbox';required.checked=this.data.required;required.disabled=this.readOnly;required.addEventListener('change',()=>{this.data.required=required.checked});const requiredRow=document.createElement('label');requiredRow.className='interactive-learn-editor-tool-check';requiredRow.append(required,document.createTextNode(' Required for completion'));advanced.append(requiredRow);this.host.append(advanced);}
  render(){const root=document.createElement('div');root.className='interactive-learn-editor-tool interactive-learn-checkpoint-tool';this.host=root;this.rerender();return root;}
  save(){return this.data;}
}

const baseEditorTools:Record<string,LearnToolClass>={
  header:DirectHeadingTool as unknown as LearnToolClass,
  list:StructuredListTool as unknown as LearnToolClass,
  checklist:ChecklistTool as unknown as LearnToolClass,
  quote:createTool({title:'Quote',icon:ICONS.quote,defaults:{text:'',caption:''},fields:[{key:'text',label:'Quote',kind:'rich'},{key:'caption',label:'Caption',kind:'text'}]}),
  delimiter:createTool({title:'Divider',icon:ICONS.divider,fields:[]}),
  table:VisualTableTool as unknown as LearnToolClass,
  callout:createTool({title:'Callout',icon:ICONS.callout,defaults:{tone:'info',title:'',text:'',required:false},fields:[{key:'tone',label:'Tone',kind:'select',options:[{value:'info',label:'Info'},{value:'tip',label:'Tip'},{value:'warning',label:'Warning'},{value:'danger',label:'Danger'},{value:'success',label:'Success'}]},{key:'title',label:'Title',kind:'text'},{key:'text',label:'Message',kind:'rich'}]}),
  image:ImageTool as unknown as LearnToolClass,
  gallery:GalleryTool as unknown as LearnToolClass,
  video:VideoTool as unknown as LearnToolClass,
  audio:AudioTool as unknown as LearnToolClass,
  file:FileTool as unknown as LearnToolClass,
  code:RunnableCodeTool as unknown as LearnToolClass,
  terminal:TerminalTool as unknown as LearnToolClass,
  terminalTask:TerminalTaskTool as unknown as LearnToolClass,
  codeExercise:CodeExerciseTool as unknown as LearnToolClass,
  checkpoint:CheckpointTool as unknown as LearnToolClass,
  solution:createTool({title:'Solution',icon:ICONS.callout,defaults:{title:'Solution',content:'',releasePolicy:'always',attemptsRequired:1,instructorReleased:false},fields:[{key:'title',label:'Title',kind:'text'},{key:'content',label:'Protected solution',kind:'textarea'},{key:'releasePolicy',label:'Reveal policy',kind:'select',options:[{value:'always',label:'Always available after sign-in'},{value:'after-first-attempt',label:'After first assessment attempt'},{value:'after-failed-attempts',label:'After failed attempts'},{value:'after-assessment-pass',label:'After assessment pass'},{value:'after-lesson-completion',label:'After lesson completion'},{value:'instructor-release',label:'Instructor release only'},{value:'never',label:'Never reveal'}]},{key:'attemptsRequired',label:'Failed attempts required',kind:'number'},{key:'instructorReleased',label:'Instructor released',kind:'checkbox'}]}),
  tabs:createVisualSectionTool('tabs'),
  accordion:createVisualSectionTool('accordion'),
  legacyImport:LegacyImportTool as unknown as LearnToolClass,
};

const cmsSolutionTool=createTool({title:'Solution',icon:ICONS.callout,defaults:{title:'Solution',content:'',releasePolicy:'always',attemptsRequired:1},fields:[{key:'title',label:'Title',kind:'text'},{key:'content',label:'Protected solution',kind:'textarea'},{key:'releasePolicy',label:'Reveal policy',kind:'select',options:[{value:'always',label:'Always available after sign-in'},{value:'after-first-attempt',label:'After first assessment attempt'},{value:'after-failed-attempts',label:'After failed attempts'},{value:'after-assessment-pass',label:'After assessment pass'},{value:'never',label:'Never reveal'}]},{key:'attemptsRequired',label:'Failed attempts required',kind:'number'}]});
const ADAPTER_TOOL_NAMES=new Set(['image','gallery','video','audio','file','code','terminal','terminalTask','codeExercise']);
export function createInteractiveContentTools(adapter:InteractiveContentEditorAdapter,profile:InteractiveContentCapabilityProfile=interactiveContentProfileForContext(adapter.context)):Record<string,EditorToolEntry>{
  return Object.fromEntries(Object.entries(baseEditorTools).filter(([name])=>profile.allowedBlocks.has(name as InteractiveContentBlockType)).map(([name,tool])=>{
    if(name==='solution'&&adapter.context!=='learn')return[name,cmsSolutionTool];
    return[name,ADAPTER_TOOL_NAMES.has(name)?{class:tool,config:adapter}:tool];
  }));
}
export const learnEditorTools:Record<string,EditorToolEntry>=createInteractiveContentTools(learnEditorAdapter);

function editorSafeArray(value:unknown):unknown[]{return Array.isArray(value)?value:[];}
function normalizeEditorBlockData(type:string,value:unknown):Record<string,unknown>{
  const data=(value&&typeof value==='object'&&!Array.isArray(value)?structuredClone(value):{}) as Record<string,unknown>;
  if(type==='list')data.items=editorSafeArray(data.items);
  if(type==='checklist')data.items=editorSafeArray(data.items);
  if(type==='gallery'){data.mediaIds=editorSafeArray(data.mediaIds);data.mediaUrls=editorSafeArray(data.mediaUrls);data.alts=editorSafeArray(data.alts);}
  if(type==='table')data.content=editorSafeArray(data.content).map((row)=>Array.isArray(row)?row:[]);
  if(type==='checkpoint'){data.options=editorSafeArray(data.options);data.correct=editorSafeArray(data.correct);}
  if(type==='codeExercise'){data.visibleTests=editorSafeArray(data.visibleTests);data.hiddenTests=editorSafeArray(data.hiddenTests);data.hints=editorSafeArray(data.hints);}
  if(type==='tabs'||type==='accordion'){
    const key=type==='tabs'?'tabs':'items';data[key]=editorSafeArray(data[key]).map((raw,index)=>{const section=(raw&&typeof raw==='object'&&!Array.isArray(raw)?raw:{}) as Record<string,unknown>;return{...section,id:String(section.id||`${type}-${index+1}`),title:String(section.title||`${type==='tabs'?'Tab':'Section'} ${index+1}`),blocks:editorSafeArray(section.blocks).map((block)=>{const row=(block&&typeof block==='object'&&!Array.isArray(block)?block:{}) as Record<string,unknown>;const nestedType=String(row.type||'paragraph');return{...row,type:nestedType,data:normalizeEditorBlockData(nestedType,row.data)};})};});
  }
  return data;
}
export function editorBlocksFromDocument(blocks:InteractiveContentBlock[]){return (Array.isArray(blocks)?blocks:[]).map(block=>({id:block.id,type:block.type,data:normalizeEditorBlockData(block.type,block.data)}));}
