import MarkdownIt from 'markdown-it'
import multimdTable from 'markdown-it-multimd-table'
import hljs from 'highlight.js'
import DOMPurify from 'dompurify'
import { getCachedImageBlobUrl, webVfs } from './platform/webVfs'
import 'highlight.js/styles/github.min.css'

const MERMAID_LANG = new Set(['mermaid', 'flowchart', 'sequencediagram', 'sequence'])
const MERMAID_START =
  /^\s*(sequenceDiagram|classDiagram|stateDiagram(?:-v2)?|erDiagram|gantt|pie|gitGraph|journey|flowchart\s|graph\s|mindmap|timeline|quadrantChart|xychart-beta|block-beta)/i

function isMermaidFence(lang: string, content: string): boolean {
  const l = lang.toLowerCase()
  if (MERMAID_LANG.has(l)) return true
  if (!lang.trim() && MERMAID_START.test(content)) return true
  return false
}

function highlightCodeBlock(content: string, lang: string): string {
  const trimmed = (lang || '').trim()
  if (trimmed && hljs.getLanguage(trimmed)) {
    try {
      return hljs.highlight(content, { language: trimmed, ignoreIllegals: true }).value
    } catch {
      /* fall through */
    }
  }
  if (trimmed) {
    try {
      return hljs.highlightAuto(content).value
    } catch {
      /* fall through */
    }
  }
  return md.utils.escapeHtml(content)
}

const md = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
  highlight: function (this: MarkdownIt, str: string, lang: string) {
    const body = highlightCodeBlock(str.replace(/\n$/, ''), lang)
    return `<pre class="hljs"><code>${body}</code></pre>`
  }
})

md.use(multimdTable, { multiline: true, rowspan: true, headerless: false })

/** 允许本地 file:// 图片与链接（markdown-it 默认禁止 file: 协议）。 */
const defaultValidateLink = md.validateLink.bind(md)
md.validateLink = (url: string): boolean => {
  if (/^file:/i.test(url)) return true
  if (/^blob:/i.test(url)) return true
  return defaultValidateLink(url)
}

/** 禁用 Setext 标题，避免 `---` 等被当成 h2。 */
md.disable(['lheading'])

function setBlockSourceLines(token: { map?: [number, number] | null; attrSet: (n: string, v: string) => void }): void {
  if (!token.map) return
  const start = token.map[0]
  const end = Math.max(start, token.map[1] - 1)
  token.attrSet('data-source-line', String(start))
  token.attrSet('data-source-line-end', String(end))
}

const defaultHeadingOpen =
  md.renderer.rules.heading_open ??
  function (tokens, idx, options, _env, self) {
    return self.renderToken(tokens, idx, options)
  }

md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
  const token = tokens[idx]
  setBlockSourceLines(token)
  token.attrSet('id', slugForLine(token.map ? token.map[0] : 0))
  return defaultHeadingOpen(tokens, idx, options, env, self)
}

function wrapBlockOpenRule(ruleName: 'paragraph_open' | 'list_item_open' | 'blockquote_open'): void {
  const prev = md.renderer.rules[ruleName]
  md.renderer.rules[ruleName] = (tokens, idx, options, env, self) => {
    setBlockSourceLines(tokens[idx]!)
    if (prev) return prev(tokens, idx, options, env, self)
    return self.renderToken(tokens, idx, options)
  }
}

wrapBlockOpenRule('paragraph_open')
wrapBlockOpenRule('list_item_open')
wrapBlockOpenRule('blockquote_open')

const defaultTableOpen = md.renderer.rules.table_open
const defaultTableClose = md.renderer.rules.table_close

md.renderer.rules.table_open = (tokens, idx, options, env, self) => {
  setBlockSourceLines(tokens[idx]!)
  const inner = defaultTableOpen
    ? defaultTableOpen(tokens, idx, options, env, self)
    : '<table>'
  return `<div class="md-table-wrap">${inner}`
}

md.renderer.rules.table_close = (tokens, idx, options, env, self) => {
  const inner = defaultTableClose
    ? defaultTableClose(tokens, idx, options, env, self)
    : '</table>'
  return `${inner}</div>`
}

/** 1×1 透明占位图：浏览器禁止在 <img> 中直接加载 file://。 */
const IMG_PLACEHOLDER =
  'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

function resolveImageDiskPath(src: string, baseFilePath: string | null | undefined): string | null {
  return webVfs.resolveImageAbsPath(src, baseFilePath?.trim() || null)
}

function normalizeImageSrc(src: string, baseFilePath: string | null | undefined): string {
  const s = src.trim()
  if (!s) return s
  if (/^(blob:|data:|https?:)/i.test(s)) return s

  const absPath = resolveImageDiskPath(s, baseFilePath)
  if (!absPath) return s

  const hit = getCachedImageBlobUrl(absPath)
  if (hit) return hit

  return IMG_PLACEHOLDER
}

