/**
 * 文章结构化 PPT 幻灯片模型
 * 预览与导出共用同一套分页逻辑
 */
import MarkdownIt from 'markdown-it'
import type Token from 'markdown-it/lib/token.mjs'
import type { Post } from '../services/api'

export type ContentBlock =
  | { type: 'h1'; text: string }
  | { type: 'h2'; text: string }
  | { type: 'h3'; text: string }
  | { type: 'paragraph'; text: string }
  | { type: 'code'; text: string; lang?: string }
  | { type: 'image'; alt: string; src: string }
  | { type: 'video'; src: string; poster?: string; mimeType?: string }
  | { type: 'list'; ordered: boolean; items: string[] }
  | { type: 'placeholder'; text: string }

export type RenderUnit =
  | { kind: 'single'; block: ContentBlock }
  | { kind: 'image-text'; image: Extract<ContentBlock, { type: 'image' }>; text: Extract<ContentBlock, { type: 'paragraph' }> }

export interface SlideSection {
  title: string
  blocks: ContentBlock[]
}

export interface PptTitleSlide {
  kind: 'title'
  title: string
  date?: string
  categoryName?: string
  userName?: string
  tagNames: string[]
}

export interface PptSectionSlide {
  kind: 'section'
  title: string
  blocks: ContentBlock[]
  continued: boolean
}

export type PptSlide = PptTitleSlide | PptSectionSlide

const PPT_SLIDE_BOTTOM = 5.1
const SECTION_HEADER_HEIGHT = 0.85
const IMAGE_PLACEHOLDER_GLOBAL_RE = /\[图片(?::[^\]]*)?\]/g
const IMAGE_FILENAME_SUFFIX_RE =
  /\s+[\w\u4e00-\u9fa5.-]+\.(jpg|jpeg|png|gif|webp|svg|bmp)\s*$/i

function stripTrailingImageFilenames(text: string): string {
  let result = text
  while (IMAGE_FILENAME_SUFFIX_RE.test(result)) {
    result = result.replace(IMAGE_FILENAME_SUFFIX_RE, '')
  }
  return result
}

let markdownParser: MarkdownIt | null = null

function getMarkdownParser(): MarkdownIt {
  if (!markdownParser) {
    markdownParser = new MarkdownIt({ html: true, linkify: true })
  }
  return markdownParser
}

function renderTokenChildren(children: Token[]): string {
  return children
    .map((child) => {
      if (child.children?.length) return renderTokenChildren(child.children)
      switch (child.type) {
        case 'text':
        case 'code_inline':
          return child.content
        case 'softbreak':
        case 'hardbreak':
          return '\n'
        case 'image':
          return `[图片: ${child.content || child.attrGet('alt') || 'image'}]`
        default:
          return child.content || ''
      }
    })
    .join('')
}

function renderInlineText(inlineToken: Token): string {
  if (!inlineToken.children?.length) return inlineToken.content
  return renderTokenChildren(inlineToken.children)
}

/** 移除 inline 渲染产生的 [图片: ...] 占位片段与段尾图片文件名 */
export function stripImagePlaceholders(text: string): string {
  let result = text.replace(IMAGE_PLACEHOLDER_GLOBAL_RE, '')
  result = stripTrailingImageFilenames(result)
  return result.replace(/\s+/g, ' ').trim()
}

function getImgSrc(el: Element): string {
  return (
    el.getAttribute('src') ||
    el.getAttribute('data-src') ||
    el.getAttribute('data-original') ||
    ''
  )
}

function htmlHeadingType(tag: string): 'h1' | 'h2' | 'h3' | null {
  if (tag === 'H1') return 'h1'
  if (tag === 'H2') return 'h2'
  if (tag === 'H3' || tag === 'H4' || tag === 'H5' || tag === 'H6') return 'h3'
  return null
}

