/**
 * 文本渲染引擎（Text Renderer）
 *
 * 按照文档描述的 10 步管线，将 LLM 原始文本转换为可直接 dangerouslySetInnerHTML 渲染的 HTML 字符串。
 * 这是一个纯函数模块，不依赖任何 React 状态，便于测试和复用。
 *
 * 渲染管线（与 ST text-decorator.js 对齐）：
 *  1. 正则脚本处理（由调用方在外部完成，传入 processedText）
 *  2. 变量替换 {{user}} / {{char}} / ...
 *  3. 清理控制标签 <phase>, <maintext>, <!-- ... -->
 *  4. 规范行间空白
 *  5. 抽取 fenced code blocks，用占位符替换暂存
 *  6. HTML 转义（防 XSS）
 *  7. 引号美化
 *  8. 动作文本美化 *动作*
 *  9. \n → <br>
 * 10. 恢复代码块（html 块 → html-code-block / 其余 → pre>code）
 */

// ============= 类型定义 =============

export interface RenderOptions {
  /** 消息在对话中的索引（用于正则 depth 过滤等，暂留扩展） */
  index?: number
  /** 当前角色名称（用于变量替换 {{char}}） */
  characterName?: string
  /** 当前用户名称（用于变量替换 {{user}}） */
  userName?: string
  /** 额外的自定义变量（key → value） */
  customVars?: Record<string, string>
}

/** 解析后的消息结构（在渲染前从原始内容中提取） */
export interface ParsedMessage {
  /** 提取 <maintext>...</maintext> 后的正文（如不存在则为空字符串） */
  maintext: string
  /** 提取 <Status_block>...</Status_block> 后的 YAML 字符串（如不存在则为空字符串） */
  statusYaml: string
  /** 提取旧版 <status_current_variable>...</status_current_variable> 的 JSON 对象（兼容） */
  statusJson: Record<string, unknown> | null
  /** 解析出的选择项列表 */
  choices: Choice[]
  /** 去掉所有特殊块后、待渲染的主体文本 */
  bodyText: string
  /** 主体文本是否含有 <script> 标签 */
  hasScript: boolean
  /** 主体文本是否含有 HTML 标签或代码块 */
  hasHtml: boolean
}

export interface Choice {
  label: string
  text: string
}

// ============= 解析函数（提取特殊块） =============

/** 从原始内容中一次性解析出所有特殊区域 */
export function parseRawMessage(raw: string): ParsedMessage {
  let content = raw

  // 1. 提取 <maintext>
  let maintext = ''
  content = content.replace(/<maintext>([\s\S]*?)<\/maintext>/i, (_, inner) => {
    maintext = inner.trim()
    return ''
  })

  // 2. 提取 <Status_block> (YAML)
  let statusYaml = ''
  content = content.replace(/<Status_block>\s*([\s\S]*?)\s*<\/Status_block>/i, (_, inner) => {
    statusYaml = inner.trim()
    return ''
  })

  // 3. 提取 <status_current_variable> (JSON，兼容旧版)
  let statusJson: Record<string, unknown> | null = null
  content = content.replace(/<status_current_variable>([\s\S]*?)<\/status_current_variable>/i, (_, inner) => {
    try {
      statusJson = JSON.parse(inner.trim())
    } catch {
      // ignore malformed JSON
    }
    return ''
  })

  // 4. 提取 choices
  const { choices, cleanContent } = extractChoices(content)
  content = cleanContent

  // 5. 检测 script / html（在原始内容上检测，而非剥离特殊块后的 bodyText）
  const hasScript = /<script[\s\S]*?<\/script>/i.test(raw)
  const hasHtml = /<[a-zA-Z][^>]*>/.test(raw) || /```[\s\S]*?```/.test(raw)

  return {
    maintext,
    statusYaml,
    statusJson,
    choices,
    bodyText: content.trim(),
    hasScript,
    hasHtml,
  }
}

