import type { InteractiveContentBlock, InteractiveContentBlockType, InteractiveContentDocument } from './types';
import type { InteractiveContentCapabilityProfile } from './profiles';

export const CURRENT_INTERACTIVE_CONTENT_SCHEMA_VERSION = 1 as const;
const PARAGRAPH='paragraph';
const CALLOUT_TONES=new Set(['info','tip','warning','danger','success']);
const ALIGNMENTS=new Set(['left','center','right']);
const VIDEO_SOURCES=new Set(['upload','youtube','wistia','direct']);
const NESTED_BLOCK_TYPES=new Set(['paragraph','header','list','quote','callout','code','image','file']);
const MAX_NESTED_DEPTH=4;

function record(value:unknown):Record<string,unknown>{return value&&typeof value==='object'&&!Array.isArray(value)?value as Record<string,unknown>:{};}
function array(value:unknown):unknown[]{return Array.isArray(value)?value:[];}
function text(value:unknown,max=100_000):string{return String(value??'').slice(0,max);}
function clean(value:unknown,max=1000):string{return String(value??'').trim().slice(0,max);}
function bool(value:unknown,fallback=false):boolean{return value===undefined?fallback:Boolean(value);}
function integer(value:unknown,min:number,max:number,fallback:number):number{const next=Number(value);return Number.isInteger(next)&&next>=min&&next<=max?next:fallback;}
function newBlockId(type:string,index:number):string{return `${type}-${globalThis.crypto?.randomUUID?.()||`${Date.now()}-${index}-${Math.random().toString(36).slice(2,8)}`}`;}
function safeId(value:unknown,fallback:string):string{return clean(value,120).replace(/[^A-Za-z0-9_-]/gu,'').slice(0,120)||fallback;}
function safeMediaReference(value:unknown):string{const raw=clean(value,2000);if(!raw)return'';if(raw.startsWith('project-asset:'))return/^project-asset:assets\/[A-Za-z0-9._/-]+$/u.test(raw)?raw:'';if(/^https:\/\//u.test(raw))return raw;if(/^\/learnitive-media\/(?:blog|docs)\/[A-Za-z0-9._/-]+$/u.test(raw))return raw;return'';}
function safeHttps(value:unknown):string{const raw=clean(value,2000);return /^https:\/\//u.test(raw)?raw:'';}

function sourceSchemaVersion(source:Record<string,unknown>):number{
  if(source.schemaVersion===undefined||source.schemaVersion===null||source.schemaVersion==='')return 1;
  const version=Number(source.schemaVersion);
  if(!Number.isInteger(version)||version<1)throw new Error('Interactive Content document has an invalid schema version.');
  if(version>CURRENT_INTERACTIVE_CONTENT_SCHEMA_VERSION)throw new Error(`This document uses Interactive Content schema ${version}, but this version of Learnitive supports up to schema ${CURRENT_INTERACTIVE_CONTENT_SCHEMA_VERSION}. Update Learnitive before editing this document.`);
  return version;
}

function migrateDocument(source:Record<string,unknown>):Record<string,unknown>{
  const version=sourceSchemaVersion(source);
  // Migration pipeline intentionally exists even while v1 is current so a future
  // client cannot silently coerce a newer document back to schema 1.
  let migrated=structuredClone(source);
  let current=version;
  while(current<CURRENT_INTERACTIVE_CONTENT_SCHEMA_VERSION){
    // Future migrations are applied one version at a time here.
    current+=1;
    migrated={...migrated,schemaVersion:current};
  }
  return migrated;
}

function normalizeListItems(value:unknown,depth=0):Array<{content:string;items:Array<{content:string;items:unknown[]}>}>{
  if(depth>8)return[];
  return array(value).slice(0,200).map((raw)=>{
    if(typeof raw==='string')return{content:text(raw,10_000),items:[]};
    const item=record(raw);
    return{content:text(item.content??item.text,10_000),items:normalizeListItems(item.items,depth+1)};
  });
}

function normalizeChecklistItems(value:unknown):Array<{text:string;checked:boolean}>{
  return array(value).slice(0,500).map((raw)=>{const item=record(raw);return{text:text(item.text??item.content,10_000),checked:Boolean(item.checked)};});
}

function normalizeTableRows(value:unknown):string[][]{
  const rows=array(value).slice(0,500);
  return rows.map((raw)=>array(raw).slice(0,100).map((cell)=>text(cell,20_000)));
}

function normalizeNestedBlocks(value:unknown,profile:InteractiveContentCapabilityProfile,depth=0):InteractiveContentBlock[]{
  if(depth>=MAX_NESTED_DEPTH)return[];
  const ids=new Set<string>();
  const blocks:InteractiveContentBlock[]=[];
  array(value).slice(0,100).forEach((raw,index)=>{
    const source=record(raw);const type=String(source.type||PARAGRAPH);
    if(!NESTED_BLOCK_TYPES.has(type))return;
    if(type!==PARAGRAPH&&!profile.allowedBlocks.has(type as InteractiveContentBlockType))return;
    let id=safeId(source.id,`nested-${index+1}`);if(ids.has(id))id=`${id}-${index+1}`;ids.add(id);
    blocks.push({id,type,data:normalizeBlockData(type,record(source.data),profile,depth+1)});
  });
  return blocks;
}

function normalizeSections(value:unknown,key:'tabs'|'items',profile:InteractiveContentCapabilityProfile,depth:number):Record<string,unknown>[] {
  const limit=key==='tabs'?20:50;
  const noun=key==='tabs'?'Tab':'Section';
  return array(value).slice(0,limit).map((raw,index)=>{
    const item=record(raw);
    const legacy=text(item.content,50_000);
    const blocks=normalizeNestedBlocks(item.blocks,profile,depth+1);
    return{
      id:safeId(item.id,`${key==='tabs'?'tab':'section'}-${index+1}`),
      title:clean(item.title||`${noun} ${index+1}`,500),
      blocks:blocks.length?blocks:(legacy?[{id:`legacy-${index+1}`,type:'paragraph',data:{text:legacy}}]:[]),
    };
  });
}

function normalizeBlockData(type:string,data:Record<string,unknown>,profile:InteractiveContentCapabilityProfile,depth=0):Record<string,unknown>{
  switch(type){
    case'paragraph':return{text:text(data.text)};
    case'header':return{text:text(data.text,20_000),level:integer(data.level,2,4,2)};
    case'list':return{style:String(data.style)==='ordered'?'ordered':'unordered',items:normalizeListItems(data.items)};
    case'checklist':return{items:normalizeChecklistItems(data.items)};
    case'quote':return{text:text(data.text,50_000),caption:clean(data.caption,500),alignment:ALIGNMENTS.has(String(data.alignment))?String(data.alignment):'left'};
    case'delimiter':return{};
    case'callout':return{tone:CALLOUT_TONES.has(String(data.tone))?String(data.tone):'info',title:clean(data.title,300),text:text(data.text,50_000)};
    case'table':return{content:normalizeTableRows(data.content),withHeadings:bool(data.withHeadings,true),stretched:bool(data.stretched,false)};
    case'image':return{
      mediaId:integer(data.mediaId,0,Number.MAX_SAFE_INTEGER,0),mediaUrl:safeMediaReference(data.mediaUrl||data.assetPath||data.url),
      widthPercent:integer(data.widthPercent,25,100,100),alignment:ALIGNMENTS.has(String(data.alignment))?String(data.alignment):'center',caption:clean(data.caption,1000),alt:clean(data.alt,500),
    };
    case'gallery':return{
      mediaIds:array(data.mediaIds).slice(0,24).map((item)=>integer(item,1,Number.MAX_SAFE_INTEGER,0)).filter(Boolean),
      mediaUrls:array(data.mediaUrls||data.assetPaths).slice(0,24).map(safeMediaReference).filter(Boolean),
      alts:array(data.alts).slice(0,24).map((item)=>clean(item,500)),caption:clean(data.caption,1000),
    };
    case'video':{const source=VIDEO_SOURCES.has(String(data.source))?String(data.source):'upload';return{
      source,mediaId:integer(data.mediaId,0,Number.MAX_SAFE_INTEGER,0),mediaUrl:safeMediaReference(data.mediaUrl||data.assetPath),externalId:clean(data.externalId,500),directUrl:safeHttps(data.directUrl),
      posterMediaId:integer(data.posterMediaId,0,Number.MAX_SAFE_INTEGER,0),posterMediaUrl:safeMediaReference(data.posterMediaUrl),captionMediaId:integer(data.captionMediaId,0,Number.MAX_SAFE_INTEGER,0),captionMediaUrl:safeMediaReference(data.captionMediaUrl),
      title:clean(data.title,500),caption:clean(data.caption,2000),credit:clean(data.credit,500),creditUrl:safeHttps(data.creditUrl),requiredWatchPercent:integer(data.requiredWatchPercent,0,100,0),
    };}
    case'audio':return{mediaId:integer(data.mediaId,0,Number.MAX_SAFE_INTEGER,0),mediaUrl:safeMediaReference(data.mediaUrl||data.assetPath),title:clean(data.title,500),requiredListenPercent:integer(data.requiredListenPercent,0,100,0)};
    case'file':return{mediaId:integer(data.mediaId,0,Number.MAX_SAFE_INTEGER,0),mediaUrl:safeMediaReference(data.mediaUrl||data.assetPath),filename:clean(data.filename,500),mimeType:clean(data.mimeType,200),sizeBytes:integer(data.sizeBytes,0,Number.MAX_SAFE_INTEGER,0),label:clean(data.label,500),description:clean(data.description,2000)};
    case'code':return{language:clean(data.language||'text',80),projectModeId:clean(data.projectModeId,160),filename:clean(data.filename,260),code:text(data.code,500_000),stdin:text(data.stdin,1_000_000),allowStdin:bool(data.allowStdin,Boolean(String(data.stdin||''))),lineNumbers:bool(data.lineNumbers,true),runnable:bool(data.runnable,false)};
    case'tabs':return{tabs:normalizeSections(data.tabs,'tabs',profile,depth)};
    case'accordion':return{items:normalizeSections(data.items,'items',profile,depth)};
    // Advanced Learn/CMS blocks stay owned by their server-side normalizers. If
    // this neutral normalizer is used with those profiles, bound arbitrary data
    // structurally without changing the established advanced schema.
    case'terminal':case'terminalTask':case'codeExercise':case'checkpoint':case'solution':case'legacyImport':return normalizeUnknownValue(data) as Record<string,unknown>;
    default:return{};
  }
}

function normalizeUnknownValue(value:unknown,depth=0):unknown{
  if(depth>8)return'[depth limit]';
  if(value===null||typeof value==='boolean'||typeof value==='number')return value;
  if(typeof value==='string')return value.slice(0,500_000);
  if(Array.isArray(value))return value.slice(0,500).map((item)=>normalizeUnknownValue(item,depth+1));
  if(value&&typeof value==='object'){
    const output:Record<string,unknown>={};
    for(const [key,item] of Object.entries(value as Record<string,unknown>).slice(0,500))output[clean(key,160)||'field']=normalizeUnknownValue(item,depth+1);
    return output;
  }
  return String(value??'').slice(0,1000);
}

export function normalizeInteractiveContentDocument(value:unknown,profile:InteractiveContentCapabilityProfile):InteractiveContentDocument{
  const source=migrateDocument(record(value));
  const rawBlocks=Array.isArray(source.blocks)?source.blocks:[];
  if(rawBlocks.length>profile.maxTopLevelBlocks)throw new Error(`Document contains ${rawBlocks.length} blocks; this editor supports up to ${profile.maxTopLevelBlocks}.`);
  const ids=new Set<string>();
  const blocks:InteractiveContentBlock[]=[];
  rawBlocks.forEach((raw,index)=>{
    const item=record(raw);const type=String(item.type||PARAGRAPH);
    if(type!==PARAGRAPH&&!profile.allowedBlocks.has(type as InteractiveContentBlockType))throw new Error(`Unsupported block type: ${type}.`);
    let id=safeId(item.id,newBlockId(type,index));
    if(ids.has(id))id=newBlockId(type,index);ids.add(id);
    blocks.push({id,type,data:normalizeBlockData(type,record(item.data),profile)});
  });
  return{schemaVersion:CURRENT_INTERACTIVE_CONTENT_SCHEMA_VERSION,time:integer(source.time,0,Number.MAX_SAFE_INTEGER,Date.now()),blocks};
}

function stripInlineHtml(value:unknown):string{return String(value??'').replace(/<br\s*\/?\s*>/giu,'\n').replace(/<[^>]*>/gu,'').replace(/&nbsp;/gu,' ').replace(/&amp;/gu,'&').replace(/&lt;/gu,'<').replace(/&gt;/gu,'>').trim();}
function escapeMarkdownText(value:unknown):string{return stripInlineHtml(value).replace(/\\/gu,'\\\\').replace(/([*_\[\]<>])/gu,'\\$1');}
function portableMarkdownUrl(value:unknown):string{const url=String(value??'').trim();return url.startsWith('project-asset:')?url.slice('project-asset:'.length):url;}
function markdownUrl(value:unknown):string{const raw=portableMarkdownUrl(value).replace(/>/gu,'%3E');return raw?`<${raw}>`:'';}
function escapeTableCell(value:unknown):string{return stripInlineHtml(value).replace(/\\/gu,'\\\\').replace(/\|/gu,'\\|').replace(/\r?\n/gu,'<br>');}
function markdownList(items:unknown[],ordered:boolean,depth=0):string[]{const lines:string[]=[];items.forEach((raw,index)=>{const item=typeof raw==='string'?{content:raw,items:[]} : record(raw);const prefix=ordered?`${index+1}. `:'- ';lines.push(`${'  '.repeat(depth)}${prefix}${escapeMarkdownText(item.content??item.text)}`);if(Array.isArray(item.items)&&item.items.length)lines.push(...markdownList(item.items,ordered,depth+1));});return lines;}
function nestedBlocksToMarkdown(value:unknown):string{if(!Array.isArray(value))return'';return value.map((raw)=>blockToMarkdown({id:'nested',type:String(record(raw).type||'paragraph'),data:record(record(raw).data)})).filter(Boolean).join('\n\n');}
function codeFence(code:string):string{const longest=Math.max(0,...[...code.matchAll(/`+/gu)].map((match)=>match[0].length));return'`'.repeat(Math.max(3,longest+1));}
function canonicalVideoUrl(d:Record<string,unknown>):string{const source=String(d.source||'upload');if(source==='youtube'&&clean(d.externalId,500))return`https://www.youtube.com/watch?v=${encodeURIComponent(clean(d.externalId,500))}`;if(source==='wistia'&&clean(d.externalId,500))return`https://home.wistia.com/medias/${encodeURIComponent(clean(d.externalId,500))}`;if(source==='direct')return portableMarkdownUrl(d.directUrl);return portableMarkdownUrl(d.mediaUrl||d.assetPath||d.url);}

export function blockToMarkdown(block:InteractiveContentBlock):string{const d=block.data;switch(block.type){
  case'paragraph':return escapeMarkdownText(d.text);
  case'header':return `${'#'.repeat(Math.min(4,Math.max(2,Number(d.level)||2)))} ${escapeMarkdownText(d.text)}`;
  case'list':return markdownList(Array.isArray(d.items)?d.items:[],String(d.style)==='ordered').join('\n');
  case'checklist':return (Array.isArray(d.items)?d.items:[]).map((raw)=>{const item=record(raw);return `- [${item.checked?'x':' '}] ${escapeMarkdownText(item.text??item.content)}`;}).join('\n');
  case'quote':return stripInlineHtml(d.text).split('\n').map((line)=>`> ${escapeMarkdownText(line)}`).join('\n');
  case'delimiter':return'---';
  case'callout':{const title=escapeMarkdownText(d.title);const body=escapeMarkdownText(d.text);return `> **${title||escapeMarkdownText(String(d.tone||'Note'))}**${body?`\n> ${body.replace(/\n/gu,'\n> ')}`:''}`;}
  case'table':{const rows=(Array.isArray(d.content)?d.content:[]).map((row)=>Array.isArray(row)?row.map(escapeTableCell):[]);if(!rows.length)return'';const cols=Math.max(...rows.map((row)=>row.length),1);const normalized=rows.map((row)=>Array.from({length:cols},(_,index)=>row[index]||''));const withHeadings=d.withHeadings!==false;const head=withHeadings?normalized[0]:Array.from({length:cols},()=> '');const rest=withHeadings?normalized.slice(1):normalized;return[`| ${head.join(' | ')} |`,`| ${head.map(()=> '---').join(' | ')} |`,...rest.map((row)=>`| ${row.join(' | ')} |`)].join('\n');}
  case'image':{const src=markdownUrl(d.mediaUrl||d.assetPath||d.url||'');return src?`![${escapeMarkdownText(d.alt)}](${src})${d.caption?`\n\n_${escapeMarkdownText(d.caption)}_`:''}`:'';}
  case'gallery':{const values=(Array.isArray(d.mediaUrls)?d.mediaUrls:Array.isArray(d.assetPaths)?d.assetPaths:[]);return values.map((src,index)=>`![${escapeMarkdownText((Array.isArray(d.alts)?d.alts[index]:''))}](${markdownUrl(src)})`).join('\n\n');}
  case'video':{const url=canonicalVideoUrl(d);return url?`[${escapeMarkdownText(d.title)||'Video'}](${markdownUrl(url)})`:'';}
  case'audio':case'file':{const src=markdownUrl(d.mediaUrl||d.assetPath||d.url||'');return src?`[${escapeMarkdownText(d.label||d.title||d.filename)||block.type}](${src})`:'';}
  case'code':{const code=String(d.code||'');const fence=codeFence(code);const language=String(d.language||'').replace(/[^A-Za-z0-9_+#.-]/gu,'').slice(0,80);return `${fence}${language}\n${code}\n${fence}`;}
  case'tabs':return (Array.isArray(d.tabs)?d.tabs:[]).map((raw)=>{const tab=record(raw);return `### ${escapeMarkdownText(tab.title)||'Tab'}\n\n${nestedBlocksToMarkdown(tab.blocks)}`;}).join('\n\n');
  case'accordion':return (Array.isArray(d.items)?d.items:[]).map((raw)=>{const item=record(raw);return `### ${escapeMarkdownText(item.title)||'Section'}\n\n${nestedBlocksToMarkdown(item.blocks)}`;}).join('\n\n');
  default:return'';
}}

export function interactiveContentToMarkdown(document:InteractiveContentDocument,title?:string):string{const body=document.blocks.map(blockToMarkdown).filter((value)=>value.trim()).join('\n\n');return `${title?`# ${escapeMarkdownText(title.trim())}\n\n`:''}${body}${body?'\n':''}`;}

export function collectInteractiveContentAssetReferences(document:InteractiveContentDocument):string[]{
  const output=new Set<string>();
  const scan=(value:unknown)=>{if(typeof value==='string'){if(value.startsWith('project-asset:')&&/^project-asset:assets\/[A-Za-z0-9._/-]+$/u.test(value))output.add(value);return;}if(Array.isArray(value)){value.forEach(scan);return;}if(value&&typeof value==='object')Object.values(value as Record<string,unknown>).forEach(scan);};
  scan(document.blocks);return[...output].sort();
}

export function interactiveContentIssues(document:InteractiveContentDocument,profile:InteractiveContentCapabilityProfile,options?:{availableAssets?:ReadonlySet<string>}):string[]{
  const issues:string[]=[];if(document.blocks.length>profile.maxTopLevelBlocks)issues.push(`Document exceeds the ${profile.maxTopLevelBlocks}-block limit.`);
  let previousHeading=1;
  const inspect=(blocks:InteractiveContentBlock[],path='')=>{for(const block of blocks){if(block.type!==PARAGRAPH&&!profile.allowedBlocks.has(block.type as InteractiveContentBlockType)){issues.push(`Unsupported block type: ${block.type}.`);continue;}const d=block.data;const prefix=path?`${path}: `:'';
    if(block.type==='header'){const label=stripInlineHtml(d.text);if(!label)issues.push(`${prefix}A heading is empty.`);const level=Math.min(4,Math.max(2,Number(d.level)||2));if(level>previousHeading+1)issues.push(`${prefix}Heading hierarchy jumps from H${previousHeading} to H${level}.`);previousHeading=level;}
    if(block.type==='image'){const src=String(d.mediaUrl||d.assetPath||d.url||'').trim();if(!src)issues.push(`${prefix}An image has no source.`);if(src&&!clean(d.alt,500))issues.push(`${prefix}An image is missing alt text.`);}
    if(block.type==='gallery'){const urls=array(d.mediaUrls||d.assetPaths);const alts=array(d.alts);urls.forEach((_,index)=>{if(!clean(alts[index],500))issues.push(`${prefix}Gallery image ${index+1} is missing alt text.`);});}
    if(block.type==='video'){const source=String(d.source||'upload');if(source==='youtube'||source==='wistia'){if(!clean(d.externalId,500))issues.push(`${prefix}A video is missing its provider ID.`);}else if(source==='direct'){if(!safeHttps(d.directUrl))issues.push(`${prefix}A direct video is missing a valid HTTPS URL.`);}else if(!String(d.mediaUrl||'').trim())issues.push(`${prefix}An uploaded video has no source.`);}
    if(block.type==='table'&&(!Array.isArray(d.content)||!d.content.length))issues.push(`${prefix}A table has no rows.`);
    if(block.type==='code'){if(!String(d.code||'').trim())issues.push(`${prefix}A code block is empty.`);if(d.runnable===true&&!String(d.projectModeId||d.language||'').trim())issues.push(`${prefix}Runnable code has no runtime/language.`);}
    if(block.type==='tabs'&&Array.isArray(d.tabs)){if(!d.tabs.length)issues.push(`${prefix}A Tabs block is empty.`);d.tabs.forEach((raw,index)=>{const tab=record(raw);if(!String(tab.title||'').trim())issues.push(`${prefix}A tab is missing a title.`);const nested=Array.isArray(tab.blocks)?tab.blocks as InteractiveContentBlock[]:[];if(!nested.length)issues.push(`${prefix}Tab ${index+1} has no content.`);inspect(nested,`Tab ${index+1}`);});}
    if(block.type==='accordion'&&Array.isArray(d.items)){if(!d.items.length)issues.push(`${prefix}An Accordion block is empty.`);d.items.forEach((raw,index)=>{const item=record(raw);if(!String(item.title||'').trim())issues.push(`${prefix}An accordion section is missing a title.`);const nested=Array.isArray(item.blocks)?item.blocks as InteractiveContentBlock[]:[];if(!nested.length)issues.push(`${prefix}Accordion section ${index+1} has no content.`);inspect(nested,`Accordion section ${index+1}`);});}
  }};
  inspect(document.blocks);
  if(options?.availableAssets){for(const reference of collectInteractiveContentAssetReferences(document))if(!options.availableAssets.has(reference))issues.push(`Referenced project asset is missing: ${reference.slice('project-asset:'.length)}.`);}
  return[...new Set(issues)];
}