/** 从 HTML 片段提取块（供 html_inline / 段落内 HTML 使用） */
export function parseInlineHtmlFragment(html: string): ContentBlock[] {
  const wrapped = html.trim().startsWith('<') ? html : `<span>${html}</span>`
  return parseHtmlBlock(wrapped)
}

/** 从 inline token 提取图片、视频与剥离占位后的文本 */
export function extractMediaFromInline(inlineToken: Token): {
  blocks: ContentBlock[]
  text: string
} {
  const blocks: ContentBlock[] = []
  if (!inlineToken.children?.length) {
    return { blocks, text: stripImagePlaceholders(inlineToken.content || '') }
  }

  for (const child of inlineToken.children) {
    if (child.type === 'image') {
      const src = child.attrGet('src') || ''
      if (src) {
        blocks.push({
          type: 'image',
          alt: child.content || child.attrGet('alt') || 'image',
          src,
        })
      }
      continue
    }
    if (child.type === 'html_inline' && child.content) {
      parseInlineHtmlFragment(child.content).forEach((block) => pushBlock(blocks, block))
    }
  }

  const text = stripImagePlaceholders(renderInlineText(inlineToken))
  return { blocks, text }
}

function pushInlineContent(blocks: ContentBlock[], inline: Token): void {
  const { blocks: mediaBlocks, text } = extractMediaFromInline(inline)
  mediaBlocks.forEach((block) => pushBlock(blocks, block))
  if (text) pushBlock(blocks, { type: 'paragraph', text })
}

function getHeadingText(tokens: Token[], idx: number): string {
  const inline = tokens[idx + 1]
  return inline?.type === 'inline' ? renderInlineText(inline) : ''
}

function parseListItems(
  tokens: Token[],
  startIdx: number,
  listType: 'bullet_list_close' | 'ordered_list_close',
  blocks: ContentBlock[]
): { items: string[]; endIdx: number } {
  const items: string[] = []
  let i = startIdx + 1

  while (i < tokens.length && tokens[i].type !== listType) {
    if (tokens[i].type === 'list_item_open') {
      for (let j = i + 1; j < tokens.length; j++) {
        if (tokens[j].type === 'inline') {
          const { blocks: mediaBlocks, text } = extractMediaFromInline(tokens[j])
          mediaBlocks.forEach((block) => pushBlock(blocks, block))
          if (text) items.push(text)
          break
        }
        if (tokens[j].type === 'list_item_close') break
      }
    }
    i++
  }

  return { items, endIdx: i }
}

function pushBlock(blocks: ContentBlock[], block: ContentBlock): void {
  if (block.type === 'paragraph' && !block.text.trim()) return
  blocks.push(block)
}

export function inferVideoExtn(src: string, mimeType?: string): string {
  if (mimeType?.includes('webm')) return 'webm'
  if (mimeType?.includes('quicktime')) return 'mov'
  const match = src.match(/\.(\w+)(?:\?|$)/)
  if (match) {
    const ext = match[1].toLowerCase()
    if (['mp4', 'webm', 'mov', 'm4v', 'ogg'].includes(ext)) return ext
  }
  return 'mp4'
}

function parsePictureSrc(picture: Element): string {
  const img = picture.querySelector('img')
  if (img) return getImgSrc(img)
  const source = picture.querySelector('source')
  if (source) {
    const srcset = source.getAttribute('srcset') || source.getAttribute('src') || ''
    return srcset.split(/\s+/)[0] || ''
  }
  return ''
}