/** 提取 <choice>...</choice> 或 [choice]...[/choice] 选择项 */
function extractChoices(content: string): { choices: Choice[]; cleanContent: string } {
  const tagRegex = /(?:<choice>|\[choice\])([\s\S]*?)(?:<\/choice>|\[\/choice\])/i
  const tagMatch = content.match(tagRegex)

  if (tagMatch) {
    const block = tagMatch[1]
    const choices = parseChoiceBlock(block)
    const cleanContent = content.replace(tagRegex, '').trim()
    return { choices, cleanContent }
  }

  // 尝试纯文本格式 "A. xxx\nB. xxx"（至少 2 项才认为是选择列表）
  const textChoiceRegex = /^([A-E])[.、:：]\s*(.+?)(?=\n[A-E][.、:：]|\n*$)/gm
  const choices: Choice[] = []
  let m: RegExpExecArray | null
  while ((m = textChoiceRegex.exec(content)) !== null) {
    choices.push({ label: m[1], text: m[2].trim() })
  }
  if (choices.length >= 2) {
    const cleanContent = content.replace(textChoiceRegex, '').trim()
    return { choices, cleanContent }
  }

  return { choices: [], cleanContent: content }
}

function parseChoiceBlock(block: string): Choice[] {
  const choices: Choice[] = []
  const optionRegex = /^([A-Z])[.、:：]\s*(.+)$/gm
  let m: RegExpExecArray | null
  while ((m = optionRegex.exec(block)) !== null) {
    choices.push({ label: m[1], text: m[2].trim() })
  }
  return choices
}

// ============= 文本渲染管线 =============

/** renderMessageHtml 的返回值 */
export interface RenderResult {
  /** 可安全用于 dangerouslySetInnerHTML 的 HTML 字符串（html-code-block 已被移除） */
  html: string
  /** 从 fenced code blocks 中提取的 HTML 片段列表（供 StatusBarIframe 渲染） */
  htmlBlocks: string[]
}

/**
 * 将原始消息正文（bodyText）渲染为 HTML 字符串。
 * 调用前应先通过 parseRawMessage() 拿到 bodyText。
 *
 * @param rawText  待渲染的原始文本（bodyText，非完整原始消息）
 * @param role     消息角色
 * @param options  渲染选项
 * @returns RenderResult — html 字符串 + htmlBlocks 列表
 */
export function renderMessageHtml(
  rawText: string,
  role: 'user' | 'assistant',
  options: RenderOptions = {}
): RenderResult {
  // 用户消息：不做 HTML 渲染，只做基本转义 + 换行
  if (role === 'user') {
    return { html: escapeHtml(rawText).replace(/\n/g, '<br>'), htmlBlocks: [] }
  }

  let text = rawText

  // 早期检测：bodyText 本身就是一份完整 HTML 文档（AI 直接返回裸 HTML，未包在代码围栏中）
  // 直接路由到 htmlBlocks，不走普通文本渲染管线
  if (/^\s*<!DOCTYPE\s/i.test(text) || /^\s*<html[\s>]/i.test(text)) {
    return { html: '', htmlBlocks: [text] }
  }

  // Step 2: 变量替换
  text = substituteVariables(text, options)

  // Step 5（提前）: 先抽取 fenced code blocks，用占位符替换
  // 必须在 cleanControlTags 之前执行，防止 HTML 注释清理破坏代码块内容
  const { text: textWithPlaceholders, blocks } = extractCodeBlocks(text)
  text = textWithPlaceholders

  // Step 5b: 检测并提取内联 HTML 块（含 <style> 或 <script> 的富 HTML）
  // 在代码块已抽取之后、转义之前执行，避免 HTML 被 escapeHtml 破坏
  const { text: textAfterInlineExtract, htmlBlocks: inlineHtmlBlocks } = extractInlineHtmlBlocks(text)
  text = textAfterInlineExtract

  // Step 3: 清理控制性标签/区域（仅作用于代码块以外的文本）
  text = cleanControlTags(text)

  // Step 4: 规范行间空白
  text = normalizeWhitespace(text)

  // Step 6: HTML 转义主体文本（代码块已抽出，不会被误转义）
  text = escapeHtml(text)

  // Step 7: 引号美化
  text = beautifyQuotes(text)

  // Step 8: 动作文本美化 *动作*
  text = beautifyActions(text)

  // Step 9: 换行 → <br>
  text = text.replace(/\n/g, '<br>')

  // Step 10: 恢复代码块（html 块替换为空占位，收集到 htmlBlocks）
  const { html, htmlBlocks: codeBlockHtmlBlocks } = restoreCodeBlocks(text, blocks)

  return { html, htmlBlocks: [...inlineHtmlBlocks, ...codeBlockHtmlBlocks] }
}

// ============= 各步骤实现 =============

