import * as React from 'react'
import { EditorPane, type MarkdownEditorHandle } from './EditorPane'
import type { EditorView } from '@codemirror/view'
import { MarkdownFormatToolbar } from './MarkdownFormatToolbar'
import { OutlinePanel } from './OutlinePanel'
import { PreviewArticle } from './PreviewArticle'
import {
  extractOutline,
  previewLineCenterInShell,
  renderMarkdown,
  scrollPreviewToSourceLine,
  type OutlineItem
} from './markdown'
import { BACKGROUNDS, FONTS, validBackgroundId, validFontId, type ViewMode } from './appearance'
import { COLOR_SWATCH_ROWS } from './colorPresets'
import { registerPathPrompt } from './platform/pathPrompt'
import {
  downloadTextFile,
  fileNameFromPath,
  lacksFileSystemAccessContext
} from './downloadTextFile'
import { randomId } from './randomId'
import { webVfs } from './platform/webVfs'

type Tab = {
  id: string
  filePath: string | null
  content: string
  savedContent: string
}

const BG_KEY = 'myreader-bg-id'
const FONT_KEY = 'myreader-font-id'
const VIEW_KEY = 'myreader-view'
const LEGACY_READ_KEY = 'myreader-read'
const SESSION_KEY = 'myreader-session-v1'
const SIDEBAR_W_KEY = 'myreader-sidebar-w'
const SIDEBAR_HIDDEN_KEY = 'myreader-sidebar-hidden'
const DESKTOP_DOWNLOAD_URL =
  'http://huangzjsecret.online/api/uploads/huangzhijun`sReader.zip'
const SPLIT_KEY = 'myreader-split'
const PREVIEW_W_KEY = 'myreader-preview-max'
const PREVIEW_LINE_KEY = 'myreader-preview-line-height'

function loadPreviewLineHeight(): string {
  const raw = localStorage.getItem(PREVIEW_LINE_KEY)
  if (!raw) return '1.5'
  const n = Number.parseFloat(raw)
  if (!Number.isFinite(n)) return '1.5'
  return String(Math.min(3, Math.max(1, n)))
}

type SessionTabRow = { path: string | null; content: string; saved: string }
type SessionV1 = { v: 1; activeIndex: number; tabs: SessionTabRow[]; bgId?: string; fontId?: string }

function clamp(n: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, n))
}

function normalizePathKey(p: string): string {
  return p.replace(/\\/g, '/').toLowerCase()
}

function newTab(): Tab {
  const id = randomId()
  return { id, filePath: null, content: '', savedContent: '' }
}

function tabLabel(t: Tab): string {
  if (t.filePath) {
    const parts = t.filePath.split(/[/\\]/)
    return parts[parts.length - 1] || t.filePath
  }
  return '未命名'
}

function isDirty(t: Tab): boolean {
  return t.content !== t.savedContent
}

function loadViewMode(): ViewMode {
  const v = localStorage.getItem(VIEW_KEY) as ViewMode | null
  if (v === 'edit' || v === 'read' || v === 'hybrid') return v
  if (localStorage.getItem(LEGACY_READ_KEY) === '1') return 'read'
  if (localStorage.getItem(LEGACY_READ_KEY) === '0') return 'edit'
  return 'read'
}

function viewModeLabel(m: ViewMode): string {
  if (m === 'edit') return '编辑'
  if (m === 'read') return '阅读'
  return '分栏'
}