const defaultImage = md.renderer.rules.image!
md.renderer.rules.image = (tokens, idx, options, env, self) => {
  const token = tokens[idx]!
  const src = token.attrGet('src')
  const base = (env as { baseFilePath?: string | null }).baseFilePath
  if (src) {
    const disk = resolveImageDiskPath(src, base)
    if (disk) token.attrSet('data-disk-path', disk)
    token.attrSet('src', normalizeImageSrc(src, base))
  }
  const html = defaultImage(tokens, idx, options, env, self)
  return html.replace(/<img /i, '<img loading="lazy" decoding="async" ')
}

const defaultFence = md.renderer.rules.fence
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
  const token = tokens[idx]
  if (!token.map) {
    return defaultFence ? defaultFence(tokens, idx, options, env, self) : ''
  }
  const lang = (token.info || '').trim()
  const raw = token.content.replace(/\n$/, '')
  const codeEndLine = Math.max(token.map[0], token.map[1] - 1)

  if (isMermaidFence(lang, raw)) {
    const trimmed = raw.trim()
    const escaped = md.utils.escapeHtml(trimmed)
    const dataSrc = md.utils.escapeHtml(encodeURIComponent(trimmed))
    return `<div class="md-diagram-wrap" data-source-line="${token.map[0]}" data-source-line-end="${codeEndLine}" data-mermaid-src="${dataSrc}"><pre class="mermaid">${escaped}</pre></div>`
  }

  const highlighted = highlightCodeBlock(raw, lang)
  const langLabel = lang
    ? `<span class="md-code-lang">${md.utils.escapeHtml(lang.split(/\s+/)[0] ?? lang)}</span>`
    : ''
  return `<div class="md-code-wrap" data-source-line="${token.map[0]}" data-source-line-end="${codeEndLine}">${langLabel}<pre class="hljs"><code>${highlighted}</code></pre></div>`
}