/** Step 2: 变量替换 */
function substituteVariables(text: string, options: RenderOptions): string {
  const vars: Record<string, string> = {
    user: options.userName ?? '',
    char: options.characterName ?? '',
    ...options.customVars,
  }

  // 替换用户自定义变量
  for (const [key, value] of Object.entries(vars)) {
    text = text.replace(new RegExp(`\\{\\{${escapeRegexStr(key)}\\}\\}`, 'gi'), value)
  }

  // 时间变量
  const now = new Date()
  text = text.replace(/\{\{time\}\}/gi, now.toLocaleTimeString('en-US', { hour12: false }))
  text = text.replace(/\{\{time_12h\}\}/gi, now.toLocaleTimeString('en-US', { hour12: true }))
  text = text.replace(/\{\{date\}\}/gi, now.toLocaleDateString('en-CA'))
  text = text.replace(/\{\{datetime\}\}/gi, now.toLocaleString())

  // 随机数
  text = text.replace(/\{\{random:(\d+)-(\d+)\}\}/gi, (_, min, max) =>
    String(Math.floor(Math.random() * (Number(max) - Number(min) + 1) + Number(min)))
  )
  text = text.replace(/\{\{random\}\}/gi, () => String(Math.floor(Math.random() * 100)))

  // pick
  text = text.replace(/\{\{pick:([^}]+)\}\}/gi, (_, options) => {
    const list = options.split('|')
    return list[Math.floor(Math.random() * list.length)]
  })

  // 特殊字符
  text = text.replace(/\{\{newline\}\}/gi, '\n')

  return text
}

/** Step 3: 清理控制性标签 */
function cleanControlTags(text: string): string {
  // <phase ...> / </phase>
  text = text.replace(/<phase[^>]*>/gi, '')
  text = text.replace(/<\/phase>/gi, '')

  // <!-- consider: ... --> 和普通 HTML 注释
  text = text.replace(/<!--[\s\S]*?-->/g, '')

  return text
}

/** Step 4: 规范行间空白 */
function normalizeWhitespace(text: string): string {
  // 统一换行符
  text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
  // 去除每行尾部空白
  text = text.replace(/[ \t]+\n/g, '\n')
  // 压缩连续 ≥3 个空行为 2 个
  text = text.replace(/\n{3,}/g, '\n\n')
  return text.trim()
}

// ---- 内联 HTML 块提取 ----

/**
 * 检测并提取内联富 HTML 内容（含 <style>...</style> 或 <script>...</script> 的 HTML）。
 * 触发条件：剩余文本（代码块占位符以外的部分）包含完整的 <style>...</style> 或 <script>...</script> 块。
 * 提取逻辑：从最早的 HTML 文档标签或块级标签开始，到文本末尾，整段作为一个 HTML block 交给 iframe 渲染；
 *           该标签之前的文本保留为普通叙事文本继续走渲染管线。
 */
function extractInlineHtmlBlocks(text: string): { text: string; htmlBlocks: string[] } {
  // 仅在包含完整 <style>...</style> 或 <script>...</script> 时触发
  const hasStyle = /<style[\s>][\s\S]*?<\/style>/i.test(text)
  const hasScript = /<script[\s>][\s\S]*?<\/script>/i.test(text)
  if (!hasStyle && !hasScript) {
    return { text, htmlBlocks: [] }
  }

  // 找到最早的 HTML 文档起始点：
  // 优先匹配 <!DOCTYPE 或 <html（完整文档），其次匹配 <head>/<body> 等文档结构标签，
  // 最后匹配块级 HTML 标签（<div>, <style>, <script> 等）
  const candidates: number[] = []

  const docTypeMatch = text.match(/<!DOCTYPE\s/i)
  if (docTypeMatch?.index !== undefined) candidates.push(docTypeMatch.index)

  const htmlTagMatch = text.match(/<html[\s>]/i)
  if (htmlTagMatch?.index !== undefined) candidates.push(htmlTagMatch.index)

  const headMatch = text.match(/<head[\s>]/i)
  if (headMatch?.index !== undefined) candidates.push(headMatch.index)

  const bodyMatch = text.match(/<body[\s>]/i)
  if (bodyMatch?.index !== undefined) candidates.push(bodyMatch.index)

  const blockTagPattern = /<(?:div|section|article|header|footer|nav|main|aside|style|script|table|ul|ol|dl|form|fieldset|details|figure|blockquote)[\s>]/i
  const blockMatch = text.match(blockTagPattern)
  if (blockMatch?.index !== undefined) candidates.push(blockMatch.index)

  if (candidates.length === 0) {
    return { text, htmlBlocks: [] }
  }

  // 从最早的匹配点开始截取
  const splitIndex = Math.min(...candidates)
  const narrativeText = text.slice(0, splitIndex)
  const htmlBlock = text.slice(splitIndex)

  return {
    text: narrativeText,
    htmlBlocks: [htmlBlock],
  }
}