/** 从 HTML 块按 DOM 顺序提取图片、视频与文本 */
export function parseHtmlBlock(html: string): ContentBlock[] {
  if (typeof DOMParser === 'undefined') {
    return [{ type: 'placeholder', text: '[嵌入内容]' }]
  }

  const doc = new DOMParser().parseFromString(html, 'text/html')
  const blocks: ContentBlock[] = []

  const pushParagraph = (text: string) => {
    const trimmed = stripImagePlaceholders(text)
    if (trimmed) pushBlock(blocks, { type: 'paragraph', text: trimmed })
  }

  const parseList = (list: Element) => {
    const items = Array.from(list.querySelectorAll(':scope > li'))
      .map((li) => stripImagePlaceholders(li.textContent || ''))
      .filter(Boolean)
    if (items.length) {
      blocks.push({ type: 'list', ordered: list.tagName === 'OL', items })
    }
  }

  const parseVideo = (video: HTMLVideoElement) => {
    let src = video.getAttribute('src') || ''
    let mimeType = video.getAttribute('type') || undefined
    if (!src) {
      const source = video.querySelector('source')
      if (source) {
        src = source.getAttribute('src') || ''
        mimeType = source.getAttribute('type') || mimeType
      }
    }
    if (src) {
      blocks.push({
        type: 'video',
        src,
        poster: video.getAttribute('poster') || undefined,
        mimeType,
      })
    }
  }

  const walkNode = (node: Node) => {
    if (node.nodeType !== Node.ELEMENT_NODE) return
    const el = node as Element
    const tag = el.tagName

    const headingType = htmlHeadingType(tag)
    if (headingType) {
      pushBlock(blocks, { type: headingType, text: el.textContent?.trim() || '' })
      return
    }

    if (tag === 'IMG') {
      const src = getImgSrc(el)
      if (src) {
        blocks.push({
          type: 'image',
          alt: el.getAttribute('alt') || 'image',
          src,
        })
      }
      return
    }

    if (tag === 'VIDEO') {
      parseVideo(el as HTMLVideoElement)
      return
    }

    if (tag === 'PICTURE') {
      const src = parsePictureSrc(el)
      const img = el.querySelector('img')
      if (src) {
        blocks.push({
          type: 'image',
          alt: img?.getAttribute('alt') || 'image',
          src,
        })
      }
      return
    }

    if (tag === 'A') {
      el.querySelectorAll('img').forEach((img) => walkNode(img))
      el.querySelectorAll('video').forEach((video) => walkNode(video))
      return
    }

    if (tag === 'UL' || tag === 'OL') {
      parseList(el)
      return
    }

    if (tag === 'FIGURE') {
      const img = el.querySelector('img')
      const video = el.querySelector('video')
      if (img) walkNode(img)
      else if (video) walkNode(video)
      const caption = el.querySelector('figcaption')?.textContent?.trim()
      if (caption) pushParagraph(caption)
      return
    }

    if (tag === 'P') {
      const imgs = el.querySelectorAll('img')
      const videos = el.querySelectorAll('video')
      if (imgs.length || videos.length) {
        const blockStartLen = blocks.length
        imgs.forEach((img) => walkNode(img))
        videos.forEach((video) => walkNode(video))
        const textOnly = Array.from(el.childNodes)
          .filter((n) => n.nodeType === Node.TEXT_NODE)
          .map((n) => n.textContent || '')
          .join('')
          .trim()
        if (textOnly) {
          let lastImage: ImageBlock | undefined
          for (let k = blocks.length - 1; k >= blockStartLen; k--) {
            const candidate = blocks[k]
            if (candidate.type === 'image') {
              lastImage = candidate
              break
            }
          }
          if (!lastImage || !isImageCaptionLike(textOnly, lastImage)) {
            pushParagraph(textOnly)
          }
        }
      } else {
        pushParagraph(el.textContent || '')
      }
      return
    }

    if (tag === 'DIV' || tag === 'SECTION' || tag === 'ARTICLE' || tag === 'BLOCKQUOTE' || tag === 'SPAN') {
      el.childNodes.forEach((child) => walkNode(child))
      return
    }

    if (el.children.length > 0) {
      el.childNodes.forEach((child) => walkNode(child))
      return
    }

    if (el.textContent?.trim()) {
      pushParagraph(el.textContent)
    }
  }

  doc.body.childNodes.forEach((child) => walkNode(child))

  if (!blocks.length) {
    const text = doc.body.textContent?.trim()
    if (text) pushParagraph(text)
    else blocks.push({ type: 'placeholder', text: '[嵌入内容]' })
  }

  return blocks
}