export default function App(): React.ReactElement {
  const api = window.myreader
  const editorRef = React.useRef<MarkdownEditorHandle | null>(null)
  const [tabs, setTabs] = React.useState<Tab[]>([])
  const [activeId, setActiveId] = React.useState<string>('')
  const [sessionHydrated, setSessionHydrated] = React.useState(false)
  const [viewMode, setViewMode] = React.useState<ViewMode>(() => loadViewMode())
  const [bgId, setBgId] = React.useState<string>(() => {
    return (
      validBackgroundId(localStorage.getItem(BG_KEY)) ??
      validBackgroundId(localStorage.getItem('myreader-theme')) ??
      validBackgroundId(localStorage.getItem('myreader-bg')) ??
      '1'
    )
  })
  const [fontId, setFontId] = React.useState<string>(() => {
    return validFontId(localStorage.getItem(FONT_KEY)) ?? '0'
  })
  const [previewLineHeight, setPreviewLineHeight] = React.useState<string>(() => loadPreviewLineHeight())
  const [gotoLine, setGotoLine] = React.useState<{ line: number; seq: number } | null>(null)
  const [sidebarHidden, setSidebarHidden] = React.useState<boolean>(
    () => localStorage.getItem(SIDEBAR_HIDDEN_KEY) === '1'
  )
  const [sidebarWidth, setSidebarWidth] = React.useState<number>(() => {
    const n = Number(localStorage.getItem(SIDEBAR_W_KEY) ?? '260')
    return Number.isFinite(n) ? clamp(n, 160, 560) : 260
  })
  const [splitPct, setSplitPct] = React.useState<number>(() => {
    const n = Number(localStorage.getItem(SPLIT_KEY) ?? '48')
    return Number.isFinite(n) ? clamp(n, 22, 78) : 48
  })
  const [previewMaxWidth, setPreviewMaxWidth] = React.useState<number>(() => {
    const n = Number(localStorage.getItem(PREVIEW_W_KEY) ?? '880')
    return Number.isFinite(n) ? clamp(n, 420, 1400) : 880
  })
  const [closePromptTabId, setClosePromptTabId] = React.useState<string | null>(null)
  const [textPrompt, setTextPrompt] = React.useState<{
    title: string
    hint?: string
    defaultValue: string
    resolve: (v: string | null) => void
  } | null>(null)
  const [textPromptInput, setTextPromptInput] = React.useState('')
  const [imageInsert, setImageInsert] = React.useState<{
    resolve: (v: { url: string; alt: string } | null) => void
  } | null>(null)
  const [imageUrlInput, setImageUrlInput] = React.useState('')
  const [imageAltInput, setImageAltInput] = React.useState('图片')
  const [colorPicker, setColorPicker] = React.useState<{
    resolve: (v: string | null) => void
  } | null>(null)
  const [colorHexInput, setColorHexInput] = React.useState('#dc2626')
  const [webNotesOpen, setWebNotesOpen] = React.useState(false)
  const [desktopDownloadOpen, setDesktopDownloadOpen] = React.useState(false)
  const readPreviewOuterRef = React.useRef<HTMLDivElement | null>(null)
  const readPreviewArticleRef = React.useRef<HTMLElement | null>(null)
  const hybridPreviewRef = React.useRef<HTMLDivElement | null>(null)
  const hybridPreviewShellRef = React.useRef<HTMLDivElement | null>(null)
  const hybridPreviewArticleRef = React.useRef<HTMLElement | null>(null)
  const hybridEditorViewRef = React.useRef<EditorView | null>(null)
  const [hybridViewEpoch, setHybridViewEpoch] = React.useState(0)
  const [hybridLinePulse, setHybridLinePulse] = React.useState<{
    topPx: number
    seq: number
  } | null>(null)

  const activeTab = tabs.find((t) => t.id === activeId) ?? null
  const activeDoc = activeTab?.content ?? ''
  const activeLineCount = React.useMemo(
    () => (activeDoc ? activeDoc.split(/\r?\n/).length : 1),
    [activeDoc]
  )

  const tabsRef = React.useRef(tabs)
  tabsRef.current = tabs

  React.useEffect(() => {
    if (!api) {
      setSessionHydrated(true)
      return
    }
    let cancelled = false
    void (async () => {
      try {
        const raw = localStorage.getItem(SESSION_KEY)
        if (!raw) return
        const data = JSON.parse(raw) as Partial<SessionV1>
        if (data.v !== 1 || !Array.isArray(data.tabs)) return
        const restored: Tab[] = []
        for (const row of data.tabs) {
          if (!row || typeof row !== 'object') continue
          const id = randomId()
          const content = typeof row.content === 'string' ? row.content : ''
          const saved = typeof row.saved === 'string' ? row.saved : content
          if (row.path) {
            try {
              await api.readFile(row.path)
              restored.push({ id, filePath: row.path, content, savedContent: saved })
            } catch {
              /* missing or unreadable */
            }
          } else {
            restored.push({ id, filePath: null, content, savedContent: saved })
          }
        }
        if (cancelled) return
        setTabs(restored)
        if (restored.length) {
          let idx = typeof data.activeIndex === 'number' ? data.activeIndex : 0
          if (!Number.isFinite(idx)) idx = 0
          idx = Math.max(0, Math.min(idx, restored.length - 1))
          setActiveId(restored[idx]!.id)
        }
        if (typeof data.bgId === 'string') {
          const b = validBackgroundId(data.bgId)
          if (b) setBgId(b)
        }
        if (typeof data.fontId === 'string') {
          const f = validFontId(data.fontId)
          if (f) setFontId(f)
        }
      } catch {
        /* ignore corrupt session */
      } finally {
        if (!cancelled) setSessionHydrated(true)
      }
    })()
    return () => {
      cancelled = true
    }
  }, [api])

  React.useEffect(() => {
    if (!sessionHydrated) return
    const id = window.setTimeout(() => {
      const activeIndex =
        tabs.length === 0 ? 0 : Math.max(0, tabs.findIndex((t) => t.id === activeId))
      const idx =
        tabs.length === 0
          ? 0
          : activeIndex < 0
            ? 0
            : Math.min(activeIndex, tabs.length - 1)
      const payload: SessionV1 = {
        v: 1,
        activeIndex: idx,
        tabs: tabs.map((t) => ({
          path: t.filePath,
          content: t.content,
          saved: t.savedContent
        })),
        bgId,
        fontId
      }
      localStorage.setItem(SESSION_KEY, JSON.stringify(payload))
    }, 400)
    return () => window.clearTimeout(id)
  }, [tabs, activeId, sessionHydrated, bgId, fontId])

  React.useEffect(() => {
    localStorage.setItem(BG_KEY, bgId)
    document.documentElement.dataset.bg = bgId
  }, [bgId])

  React.useEffect(() => {
    localStorage.setItem(FONT_KEY, fontId)
    document.documentElement.dataset.font = fontId
  }, [fontId])

  React.useEffect(() => {
    localStorage.setItem(PREVIEW_LINE_KEY, previewLineHeight)
    document.documentElement.style.setProperty('--preview-line-height', previewLineHeight)
  }, [previewLineHeight])

  React.useEffect(() => {
    localStorage.setItem(VIEW_KEY, viewMode)
  }, [viewMode])

  React.useEffect(() => {
    localStorage.setItem(SIDEBAR_HIDDEN_KEY, sidebarHidden ? '1' : '0')
  }, [sidebarHidden])

  React.useEffect(() => {
    localStorage.setItem(SIDEBAR_W_KEY, String(sidebarWidth))
  }, [sidebarWidth])

  React.useEffect(() => {
    localStorage.setItem(SPLIT_KEY, String(splitPct))
  }, [splitPct])

  React.useEffect(() => {
    localStorage.setItem(PREVIEW_W_KEY, String(previewMaxWidth))
  }, [previewMaxWidth])

  React.useEffect(() => {
    if (textPrompt) setTextPromptInput(textPrompt.defaultValue)
  }, [textPrompt])

  React.useEffect(() => {
    if (imageInsert) {
      setImageUrlInput('')
      setImageAltInput('图片')
    }
  }, [imageInsert])

  React.useEffect(() => {
    if (colorPicker) setColorHexInput('#dc2626')
  }, [colorPicker])

  React.useEffect(() => {
    if (viewMode !== 'hybrid') setHybridLinePulse(null)
  }, [viewMode])

  const requestText = React.useCallback((title: string, defaultValue: string) => {
    return new Promise<string | null>((resolve) => {
      setTextPrompt({ title, defaultValue, resolve })
    })
  }, [])

  const requestDiskPath = React.useCallback(
    (title: string, hint: string, defaultValue: string) => {
      return new Promise<string | null>((resolve) => {
        setTextPrompt({ title, hint, defaultValue, resolve })
      })
    },
    []
  )

  React.useEffect(() => {
    registerPathPrompt(async (req) => {
      const value = await requestDiskPath(req.title, req.hint, req.defaultValue)
      return value?.trim() ? value.trim() : null
    })
    return () => registerPathPrompt(null)
  }, [requestDiskPath])

  const requestImageDetails = React.useCallback((): Promise<{ url: string; alt: string } | null> => {
    return new Promise((resolve) => {
      setImageInsert({ resolve })
    })
  }, [])

  const requestColor = React.useCallback((): Promise<string | null> => {
    return new Promise((resolve) => {
      setColorPicker({ resolve })
    })
  }, [])

  React.useEffect(() => {
    if (viewMode !== 'hybrid') return
    const view = hybridEditorViewRef.current
    const prevEl = hybridPreviewRef.current
    if (!view || !prevEl) return

    const edScroll = view.scrollDOM
    const maxScroll = (el: HTMLElement): number => Math.max(1, el.scrollHeight - el.clientHeight)
    let syncing = false

    const syncFromEditor = (): void => {
      if (syncing) return
      syncing = true
      const re = edScroll.scrollTop / maxScroll(edScroll)
      prevEl.scrollTop = re * maxScroll(prevEl)
      requestAnimationFrame(() => {
        syncing = false
      })
    }
    const syncFromPreview = (): void => {
      if (syncing) return
      syncing = true
      const rp = prevEl.scrollTop / maxScroll(prevEl)
      edScroll.scrollTop = rp * maxScroll(edScroll)
      requestAnimationFrame(() => {
        syncing = false
      })
    }

    edScroll.addEventListener('scroll', syncFromEditor, { passive: true })
    prevEl.addEventListener('scroll', syncFromPreview, { passive: true })
    requestAnimationFrame(() => {
      syncFromEditor()
    })
    return () => {
      edScroll.removeEventListener('scroll', syncFromEditor)
      prevEl.removeEventListener('scroll', syncFromPreview)
    }
  }, [viewMode, hybridViewEpoch, activeId])

  React.useEffect(() => {
    if (viewMode !== 'hybrid') return
    const view = hybridEditorViewRef.current
    const prevEl = hybridPreviewRef.current
    if (!view || !prevEl) return
    requestAnimationFrame(() => {
      const edScroll = view.scrollDOM
      const maxE = Math.max(1, edScroll.scrollHeight - edScroll.clientHeight)
      const maxP = Math.max(1, prevEl.scrollHeight - prevEl.clientHeight)
      const r = edScroll.scrollTop / maxE
      prevEl.scrollTop = r * maxP
    })
  }, [viewMode, activeDoc])

  const onHybridViewReady = React.useCallback((view: EditorView | null) => {
    hybridEditorViewRef.current = view
    setHybridViewEpoch((e) => e + 1)
  }, [])

  const onHybridCursorLine = React.useCallback(
    (line: number) => {
      requestAnimationFrame(() => {
        const article = hybridPreviewArticleRef.current
        const shell = hybridPreviewShellRef.current
        if (!article || !shell) return
        const topPx = previewLineCenterInShell(article, shell, line, activeLineCount)
        if (topPx === null) return
        setHybridLinePulse({ topPx, seq: Date.now() })
      })
    },
    [activeLineCount]
  )

  const navigateToOutline = React.useCallback(
    (item: OutlineItem) => {
      const line = item.line
      if (viewMode === 'read') {
        requestAnimationFrame(() => {
          const scrollEl = readPreviewOuterRef.current
          const article = readPreviewArticleRef.current
          if (scrollEl && article) {
            scrollPreviewToSourceLine(scrollEl, article, line, 'smooth', activeLineCount)
          }
        })
        return
      }
      setGotoLine({ line, seq: Date.now() })
      if (viewMode === 'hybrid') {
        requestAnimationFrame(() => {
          const scrollEl = hybridPreviewRef.current
          const article = hybridPreviewArticleRef.current
          const shell = hybridPreviewShellRef.current
          if (scrollEl && article) {
            scrollPreviewToSourceLine(scrollEl, article, line, 'smooth', activeLineCount)
          }
          if (article && shell) {
            const topPx = previewLineCenterInShell(article, shell, line, activeLineCount)
            if (topPx !== null) setHybridLinePulse({ topPx, seq: Date.now() })
          }
        })
      }
    },
    [viewMode, activeLineCount]
  )

  const outline = React.useMemo(() => extractOutline(activeDoc), [activeDoc])

  const previewHtml = React.useMemo(
    () => renderMarkdown(activeDoc, { baseFilePath: activeTab?.filePath ?? null }),
    [activeDoc, activeTab?.filePath]
  )

  const pickLocalImage = React.useCallback(async (): Promise<void> => {
    if (!api) return
    const overlay = document.querySelector<HTMLElement>('.modal-overlay')
    if (overlay) overlay.style.visibility = 'hidden'
    try {
      const r = await api.openImageDialog()
      if (!r.canceled && r.url) setImageUrlInput(r.url)
    } finally {
      if (overlay) overlay.style.visibility = ''
    }
  }, [api])

  const updateActive = React.useCallback(
    (fn: (t: Tab) => Tab): void => {
      if (!activeId) return
      setTabs((prev) => prev.map((t) => (t.id === activeId ? fn(t) : t)))
    },
    [activeId]
  )

  const onEditorChange = React.useCallback(
    (next: string) => {
      updateActive((t) => ({ ...t, content: next }))
    },
    [updateActive]
  )

  const editorMountKey = activeTab ? `${activeTab.id}-${viewMode}` : `empty-${viewMode}`

  const scrollPreviewToAnchorId = React.useCallback(
    (anchorId: string, mode: ViewMode = viewMode): void => {
      const id = decodeURIComponent(anchorId)
      if (!id) return
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          const article =
            mode === 'read' ? readPreviewArticleRef.current : hybridPreviewArticleRef.current
          if (!article) return
          let el: Element | null = null
          try {
            el = article.querySelector(`#${CSS.escape(id)}`)
          } catch {
            el = article.querySelector(`#${id}`)
          }
          if (!el) el = document.getElementById(id)
          el?.scrollIntoView({ behavior: 'smooth', block: 'start' })
        })
      })
    },
    [viewMode]
  )

  const openPathInTab = React.useCallback(
    async (path: string, anchorId?: string): Promise<void> => {
      if (!api) return
      try {
        const key = normalizePathKey(path)
        const existing = tabsRef.current.find(
          (t) => t.filePath && normalizePathKey(t.filePath) === key
        )
        if (existing) {
          setActiveId(existing.id)
          if (anchorId) scrollPreviewToAnchorId(anchorId)
          return
        }
        const content = await api.readFile(path)
        const nt: Tab = { id: randomId(), filePath: path, content, savedContent: content }
        setTabs((prev) => [...prev, nt])
        setActiveId(nt.id)
        setViewMode('read')
        if (anchorId) scrollPreviewToAnchorId(anchorId, 'read')
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e)
        window.alert(`无法打开文件：${msg}`)
      }
    },
    [api, scrollPreviewToAnchorId]
  )

  const openFileDialog = React.useCallback(async (): Promise<void> => {
    if (!api) return
    const r = await api.openFileDialog()
    if (r.canceled || !r.path || r.content === undefined) return
    const existing = tabsRef.current.find(
      (t) => r.path && t.filePath && normalizePathKey(t.filePath) === normalizePathKey(r.path)
    )
    if (existing) {
      setActiveId(existing.id)
      return
    }
    setViewMode('read')
    const nt: Tab = {
      id: randomId(),
      filePath: r.path,
      content: r.content,
      savedContent: r.content
    }
    setTabs((prev) => [...prev, nt])
    setActiveId(nt.id)
  }, [api])

  const [saveToast, setSaveToast] = React.useState<string | null>(null)

  React.useEffect(() => {
    if (!saveToast) return
    const timer = window.setTimeout(() => setSaveToast(null), 2800)
    return () => clearTimeout(timer)
  }, [saveToast])

  const markTabSaved = React.useCallback((tabId: string, filePath?: string | null): void => {
    setTabs((prev) =>
      prev.map((x) =>
        x.id === tabId
          ? {
              ...x,
              ...(filePath !== undefined ? { filePath } : {}),
              savedContent: x.content
            }
          : x
      )
    )
  }, [])

  const persistSave = React.useCallback(
    async (t: Tab, filePath: string): Promise<void> => {
      await api!.writeFile(filePath, t.content)
      const memoryOnly = webVfs.isMemoryOnlyPath(filePath)
      if (memoryOnly || lacksFileSystemAccessContext()) {
        downloadTextFile(fileNameFromPath(filePath), t.content)
        setSaveToast(
          memoryOnly
            ? '已保存：内容已更新，并已触发下载（浏览器无法直接写回您电脑上的原文件）'
            : '已保存：已触发下载'
        )
      } else {
        setSaveToast('已保存到文件')
      }
    },
    [api]
  )

  const saveTabById = React.useCallback(
    async (tabId: string): Promise<boolean> => {
      if (!api) return false
      const t = tabsRef.current.find((x) => x.id === tabId)
      if (!t) return false
      try {
        if (!t.filePath) {
          const d = await api.saveFileDialog()
          if (d.canceled || !d.path) {
            if (lacksFileSystemAccessContext()) {
              downloadTextFile('未命名.md', t.content)
              markTabSaved(tabId)
              setSaveToast('已保存：已下载「未命名.md」')
              return true
            }
            return false
          }
          await persistSave(t, d.path)
          markTabSaved(tabId, d.path)
          return true
        }
        await persistSave(t, t.filePath)
        markTabSaved(tabId)
        return true
      } catch (e) {
        console.error('[MyReader] saveTabById', e)
        const msg = e instanceof Error ? e.message : String(e)
        if (t.filePath && (lacksFileSystemAccessContext() || webVfs.isMemoryOnlyPath(t.filePath))) {
          downloadTextFile(fileNameFromPath(t.filePath), t.content)
          markTabSaved(tabId)
          setSaveToast('已保存：已触发下载')
          return true
        }
        window.alert(`保存失败：${msg}`)
        return false
      }
    },
    [api, markTabSaved, persistSave]
  )

  const saveActive = React.useCallback(async (): Promise<void> => {
    if (!activeId) {
      window.alert('请先选择要保存的标签页')
      return
    }
    await saveTabById(activeId)
  }, [activeId, saveTabById])

  const saveActiveAs = React.useCallback(async (): Promise<void> => {
    if (!api) return
    const t = tabs.find((x) => x.id === activeId)
    if (!t) {
      window.alert('请先选择要保存的标签页')
      return
    }
    try {
      const d = await api.saveFileDialog(t.filePath ?? undefined)
      if (d.canceled || !d.path) {
        if (lacksFileSystemAccessContext()) {
          downloadTextFile(fileNameFromPath(t.filePath), t.content)
          markTabSaved(activeId)
          setSaveToast('已另存为：已触发下载')
        }
        return
      }
      await persistSave(t, d.path)
      markTabSaved(activeId, d.path)
    } catch (e) {
      console.error('[MyReader] saveActiveAs', e)
      const msg = e instanceof Error ? e.message : String(e)
      if (lacksFileSystemAccessContext()) {
        downloadTextFile(fileNameFromPath(t.filePath), t.content)
        markTabSaved(activeId)
        setSaveToast('已另存为：已触发下载')
        return
      }
      window.alert(`另存为失败：${msg}`)
    }
  }, [api, activeId, tabs, markTabSaved, persistSave])

  const removeTabById = React.useCallback((tabId: string): void => {
    setTabs((prev) => {
      if (prev.length <= 1) {
        setActiveId('')
        return []
      }
      const idx = prev.findIndex((x) => x.id === tabId)
      if (idx < 0) return prev
      const next = prev.filter((x) => x.id !== tabId)
      const pick = next[Math.min(idx, next.length - 1)]!
      setActiveId(pick.id)
      return next
    })
  }, [])

  const requestCloseTab = React.useCallback(
    (tabId: string): void => {
      const t = tabsRef.current.find((x) => x.id === tabId)
      if (!t) return
      if (!isDirty(t)) {
        removeTabById(tabId)
        return
      }
      setClosePromptTabId(tabId)
    },
    [removeTabById]
  )

  const newTabAction = React.useCallback((): void => {
    setViewMode('edit')
    const nt = newTab()
    setTabs((prev) => [...prev, nt])
    setActiveId(nt.id)
  }, [])

  const closeTabAction = React.useCallback((): void => {
    requestCloseTab(activeId)
  }, [activeId, requestCloseTab])

  const tabNext = React.useCallback((): void => {
    if (tabs.length === 0) return
    const idx = tabs.findIndex((x) => x.id === activeId)
    const n = tabs[(idx + 1) % tabs.length]
    if (n) setActiveId(n.id)
  }, [activeId, tabs])

  const tabPrev = React.useCallback((): void => {
    if (tabs.length === 0) return
    const idx = tabs.findIndex((x) => x.id === activeId)
    const n = tabs[(idx - 1 + tabs.length) % tabs.length]
    if (n) setActiveId(n.id)
  }, [activeId, tabs])

  const cycleViewMode = React.useCallback((): void => {
    setViewMode((m) => (m === 'edit' ? 'read' : m === 'read' ? 'hybrid' : 'edit'))
  }, [])

  const startSidebarResize = React.useCallback((e: React.MouseEvent): void => {
    e.preventDefault()
    const startX = e.clientX
    const startW = sidebarWidth
    const onMove = (ev: MouseEvent): void => {
      setSidebarWidth(clamp(startW + (ev.clientX - startX), 160, 560))
    }
    const onUp = (): void => {
      document.removeEventListener('mousemove', onMove)
      document.removeEventListener('mouseup', onUp)
    }
    document.addEventListener('mousemove', onMove)
    document.addEventListener('mouseup', onUp)
  }, [sidebarWidth])

  const startHybridSplitResize = React.useCallback((e: React.MouseEvent): void => {
    e.preventDefault()
    const shell = (e.currentTarget as HTMLElement).closest('.hybrid-row')
    if (!shell) return
    const onMove = (ev: MouseEvent): void => {
      const rect = shell.getBoundingClientRect()
      const x = ev.clientX - rect.left
      const pct = clamp((x / Math.max(1, rect.width)) * 100, 22, 78)
      setSplitPct(pct)
    }
    const onUp = (): void => {
      document.removeEventListener('mousemove', onMove)
      document.removeEventListener('mouseup', onUp)
    }
    document.addEventListener('mousemove', onMove)
    document.addEventListener('mouseup', onUp)
  }, [])

  const startPreviewWidthResize = React.useCallback(
    (e: React.MouseEvent): void => {
      e.preventDefault()
      e.stopPropagation()
      const startX = e.clientX
      const startW = previewMaxWidth
      const onMove = (ev: MouseEvent): void => {
        setPreviewMaxWidth(clamp(startW + (ev.clientX - startX), 420, 1400))
      }
      const onUp = (): void => {
        document.removeEventListener('mousemove', onMove)
        document.removeEventListener('mouseup', onUp)
      }
      document.addEventListener('mousemove', onMove)
      document.addEventListener('mouseup', onUp)
    },
    [previewMaxWidth]
  )

  const handlersRef = React.useRef({
    openFileDialog: async (): Promise<void> => {},
    saveActive: async (): Promise<void> => {},
    saveActiveAs: async (): Promise<void> => {},
    newTabAction: (): void => {},
    closeTabAction: (): void => {},
    tabNext: (): void => {},
    tabPrev: (): void => {}
  })
  handlersRef.current = {
    openFileDialog,
    saveActive,
    saveActiveAs,
    newTabAction,
    closeTabAction,
    tabNext,
    tabPrev
  }

  React.useEffect(() => {
    if (!api) return
    const unsubClose = api.onRequestClose(() => {
      try {
        const anyDirty = tabsRef.current.some(isDirty)
        if (!anyDirty) {
          api.allowClose()
          return
        }
        const ok = window.confirm('有未保存的标签，确定退出并丢弃更改？')
        if (ok) api.allowClose()
      } catch (e) {
        console.error('[MyReader] request-close handler', e)
        api.allowClose()
      }
    })
    const unsubMenu = api.onMenuAction((action) => {
      void (async () => {
        try {
          const h = handlersRef.current
          switch (action) {
          case 'file-open':
            await h.openFileDialog()
            break
          case 'file-save':
            await h.saveActive()
            break
          case 'file-save-as':
            await h.saveActiveAs()
            break
          case 'tab-new':
            h.newTabAction()
            break
          case 'tab-close':
            h.closeTabAction()
            break
          case 'tab-next':
            h.tabNext()
            break
          case 'tab-prev':
            h.tabPrev()
            break
          case 'toggle-read':
            cycleViewMode()
            break
          case 'help-about':
            window.alert(
              " Huangzhijun's Reader 0.1 — 本地 Markdown 阅读与编辑 \n \n 作者：黄志军 \n 支持与协助(wx)：liaofan199404"
            )
            break
          default:
            break
        }
        } catch (e) {
          console.error('[MyReader] menu action', action, e)
        }
      })()
    })
    return () => {
      unsubClose()
      unsubMenu()
    }
  }, [api, cycleViewMode])

  if (!api) {
    return (
      <div className="app-root">
        <p className="no-api">浏览器文件接口未就绪，请刷新页面重试。</p>
      </div>
    )
  }

  const renderPreviewArticle = (articleRef?: React.Ref<HTMLElement>): React.ReactElement => (
    <PreviewArticle
      key={activeId || 'no-tab'}
      html={previewHtml}
      articleRef={articleRef}
      baseFilePath={activeTab?.filePath ?? null}
      resolveRelativePath={(base, href) => api.resolveRelativePath(base, href)}
      onOpenMarkdown={(path, anchorId) => openPathInTab(path, anchorId)}
      onOpenExternal={(url) => api.openExternal(url)}
    />
  )

  const closePromptTab = closePromptTabId
    ? tabsRef.current.find((t) => t.id === closePromptTabId)
    : undefined

  return (
    <div className="app-root">
      {closePromptTabId && closePromptTab ? (
        <div
          className="modal-overlay"
          role="presentation"
          onMouseDown={() => setClosePromptTabId(null)}
        >
          <div
            className="modal"
            role="dialog"
            aria-modal="true"
            onMouseDown={(e) => e.stopPropagation()}
          >
            <h3 className="modal-title">关闭标签</h3>
            <p className="modal-body">
              「{tabLabel(closePromptTab)}」有未保存的更改，是否保存？
            </p>
            <div className="modal-actions">
              <button
                type="button"
                className="primary"
                onClick={() => {
                  void (async () => {
                    const ok = await saveTabById(closePromptTabId)
                    if (!ok) return
                    removeTabById(closePromptTabId)
                    setClosePromptTabId(null)
                  })()
                }}
              >
                保存
              </button>
              <button
                type="button"
                onClick={() => {
                  removeTabById(closePromptTabId)
                  setClosePromptTabId(null)
                }}
              >
                不保存
              </button>
              <button type="button" onClick={() => setClosePromptTabId(null)}>
                取消
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {textPrompt ? (
        <div
          className="modal-overlay"
          role="presentation"
          onMouseDown={(e) => {
            if (e.target === e.currentTarget) {
              textPrompt.resolve(null)
              setTextPrompt(null)
            }
          }}
        >
          <div
            className="modal"
            role="dialog"
            aria-modal="true"
            onMouseDown={(e) => e.stopPropagation()}
          >
            <h3 className="modal-title">{textPrompt.title}</h3>
            {textPrompt.hint ? <p className="modal-body">{textPrompt.hint}</p> : null}
            <input
              type="text"
              className="modal-text-input"
              value={textPromptInput}
              onChange={(e) => setTextPromptInput(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Enter') {
                  e.preventDefault()
                  textPrompt.resolve(textPromptInput)
                  setTextPrompt(null)
                }
                if (e.key === 'Escape') {
                  e.preventDefault()
                  textPrompt.resolve(null)
                  setTextPrompt(null)
                }
              }}
              autoFocus
            />
            <div className="modal-actions">
              <button
                type="button"
                className="primary"
                onClick={() => {
                  textPrompt.resolve(textPromptInput)
                  setTextPrompt(null)
                }}
              >
                确定
              </button>
              <button
                type="button"
                onClick={() => {
                  textPrompt.resolve(null)
                  setTextPrompt(null)
                }}
              >
                取消
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {imageInsert ? (
        <div
          className="modal-overlay"
          role="presentation"
          onMouseDown={(e) => {
            if (e.target === e.currentTarget) {
              imageInsert.resolve(null)
              setImageInsert(null)
            }
          }}
        >
          <div
            className="modal modal--wide"
            role="dialog"
            aria-modal="true"
            onMouseDown={(e) => e.stopPropagation()}
          >
            <h3 className="modal-title">插入图片</h3>
            <p className="modal-hint">支持网络地址或本地图片文件（Markdown 中保存为 file:// 链接）。</p>
            <label className="modal-field">
              <span className="modal-label">图片地址</span>
              <input
                type="text"
                className="modal-text-input"
                placeholder="https://… 或选择本地文件"
                value={imageUrlInput}
                onChange={(e) => setImageUrlInput(e.target.value)}
              />
            </label>
            <div className="modal-row-btns">
              <button type="button" onClick={() => void pickLocalImage()}>
                浏览本地图片…
              </button>
            </div>
            <label className="modal-field">
              <span className="modal-label">替代文字</span>
              <input
                type="text"
                className="modal-text-input"
                value={imageAltInput}
                onChange={(e) => setImageAltInput(e.target.value)}
              />
            </label>
            <div className="modal-actions">
              <button
                type="button"
                className="primary"
                onClick={() => {
                  const url = imageUrlInput.trim()
                  if (!url) return
                  imageInsert.resolve({ url, alt: imageAltInput.trim() || '图片' })
                  setImageInsert(null)
                }}
              >
                插入
              </button>
              <button
                type="button"
                onClick={() => {
                  imageInsert.resolve(null)
                  setImageInsert(null)
                }}
              >
                取消
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {colorPicker ? (
        <div
          className="modal-overlay"
          role="presentation"
          onMouseDown={(e) => {
            if (e.target === e.currentTarget) {
              colorPicker.resolve(null)
              setColorPicker(null)
            }
          }}
        >
          <div
            className="modal modal--wide"
            role="dialog"
            aria-modal="true"
            onMouseDown={(e) => e.stopPropagation()}
          >
            <h3 className="modal-title">文字颜色</h3>
            <div className="color-picker-native">
              <span className="modal-label">取色器</span>
              <input
                type="color"
                className="color-picker-spectrum"
                value={/^#[0-9a-fA-F]{6}$/.test(colorHexInput) ? colorHexInput : '#dc2626'}
                onChange={(e) => setColorHexInput(e.target.value.toLowerCase())}
                aria-label="色谱"
              />
            </div>
            <label className="modal-field">
              <span className="modal-label">十六进制</span>
              <input
                type="text"
                className="modal-text-input"
                value={colorHexInput}
                onChange={(e) => setColorHexInput(e.target.value)}
                spellCheck={false}
              />
            </label>
            <div className="color-swatch-grid" role="list">
              {COLOR_SWATCH_ROWS.map((row, ri) => (
                <div key={ri} className="color-swatch-row" role="presentation">
                  {row.map((hex) => (
                    <button
                      key={hex + ri}
                      type="button"
                      className={`color-swatch ${colorHexInput.toLowerCase() === hex ? 'selected' : ''}`}
                      style={{ backgroundColor: hex }}
                      title={hex}
                      aria-label={hex}
                      onClick={() => setColorHexInput(hex)}
                    />
                  ))}
                </div>
              ))}
            </div>
            <div className="modal-actions">
              <button
                type="button"
                className="primary"
                onClick={() => {
                  colorPicker.resolve(colorHexInput.trim() || '#333333')
                  setColorPicker(null)
                }}
              >
                使用此颜色
              </button>
              <button
                type="button"
                onClick={() => {
                  colorPicker.resolve(null)
                  setColorPicker(null)
                }}
              >
                取消
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {webNotesOpen ? (
        <div
          className="modal-overlay"
          role="presentation"
          onMouseDown={() => setWebNotesOpen(false)}
        >
          <div
            className="modal modal--wide"
            role="dialog"
            aria-modal="true"
            aria-labelledby="web-notes-title"
            onMouseDown={(e) => e.stopPropagation()}
          >
            <h3 className="modal-title" id="web-notes-title">
              Web 版额外说明
            </h3>
            <div className="modal-body web-notes-body">
              <p>
                在浏览器中运行，读写的是您本机通过「打开 / 保存」授权的文件，不会访问服务器磁盘。需要完整本地编辑体验请使用工具栏「桌面版下载地址」。
              </p>
              <p>
                <strong>打开与保存</strong>：工具栏「打开」选择本地 Markdown；「保存」写回原文件（需浏览器支持
                File System Access API）；「另存为」可保存到新位置。推荐使用 Chrome / Edge 桌面版，并通过
                HTTPS 访问以获得完整能力。
              </p>
              <p>
                <strong>侧栏</strong>：仅显示当前文档大纲。Web 版不提供文件夹浏览与打开历史，请用工具栏「打开」或
                Ctrl+O 选择文件。
              </p>
              <p>
                <strong>图片与链接</strong>：本地 <code>file://</code> 图片受浏览器安全策略限制，可能无法显示；可改用
                网络图片地址，或将图片与文档放在同一目录后重新打开。文内相对链接在已打开文件范围内解析。
              </p>
              <p>
                <strong>快捷键</strong>：Ctrl+O 打开 · Ctrl+S 保存 · Ctrl+Shift+S 另存为 · Ctrl+T 新标签 ·
                Ctrl+W 关闭标签 · Ctrl+E 切换视图 · Ctrl+Tab / Ctrl+Shift+Tab 切换标签。
              </p>
              <p className="web-notes-footer">
                作者：黄志军 · 支持与协助(wx)：liaofan199404
              </p>
            </div>
            <div className="modal-actions">
              <button type="button" className="primary" onClick={() => setWebNotesOpen(false)}>
              知道了
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {desktopDownloadOpen ? (
        <div
          className="modal-overlay"
          role="presentation"
          onMouseDown={() => setDesktopDownloadOpen(false)}
        >
          <div
            className="modal modal--wide"
            role="dialog"
            aria-modal="true"
            aria-labelledby="desktop-download-title"
            onMouseDown={(e) => e.stopPropagation()}
          >
            <h3 className="modal-title" id="desktop-download-title">
              桌面版下载说明
            </h3>
            <div className="modal-body web-notes-body">
              <p>
                <strong>为什么推荐桌面版？</strong>
                Web 版在 HTTP 站点上无法直接写回您电脑上的原文件，保存时往往只能触发下载；文件夹浏览、稳定保存等能力也受浏览器限制。桌面版（Electron）在
                Windows 本地运行，可像普通编辑器一样打开、保存 Markdown，体验更接近完整版 MyReader。
              </p>
              <p>
                <strong>如何安装？</strong>
              </p>
              <ol className="web-notes-list">
                <li>点击下方链接下载 ZIP 压缩包；</li>
                <li>解压到任意目录（路径尽量不要含特殊字符）；</li>
                <li>进入解压目录，运行其中的可执行程序（一般为 <code>.exe</code>）；</li>
                <li>首次使用可通过菜单「打开」选择本地 <code>.md</code> 文件进行阅读与编辑。</li>
              </ol>
              <p>
                <strong>下载地址：</strong>
                <br />
                <a href={DESKTOP_DOWNLOAD_URL} target="_blank" rel="noopener noreferrer">
                  {DESKTOP_DOWNLOAD_URL}
                </a>
              </p>
              <p className="web-notes-muted">
                若链接无法打开，请检查网络或联系站点管理员。桌面版仅支持 Windows；Mac / Linux 请继续使用 Web 版。
              </p>
            </div>
            <div className="modal-actions">
              <a
                className="primary modal-link-btn"
                href={DESKTOP_DOWNLOAD_URL}
                target="_blank"
                rel="noopener noreferrer"
              >
                前往下载
              </a>
              <button type="button" onClick={() => setDesktopDownloadOpen(false)}>
                关闭
              </button>
            </div>
          </div>
        </div>
      ) : null}

      <header className="tab-bar">
        {tabs.map((t) => (
          <div key={t.id} className={`tab ${t.id === activeId ? 'active' : ''}`}>
            <button
              type="button"
              className="tab-main"
              onClick={() => {
                setActiveId(t.id)
              }}
            >
              <span className="tab-title">{tabLabel(t)}</span>
              {isDirty(t) ? <span className="dot">•</span> : null}
            </button>
            <button
              type="button"
              className="tab-close"
              aria-label="关闭标签"
              onClick={(e) => {
                e.stopPropagation()
                requestCloseTab(t.id)
              }}
            >
              ×
            </button>
          </div>
        ))}
      </header>

      <div className="toolbar">
        {saveToast ? (
            <div className="save-toast" role="status">
              {saveToast}
            </div>
        ) : null}
        <button type="button" onClick={() => void openFileDialog()}>
          打开
        </button>
        <button type="button" onClick={() => void saveActive()}>
          保存
        </button>
        <button type="button" onClick={() => void saveActiveAs()}>
          另存为
        </button>
        <button type="button" onClick={newTabAction}>
          新标签
        </button>
        <button type="button" onClick={cycleViewMode} title="在 编辑 / 阅读 / 分栏 间切换">
          视图：{viewModeLabel(viewMode)}
        </button>
        <button type="button" onClick={() => setSidebarHidden((v) => !v)}>
          {sidebarHidden ? '显示侧栏' : '隐藏侧栏'}
        </button>
        <button type="button" onClick={() => setWebNotesOpen(true)} title="Web 版使用说明">
          额外说明
        </button>
        <button
          type="button"
          onClick={() => setDesktopDownloadOpen(true)}
          title="查看桌面版下载说明与下载链接"
        >
          桌面版下载地址
        </button>
        <div className="toolbar-tail">
          <label className="bg-select">
            行距
            <input
                type="number"
                className="toolbar-mini-input"
                min={1}
                max={3}
                step={0.05}
                value={previewLineHeight}
                title="阅读视图行距（1–3）"
                onChange={(e) => setPreviewLineHeight(e.target.value)}
                onBlur={() => {
                  const n = Number.parseFloat(previewLineHeight)
                  if (!Number.isFinite(n)) {
                    setPreviewLineHeight('1.5')
                    return
                  }
                  setPreviewLineHeight(String(clamp(n, 1, 3)))
                }}
            />
          </label>
          <label className="bg-select">
            字体
            <select value={fontId} onChange={(e) => setFontId(e.target.value)}>
              {FONTS.map((t) => (
                  <option key={t.id} value={t.id}>
                    {t.name}
                  </option>
              ))}
            </select>
          </label>
          <label className="bg-select">
            背景
            <select value={bgId} onChange={(e) => setBgId(e.target.value)}>
              {BACKGROUNDS.map((t) => (
                  <option key={t.id} value={t.id}>
                    {t.name}
                  </option>
              ))}
            </select>
          </label>
        </div>
      </div>

      <div className="main">
        {!sidebarHidden ? (
            <>
              <aside className="sidebar" style={{ width: sidebarWidth, minWidth: sidebarWidth }}>
              <div className="file-pane outline-pane">
                <OutlinePanel items={outline} onSelect={navigateToOutline} />
              </div>
            </aside>
            <div
              className="sidebar-resize"
              onMouseDown={startSidebarResize}
              title="拖动调整侧栏宽度"
            />
          </>
        ) : (
          <button
            type="button"
            className="sidebar-reopen"
            title="显示侧栏"
            onClick={() => setSidebarHidden(false)}
          >
            ⟨
          </button>
        )}

        <section className="content">
          {(viewMode === 'edit' || viewMode === 'hybrid') && activeTab ? (
            <MarkdownFormatToolbar editorRef={editorRef} />
          ) : null}
          {viewMode === 'read' ? (
            activeTab ? (
              <div className="preview-outer" ref={readPreviewOuterRef}>
                <div className="preview-shell" style={{ maxWidth: previewMaxWidth }}>
                  {renderPreviewArticle(readPreviewArticleRef)}
                  <div
                    className="preview-resizer"
                    title="拖动调整阅读区宽度"
                    onMouseDown={startPreviewWidthResize}
                  />
                </div>
              </div>
            ) : (
              <div className="content-empty">
                请从侧栏选择文件、使用工具栏「打开」，或点击「新标签」开始编辑。
              </div>
            )
          ) : null}

          {viewMode === 'edit' ? (
            activeTab ? (
              <EditorPane
                ref={editorRef}
                mountKey={editorMountKey}
                value={activeTab.content}
                onChange={onEditorChange}
                gotoLine={gotoLine}
                showLineNumbers
                requestText={requestText}
                requestImageDetails={requestImageDetails}
                requestColor={requestColor}
              />
            ) : (
              <div className="content-empty">
                请从侧栏选择文件、使用工具栏「打开」，或点击「新标签」开始编辑。
              </div>
            )
          ) : null}

          {viewMode === 'hybrid' ? (
            activeTab ? (
              <div className="hybrid-row">
                <div className="hybrid-editor" style={{ flex: `0 0 ${splitPct}%` }}>
                  <EditorPane
                    ref={editorRef}
                    mountKey={`${editorMountKey}-hybrid`}
                    value={activeTab.content}
                    onChange={onEditorChange}
                    gotoLine={gotoLine}
                    showLineNumbers={false}
                    requestText={requestText}
                    requestImageDetails={requestImageDetails}
                    requestColor={requestColor}
                    onViewReady={onHybridViewReady}
                    onHybridCursorLine={onHybridCursorLine}
                  />
                </div>
                <div
                  className="hybrid-sash"
                  onMouseDown={startHybridSplitResize}
                  title="拖动调整分栏比例"
                />
                <div ref={hybridPreviewRef} className="hybrid-preview">
                  <div
                    ref={hybridPreviewShellRef}
                    className="preview-shell preview-shell--tight hybrid-preview-shell"
                    style={{ maxWidth: '100%' }}
                  >
                    {hybridLinePulse ? (
                      <div
                        key={hybridLinePulse.seq}
                        className="hybrid-line-pulse"
                        style={{ top: `${hybridLinePulse.topPx}px` }}
                        aria-hidden
                      />
                    ) : null}
                    {renderPreviewArticle(hybridPreviewArticleRef)}
                  </div>
                </div>
              </div>
            ) : (
              <div className="content-empty">
                请从侧栏选择文件、使用工具栏「打开」，或点击「新标签」开始编辑。
              </div>
            )
          ) : null}
        </section>
      </div>
    </div>
  )
}