const purifyOpts: Parameters<typeof DOMPurify.sanitize>[1] = {
  USE_PROFILES: { html: true },
  ADD_TAGS: ['span', 'mark', 'kbd', 'sup', 'sub', 'details', 'summary', 'font', 'div'],
  ADD_ATTR: [
    'style',
    'color',
    'open',
    'data-source-line',
    'data-source-line-end',
    'id',
    'class',
    'href',
    'data-md-href',
    'src',
    'alt',
    'width',
    'height',
    'loading',
    'decoding',
    'data-mermaid-src'
  ],
  ALLOWED_URI_REGEXP:
    /^(?:(?:https?|mailto|tel|data|file|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
}

const defaultLinkOpen = md.renderer.rules.link_open
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
  const token = tokens[idx]!
  const href = token.attrGet('href') ?? ''
  if (href && !/^https?:\/\//i.test(href) && !href.startsWith('#') && !href.startsWith('mailto:')) {
    token.attrSet('class', 'md-internal-link')
    token.attrSet('data-md-href', href)
  }
  if (defaultLinkOpen) return defaultLinkOpen(tokens, idx, options, env, self)
  return self.renderToken(tokens, idx, options)
}

export function slugForLine(line: number): string {
  return `mr-line-${line}`
}

export type RenderMarkdownEnv = {
  baseFilePath?: string | null
}

export function renderMarkdown(src: string, env?: RenderMarkdownEnv): string {
  const input = typeof src === 'string' ? src : ''
  const renderEnv = env ?? {}
  try {
    const raw = md.render(input, renderEnv)
    try {
      return DOMPurify.sanitize(raw, purifyOpts)
    } catch (e) {
      console.warn('[MyReader] DOMPurify failed', e)
      return `<p>${md.utils.escapeHtml(raw.slice(0, 8000))}</p>`
    }
  } catch (e) {
    console.warn('[MyReader] markdown render failed', e)
    return `<pre>${md.utils.escapeHtml(input.slice(0, 8000))}</pre>`
  }
}

export type OutlineItem = {
  level: number
  text: string
  displayText: string
  /** 0-based source line */
  line: number
  slug: string
}

function outlineDisplayText(raw: string): string {
  let s = raw
  s = s.replace(/\*\*([^*]+)\*\*/g, '$1')
  s = s.replace(/\*([^*]+)\*/g, '$1')
  s = s.replace(/`([^`]+)`/g, '$1')
  s = s.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
  s = s.replace(/<[^>]+>/g, '')
  s = s.replace(/\s+/g, ' ').trim()
  return s
}

/** 分隔线、纯符号等不应出现在大纲。 */
export function isValidOutlineHeading(displayText: string, rawLine: string): boolean {
  const t = displayText.trim()
  if (!t || t === '(空标题)') return false
  if (/^[-—–_=~*`#>\s]+$/.test(t)) return false
  const body = rawLine.replace(/^#{1,6}\s*/, '').trim()
  if (/^[-—–_=~*\s]+$/.test(body)) return false
  return true
}

/** 仅从 ATX `#` 标题提取；`line` 为 0-based。 */
export function extractOutline(src: string): OutlineItem[] {
  const input = typeof src === 'string' ? src : ''
  const lines = input.split(/\r?\n/)
  const out: OutlineItem[] = []
  for (let i = 0; i < lines.length; i++) {
    const raw = lines[i] ?? ''
    const m = /^(#{1,6})\s+(.+)$/.exec(raw)
    if (!m) continue
    const level = m[1]!.length
    const text = m[2]!.trim()
    const displayText = outlineDisplayText(text) || '(空标题)'
    if (!isValidOutlineHeading(displayText, raw)) continue
    out.push({
      level,
      text,
      displayText,
      line: i,
      slug: slugForLine(i)
    })
  }
  return out
}

export type SourceLineRegion = {
  line: number
  lineEnd: number
  el: HTMLElement
}

export function collectSourceLineRegions(article: HTMLElement): SourceLineRegion[] {
  const nodes = article.querySelectorAll<HTMLElement>('[data-source-line]')
  const regions: SourceLineRegion[] = []
  for (const el of nodes) {
    const start = Number(el.getAttribute('data-source-line'))
    if (!Number.isFinite(start)) continue
    const endRaw = el.getAttribute('data-source-line-end')
    const end = endRaw !== null && Number.isFinite(Number(endRaw)) ? Number(endRaw) : start
    regions.push({ line: start, lineEnd: Math.max(start, end), el })
  }
  regions.sort((a, b) => a.line - b.line)
  return regions
}

/** 阅读区中与源行号对应的元素（用于高亮闪烁）。 */
export function findPreviewAnchorForLine(
  article: HTMLElement,
  line: number
): HTMLElement | null {
  return finestElementForLine(article, line) ?? article.querySelector<HTMLElement>(`#${slugForLine(line)}`)
}

function finestElementForLine(article: HTMLElement, line: number): HTMLElement | null {
  const nodes = article.querySelectorAll<HTMLElement>(`[data-source-line="${line}"]`)
  for (const el of nodes) {
    const start = Number(el.getAttribute('data-source-line'))
    if (start !== line) continue
    const endRaw = el.getAttribute('data-source-line-end')
    const end = endRaw !== null && Number.isFinite(Number(endRaw)) ? Number(endRaw) : start
    if (end > start) continue
    return el
  }
  return null
}

/**
 * 分栏阅读区：计算源行在 shell 内的垂直中心（px）。
 * `line` 为 0-based 源行号。
 */
export function previewLineCenterInShell(
  article: HTMLElement,
  shell: HTMLElement,
  line: number,
  totalSourceLines: number
): number | null {
  const shellRect = shell.getBoundingClientRect()

  const exact = finestElementForLine(article, line)
  if (exact) {
    const r = exact.getBoundingClientRect()
    return r.top - shellRect.top + r.height / 2
  }

  const regions = collectSourceLineRegions(article)
  if (regions.length === 0) {
    const h = article.offsetHeight || 1
    return ((line + 0.5) / Math.max(1, totalSourceLines)) * h
  }

  let region = regions[0]!
  for (const r of regions) {
    if (r.line <= line) region = r
    else break
  }

  if (line >= region.line && line <= region.lineEnd) {
    const span = Math.max(1, region.lineEnd - region.line + 1)
    const offset = Math.max(0, Math.min(line - region.line, span - 1))
    const frac = (offset + 0.5) / span
    const blockRect = region.el.getBoundingClientRect()
    return blockRect.top - shellRect.top + frac * blockRect.height
  }

  const blockRect = region.el.getBoundingClientRect()
  return blockRect.top - shellRect.top + blockRect.height / 2
}

export function scrollPreviewToSourceLine(
  scrollEl: HTMLElement,
  article: HTMLElement,
  line: number,
  behavior: ScrollBehavior = 'smooth',
  totalSourceLines?: number
): HTMLElement | null {
  const total = totalSourceLines ?? article.querySelectorAll('[data-source-line]').length
  const shell = article.closest('.preview-shell') ?? article.parentElement
  const anchor = findPreviewAnchorForLine(article, line)
  if (!anchor && !shell) return null

  const scrollRect = scrollEl.getBoundingClientRect()
  let top: number
  if (anchor) {
    const anchorRect = anchor.getBoundingClientRect()
    top = anchorRect.top - scrollRect.top + scrollEl.scrollTop - 20
    anchor.classList.add('preview-anchor-flash')
    window.setTimeout(() => anchor.classList.remove('preview-anchor-flash'), 1600)
  } else if (shell instanceof HTMLElement) {
    const center = previewLineCenterInShell(article, shell, line, total)
    top = (center ?? 0) - 40
  } else {
    return null
  }
  scrollEl.scrollTo({ top: Math.max(0, top), behavior })
  return anchor
}

/** @deprecated 使用 previewLineCenterInShell */
export function anchorCenterInShell(anchor: HTMLElement, shell: HTMLElement): number {
  const shellRect = shell.getBoundingClientRect()
  const r = anchor.getBoundingClientRect()
  return r.top - shellRect.top + r.height / 2
}