/** 将 blocks 分组为渲染单元（图片/视频始终独立成块） */
export function buildRenderUnits(blocks: ContentBlock[]): RenderUnit[] {
  return blocks.map((block) => ({ kind: 'single' as const, block }))
}

/** 幻灯片分步总数（step0=仅标题） */
export function getSlideStepCount(slide: PptSlide): number {
  if (slide.kind === 'title') {
    const hasMeta = Boolean(slide.date || slide.categoryName || slide.userName || slide.tagNames.length)
    return hasMeta ? 2 : 1
  }
  return 1 + buildRenderUnits(slide.blocks).length
}

export function getSlideRenderUnits(slide: PptSectionSlide): RenderUnit[] {
  return buildRenderUnits(slide.blocks)
}

export interface H1Module {
  title: string
  h1Blocks: ContentBlock[]
  sections: SlideSection[]
}

const MEDIA_PAGINATE_HEIGHT = 2.8
const SHORT_PARAGRAPH_MAX = 80
const BARE_IMAGE_FILENAME_RE = /^[\w\u4e00-\u9fa5.-]+\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i

function normalizeCaptionKey(text: string): string {
  return text.trim().toLowerCase().replace(/\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i, '')
}

function isBareImageFilename(text: string): boolean {
  return BARE_IMAGE_FILENAME_RE.test(text.trim())
}

type ImageBlock = Extract<ContentBlock, { type: 'image' }>