// ---- 代码块处理 ----

interface CodeBlock {
  lang: string
  code: string
  isHtml: boolean
}

const PLACEHOLDER_PREFIX = '\x00CODEBLOCK\x00'

/**
 * Step 5: 提取 fenced code blocks，返回带占位符的文本和代码块字典
 * 支持：```html, ```text, ``` (自动检测是否是 HTML)
 */
function extractCodeBlocks(text: string): { text: string; blocks: Map<string, CodeBlock> } {
  const blocks = new Map<string, CodeBlock>()
  let idx = 0

  const replaced = text.replace(/```([^\n`]*)\n?([\s\S]*?)```/g, (_, lang: string, code: string) => {
    const trimmedLang = lang.trim().toLowerCase()
    const trimmedCode = code.trim()
    const key = `${PLACEHOLDER_PREFIX}${idx++}${PLACEHOLDER_PREFIX}`

    // 判断是否是 HTML：
    // 1. 明确声明 html/text 语言
    // 2. 内容含有开标签或闭标签（<\/?[a-zA-Z] 同时匹配 <div 和 </div>）
    // 3. 内容是完整 HTML 文档（DOCTYPE/html 开头）
    const isHtml =
      trimmedLang === 'html' ||
      trimmedLang === 'text' ||
      /^\s*<!DOCTYPE\s/i.test(trimmedCode) ||
      /^\s*<html[\s>]/i.test(trimmedCode) ||
      (trimmedLang === '' && /<\/?[a-zA-Z][^>]*>/.test(trimmedCode))

    blocks.set(key, { lang: trimmedLang, code: trimmedCode, isHtml })
    return key
  })

  return { text: replaced, blocks }
}

/** Step 10: 恢复代码块，html 块直接收集到 htmlBlocks 列表（不插入主 HTML） */
function restoreCodeBlocks(
  text: string,
  blocks: Map<string, CodeBlock>
): { html: string; htmlBlocks: string[] } {
  const htmlBlocks: string[] = []

  for (const [key, block] of blocks) {
    if (block.isHtml) {
      // HTML 块：收集到 htmlBlocks，主文本中用空字符串替换（不污染主渲染区）
      htmlBlocks.push(block.code)
      text = text.split(key).join('')
    } else {
      // 其他语言：普通代码块，插入主 HTML
      const escapedCode = escapeHtml(block.code)
      const langLabel = block.lang
        ? `<span class="code-lang-label">${escapeHtml(block.lang)}</span>`
        : ''
      const html = `<pre class="code-block">${langLabel}<code class="language-${escapeHtml(block.lang) || 'text'}">${escapedCode}</code></pre>`
      text = text.split(key).join(html)
    }
  }

  return { html: text, htmlBlocks }
}

// ---- 美化函数 ----

/** Step 7: 引号美化（英文/中文引号） */
function beautifyQuotes(text: string): string {
  // 不在 HTML 标签内的引号对才做高亮
  // 用正则匹配：中文书名号「」『』 / 英文 "" / 直双引号 ""
  return text.replace(
    /([""「『])((?:(?!["」』]).)*?)([""」』])/g,
    (_, open, content, close) => {
      return (
        `<span class="quote-open">${open}</span>` +
        `<span class="quote-content">${content}</span>` +
        `<span class="quote-close">${close}</span>`
      )
    }
  )
}

/** Step 8: 动作文本美化 *动作* → <span class="action-text">*动作*</span> */
function beautifyActions(text: string): string {
  // 匹配 *内容*，但不跨越换行（已转成 <br> 之前，\n 还在）
  return text.replace(/\*([^\n*]+)\*/g, (_, inner) => {
    return `<span class="action-text">*${inner}*</span>`
  })
}

// ============= 工具函数（公开导出） =============

/** HTML 转义（防 XSS） */
export function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

/** 正则元字符转义 */
function escapeRegexStr(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