function getImageBasename(src: string): string {
  const filename = src.split(/[/\\?#]/).pop() || ''
  return filename.replace(/\.(jpg|jpeg|png|gif|webp|svg|bmp)(?:\?.*)?$/i, '')
}

function getImageCaptionKeys(image: ImageBlock): Set<string> {
  const keys = new Set<string>()
  const add = (text: string) => {
    const key = normalizeCaptionKey(text)
    if (key) keys.add(key)
  }
  add(image.alt)
  add(getImageBasename(image.src))
  return keys
}

/** 判定段落/列表项是否为图片冗余 caption（含「标题 + 文件名」复合形式） */
export function isImageCaptionLike(text: string, image: ImageBlock): boolean {
  const trimmed = text.trim()
  if (!trimmed) return false

  const keys = getImageCaptionKeys(image)

  if (isBareImageFilename(trimmed)) return true

  const normalized = normalizeCaptionKey(trimmed)
  if (keys.has(normalized)) return true

  const tokens = trimmed.split(/\s+/).filter(Boolean)
  if (tokens.length > 0) {
    const tokenKeys = tokens.map((token) => normalizeCaptionKey(token))
    if (tokenKeys.every((key) => keys.has(key))) return true

    const uniqueKeys = new Set(tokenKeys.filter(Boolean))
    if (uniqueKeys.size === 1 && keys.has([...uniqueKeys][0])) return true
  }

  return false
}

/** 移除与相邻图片 alt 重复的 caption 段落（含裸文件名与复合 caption） */
export function dedupeImageCaptionBlocks(blocks: ContentBlock[]): ContentBlock[] {
  const result: ContentBlock[] = []

  for (let i = 0; i < blocks.length; i++) {
    const block = blocks[i]
    const prev = result[result.length - 1]

    if (block.type === 'paragraph') {
      const text = stripImagePlaceholders(block.text)
      if (!text) continue

      const next = blocks[i + 1]

      if (prev?.type === 'image' && isImageCaptionLike(text, prev)) {
        continue
      }
      if (next?.type === 'image' && isImageCaptionLike(text, next)) {
        continue
      }
      result.push(text === block.text ? block : { ...block, text })
      continue
    }

    if (block.type === 'list') {
      const items = block.items
        .map((item) => stripImagePlaceholders(item))
        .filter(Boolean)
        .filter((item) => !(prev?.type === 'image' && isImageCaptionLike(item, prev)))
      if (items.length) {
        result.push({ ...block, items })
      }
      continue
    }

    result.push(block)
  }

  return result
}

export function buildSlideSectionTitle(moduleTitle: string, sectionTitle: string): string {
  if (sectionTitle === moduleTitle) return moduleTitle
  return `${moduleTitle} · ${sectionTitle}`
}

function isShortParagraph(block: ContentBlock | undefined): block is Extract<ContentBlock, { type: 'paragraph' }> {
  return block?.type === 'paragraph' && block.text.length <= SHORT_PARAGRAPH_MAX
}

/** 分页用高度估算（媒体块低于渲染估算，允许与短说明同页） */
function estimatePaginationHeight(block: ContentBlock, next?: ContentBlock): number {
  if (block.type === 'image' || block.type === 'video') {
    return isShortParagraph(next) ? MEDIA_PAGINATE_HEIGHT + 0.45 : MEDIA_PAGINATE_HEIGHT
  }
  return estimateBlockHeight(block)
}

function accumulateBlockHeight(y: number, block: ContentBlock, next?: ContentBlock): number {
  switch (block.type) {
    case 'h1':
      return y + 0.6
    case 'h2':
      return y + 0.5
    case 'h3':
      return y + 0.45
    case 'paragraph':
      return y + estimateBlockHeight(block) + 0.1
    case 'code':
      return y + estimateBlockHeight(block) + 0.35
    case 'list':
      return y + estimateBlockHeight(block) + 0.1
    case 'image':
    case 'video':
      return y + MEDIA_PAGINATE_HEIGHT + (isShortParagraph(next) ? 0.45 : 0)
    case 'placeholder':
      return y + 0.4
    default:
      return y + 0.4
  }
}

function compactSparseSlides(slides: PptSectionSlide[], sectionTitle: string): PptSectionSlide[] {
  if (slides.length <= 1) return slides

  const merged: PptSectionSlide[] = []

  for (const slide of slides) {
    const contentBlocks = slide.blocks.filter(
      (b) => b.type !== 'h1' && b.type !== 'h2' && b.type !== 'h3'
    )
    const totalHeight = slide.blocks.reduce((sum, b) => sum + estimateBlockHeight(b), 0)

    const prev = merged[merged.length - 1]
    if (prev && contentBlocks.length <= 1 && totalHeight < 1.2) {
      prev.blocks.push(...slide.blocks)
      continue
    }

    merged.push({ ...slide, blocks: [...slide.blocks] })
  }

  return merged.map((slide, idx) => ({
    ...slide,
    title: formatContinuationTitle(sectionTitle, idx),
    continued: idx > 0,
  }))
}

/** 按 H1 模块（# 到下一 #）+ 模块内 ## 小节解析 Markdown */
export function parseMarkdownToH1Modules(markdown: string): H1Module[] {
  const tokens = getMarkdownParser().parse(markdown, {})
  const modules: H1Module[] = []
  let currentModule: H1Module | null = null
  let currentSection: SlideSection | null = null

  const startModule = (title: string) => {
    currentModule = { title, h1Blocks: [], sections: [] }
    modules.push(currentModule)
    currentSection = null
  }

  const startSection = (title: string, withH2Block = false) => {
    if (!currentModule) startModule('前言')
    currentSection = { title, blocks: [] }
    currentModule!.sections.push(currentSection)
    if (withH2Block && title) {
      pushBlock(currentSection.blocks, { type: 'h2', text: title })
    }
  }

  const getTargetBlocks = (): ContentBlock[] => {
    if (!currentModule) startModule('前言')
    if (!currentSection) startSection(currentModule!.title)
    return currentSection!.blocks
  }

  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i]

    if (token.type === 'heading_open') {
      const title = getHeadingText(tokens, i)
      if (token.tag === 'h1') {
        startModule(title || '章节')
        pushBlock(currentModule!.h1Blocks, { type: 'h1', text: title })
        startSection(title || '章节')
      } else if (token.tag === 'h2') {
        if (!currentModule) startModule('前言')
        startSection(title || '小节', true)
      } else if (token.tag === 'h3') {
        pushBlock(getTargetBlocks(), { type: 'h3', text: title })
      }
      i += 2
      continue
    }

    if (token.type === 'paragraph_open') {
      const inline = tokens[i + 1]
      if (inline?.type === 'inline') {
        pushInlineContent(getTargetBlocks(), inline)
      }
      i += 2
      continue
    }

    if (token.type === 'fence') {
      const lang = token.info ? token.info.split(/\s+/g)[0] : undefined
      pushBlock(getTargetBlocks(), { type: 'code', text: token.content.trimEnd(), lang })
      continue
    }

    if (token.type === 'bullet_list_open') {
      const target = getTargetBlocks()
      const { items, endIdx } = parseListItems(tokens, i, 'bullet_list_close', target)
      if (items.length) pushBlock(target, { type: 'list', ordered: false, items })
      i = endIdx
      continue
    }

    if (token.type === 'ordered_list_open') {
      const target = getTargetBlocks()
      const { items, endIdx } = parseListItems(tokens, i, 'ordered_list_close', target)
      if (items.length) pushBlock(target, { type: 'list', ordered: true, items })
      i = endIdx
      continue
    }

    if (token.type === 'blockquote_open') {
      const target = getTargetBlocks()
      const quoteParts: string[] = []
      for (let j = i + 1; j < tokens.length && tokens[j].type !== 'blockquote_close'; j++) {
        if (tokens[j].type === 'inline') {
          const { blocks: mediaBlocks, text } = extractMediaFromInline(tokens[j])
          mediaBlocks.forEach((block) => pushBlock(target, block))
          if (text) quoteParts.push(text)
        }
      }
      const quoteText = quoteParts.join('\n').trim()
      if (quoteText) pushBlock(target, { type: 'paragraph', text: `「${quoteText}」` })
      continue
    }

    if (token.type === 'table_open') {
      pushBlock(getTargetBlocks(), { type: 'placeholder', text: '[表格内容]' })
      continue
    }

    if (token.type === 'html_block') {
      parseHtmlBlock(token.content).forEach((block) => pushBlock(getTargetBlocks(), block))
      continue
    }

    if (token.type === 'html_inline') {
      parseInlineHtmlFragment(token.content).forEach((block) => pushBlock(getTargetBlocks(), block))
      continue
    }
  }

  return modules
    .map((module) => ({
      ...module,
      sections: module.sections.filter((s) => s.blocks.length > 0),
    }))
    .filter((module) => module.sections.length > 0 || module.h1Blocks.length > 0)
}

/** @deprecated 使用 parseMarkdownToH1Modules */
export function parseMarkdownToSections(markdown: string): { intro: ContentBlock[]; sections: SlideSection[] } {
  const modules = parseMarkdownToH1Modules(markdown)
  const intro: ContentBlock[] = []
  const sections: SlideSection[] = []

  for (const module of modules) {
    if (module.title === '前言' && modules[0] === module) {
      for (const section of module.sections) {
        intro.push(...section.blocks)
      }
      continue
    }
    for (const section of module.sections) {
      sections.push({
        title: buildSlideSectionTitle(module.title, section.title),
        blocks: [...section.blocks],
      })
    }
  }

  if (!sections.length && intro.length) {
    sections.push({ title: '正文', blocks: [...intro] })
    intro.length = 0
  }

  return { intro, sections }
}

/** 估算内容块在幻灯片上的高度 */
export function estimateBlockHeight(block: ContentBlock): number {
  switch (block.type) {
    case 'h1':
      return 0.6
    case 'h2':
      return 0.5
    case 'h3':
      return 0.45
    case 'paragraph':
      return Math.min(2.5, Math.max(0.45, Math.ceil(block.text.length / 70) * 0.22))
    case 'code':
      return Math.min(3.2, Math.max(0.8, block.text.split('\n').length * 0.16 + 0.3))
    case 'image':
    case 'video':
      return MEDIA_PAGINATE_HEIGHT
    case 'list':
      return Math.min(3, Math.max(0.5, block.items.length * 0.28))
    case 'placeholder':
      return 0.4
    default:
      return 0.4
  }
}

/** 判断是否为独立媒体块（用于 hero 大图布局） */
export function isStandaloneMediaBlock(block: ContentBlock): boolean {
  return block.type === 'image' || block.type === 'video'
}

function formatContinuationTitle(title: string, continuation: number): string {
  if (continuation <= 0) return title
  if (continuation === 1) return `${title}（续）`
  return `${title}（续${continuation}）`
}

/** 将章节内容按高度分页为多张幻灯片 */
export function paginateSectionBlocks(section: SlideSection): PptSectionSlide[] {
  const blocks = dedupeImageCaptionBlocks(section.blocks)
  if (!blocks.length) return []

  const slides: PptSectionSlide[] = []
  let currentBlocks: ContentBlock[] = []
  let y = SECTION_HEADER_HEIGHT
  let continuation = 0

  const flushSlide = () => {
    if (!currentBlocks.length) return
    slides.push({
      kind: 'section',
      title: formatContinuationTitle(section.title, continuation),
      blocks: currentBlocks,
      continued: continuation > 0,
    })
    currentBlocks = []
    y = SECTION_HEADER_HEIGHT
  }

  const startContinuation = () => {
    flushSlide()
    continuation += 1
  }

  const ensureSpace = (needed: number) => {
    if (y + needed > PPT_SLIDE_BOTTOM && currentBlocks.length) {
      startContinuation()
    }
  }

  for (let bi = 0; bi < blocks.length; bi++) {
    const block = blocks[bi]
    const next = blocks[bi + 1]
    ensureSpace(estimatePaginationHeight(block, next))
    currentBlocks.push(block)
    y = accumulateBlockHeight(y, block, next)
  }

  flushSlide()
  return compactSparseSlides(slides, section.title)
}

function buildTitleSlide(post: Post): PptTitleSlide {
  return {
    kind: 'title',
    title: post.title,
    date: post.date,
    categoryName: post.categoryName,
    userName: post.userName,
    tagNames: post.tags?.map((t) => t.name) || [],
  }
}

export function buildPptSlides(post: Post): PptSlide[] {
  const slides: PptSlide[] = [buildTitleSlide(post)]
  const modules = parseMarkdownToH1Modules(post.content || '')

  for (const module of modules) {
    for (let si = 0; si < module.sections.length; si++) {
      const section = module.sections[si]
      const blocks: ContentBlock[] = [...section.blocks]
      if (si === 0 && module.h1Blocks.length) {
        blocks.unshift(...module.h1Blocks)
      }

      const title = buildSlideSectionTitle(module.title, section.title)
      slides.push(...paginateSectionBlocks({ title, blocks }))
    }
  }

  return slides
}

/** 解析媒体 URL（相对路径转绝对路径） */
export function resolveImageUrl(src: string): string {
  try {
    return new URL(src, window.location.href).href
  } catch {
    return src
  }
}

/** 判断点击/键盘事件是否发生在媒体交互区域 */
export function isMediaInteractionTarget(el: EventTarget | null): boolean {
  if (!el || !(el instanceof HTMLElement)) return false
  return Boolean(
    el.closest('video, audio, .ppt-block-video-wrap, .ppt-block-image-wrap, [controls]')
  )
}
