import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
  Button,
  Dropdown,
  Empty,
  Menu,
  Message,
  Progress,
  Spin,
  Tooltip,
} from '@arco-design/web-react'
import {
  IconClose,
  IconDelete,
  IconDownload,
  IconEdit,
  IconEye,
  IconFile,
  IconFileAudio,
  IconFileImage,
  IconFilePdf,
  IconFileVideo,
  IconFolder,
  IconFolderAdd,
  IconLeft,
  IconPlus,
  IconRight,
  IconSave,
  IconShareAlt,
  IconUpload,
} from '@arco-design/web-react/icon'
import ReactMarkdown from 'react-markdown'
import rehypeSanitize from 'rehype-sanitize'
import remarkGfm from 'remark-gfm'
import DOMPurify from 'dompurify'
import type { ApiSession } from '../../api/client'
import {
  createDocumentFolder,
  createDocumentShare,
  createMarkdownDocument,
  deleteDocument,
  downloadBlob,
  exportDocument,
  fetchDocumentBlob,
  fetchLegacySheetPreview,
  getDocument,
  listDocuments,
  updateDocument,
  uploadDocument,
  type DocumentDTO,
  type DocumentOpenIntent,
  type UploadControl,
} from '../../api/documents'
import type { Theme } from './project-types'
import { DocumentMarkdownEditor } from './document-markdown-editor'

type SaveStatus = 'clean' | 'dirty' | 'saving' | 'saved' | 'error'

type DocumentTab = {
  key: string
  documentId?: string
  parentId?: string
  name: string
  extension: string
  mimeType: string
  markdown: string
  revision: number
  status: SaveStatus
  mode: 'edit' | 'preview'
}

type UploadItem = {
  id: string
  file: File
  progress: number
  status: 'queued' | 'uploading' | 'done' | 'error' | 'cancelled'
  error?: string
}

type PersistedDocumentDraft = Pick<DocumentTab, 'key' | 'documentId' | 'parentId' | 'name' | 'extension' | 'mimeType' | 'markdown' | 'revision' | 'status' | 'mode'>

export function ProjectDocuments({
  session,
  projectId,
  theme,
  intent,
  onIntentConsumed,
}: {
  session: ApiSession
  projectId: string
  theme: Theme
  intent: DocumentOpenIntent | null
  onIntentConsumed: (intentId: string) => void
}) {
  const [documents, setDocuments] = useState<DocumentDTO[]>([])
  const [tabs, setTabs] = useState<DocumentTab[]>([])
  const [activeKey, setActiveKey] = useState('')
  const [expanded, setExpanded] = useState<Set<string>>(new Set())
  const [loading, setLoading] = useState(true)
  const [treeWidth, setTreeWidth] = useState(260)
  const [treeCollapsed, setTreeCollapsed] = useState(false)
  const [mobilePane, setMobilePane] = useState<'tree' | 'editor'>('tree')
  const [contextMenu, setContextMenu] = useState<{ x: number; y: number; document: DocumentDTO } | null>(null)
  const [uploads, setUploads] = useState<UploadItem[]>([])
  const tabsRef = useRef<DocumentTab[]>([])
  const timersRef = useRef(new Map<string, number>())
  const uploadControlsRef = useRef(new Map<string, UploadControl>())
  const fileInputRef = useRef<HTMLInputElement>(null)
  tabsRef.current = tabs

  const activeTab = tabs.find((tab) => tab.key === activeKey)
  const rootDocuments = useMemo(() => buildTree(documents), [documents])

  const refreshTree = useCallback(async () => {
    const result = await listDocuments(session, projectId)
    setDocuments(result)
    return result
  }, [projectId, session])

  const openDocument = useCallback(async (document: DocumentDTO) => {
    if (document.kind === 'folder') {
      setExpanded((current) => toggleSet(current, document.id))
      return
    }
    const existing = tabsRef.current.find((tab) => tab.documentId === document.id)
    if (existing) {
      setActiveKey(existing.key)
      setMobilePane('editor')
      return
    }
    try {
      const detail = await getDocument(session, projectId, document.id)
      const next = tabFromDocument(detail)
      setTabs((current) => [...current, next])
      setActiveKey(next.key)
      setMobilePane('editor')
    } catch (error) {
      Message.error(error instanceof Error ? error.message : '文档打开失败')
    }
  }, [projectId, session])

  const createDraft = useCallback((name = '新建文件1.md', markdown = '', parentId = '') => {
    const key = `draft:${crypto.randomUUID()}`
    const tab: DocumentTab = {
      key,
      parentId,
      name,
      extension: '.md',
      mimeType: 'text/markdown; charset=utf-8',
      markdown,
      revision: 0,
      status: markdown ? 'dirty' : 'clean',
      mode: 'edit',
    }
    setTabs((current) => [...current, tab])
    setActiveKey(key)
    setMobilePane('editor')
    return key
  }, [])

  useEffect(() => {
    let cancelled = false
    const timers = timersRef.current
    setTabs([])
    setActiveKey('')
    setLoading(true)
    void refreshTree().then(async (items) => {
      if (cancelled) return
      const stored = readStoredTabs(projectId)
      const drafts = readStoredDrafts(projectId)
      const draftByDocumentID = new Map(drafts.filter((draft) => draft.documentId).map((draft) => [draft.documentId!, draft]))
      const restored: DocumentTab[] = []
      for (const id of stored) {
        const item = items.find((document) => document.id === id && document.kind === 'file')
        if (!item) continue
        try {
          const detail = await getDocument(session, projectId, id)
          if (!cancelled) restored.push(restoreDocumentDraft(tabFromDocument(detail), draftByDocumentID.get(id)))
        } catch {
          // Missing tabs are discarded.
        }
      }
      restored.push(...drafts.filter((draft) => !draft.documentId))
      if (cancelled) return
      if (restored.length > 0) {
        setTabs(restored)
        setActiveKey(restored[0].key)
        setMobilePane('editor')
      } else {
        createDraft()
      }
    }).catch((error) => {
      if (!cancelled) Message.error(error instanceof Error ? error.message : '文档树加载失败')
    }).finally(() => {
      if (!cancelled) setLoading(false)
    })
    return () => {
      cancelled = true
      for (const timer of timers.values()) window.clearTimeout(timer)
      timers.clear()
    }
  }, [createDraft, projectId, refreshTree, session])

  useEffect(() => {
    const ids = tabs.flatMap((tab) => tab.documentId ? [tab.documentId] : [])
    localStorage.setItem(storageKey(projectId), JSON.stringify(ids))
    const drafts = tabs.filter((tab) => tab.status === 'dirty' || tab.status === 'saving' || tab.status === 'error')
    localStorage.setItem(draftStorageKey(projectId), JSON.stringify(drafts))
  }, [projectId, tabs])

  useEffect(() => {
    if (!intent || intent.projectId !== projectId) return
    if (intent.mode === 'open' && intent.documentId) {
      const document = documents.find((item) => item.id === intent.documentId)
      if (document) void openDocument(document)
    } else {
      createDraft(intent.suggestedName || '新建文件1.md', intent.initialMarkdown || '', intent.targetDirectoryId || '')
    }
    onIntentConsumed(intent.intentId)
  }, [createDraft, documents, intent, onIntentConsumed, openDocument, projectId])

  const persistTab = useCallback(async (key: string, forcedMarkdown?: string) => {
    const tab = tabsRef.current.find((item) => item.key === key)
    if (!tab || tab.extension !== '.md' || tab.status === 'saving') return
    const markdown = forcedMarkdown ?? tab.markdown
    setTabs((current) => patchTab(current, key, { status: 'saving' }))
    try {
      if (!tab.documentId) {
        const created = await createMarkdownDocument(session, projectId, { name: tab.name, parentId: tab.parentId, markdown })
        setDocuments((current) => [...current, created])
        setTabs((current) => patchTab(current, key, {
          documentId: created.id,
          key: created.id,
          name: created.name,
          revision: created.revision,
          status: 'saved',
        }))
        setActiveKey((current) => current === key ? created.id : current)
      } else {
        const updated = await updateDocument(session, projectId, tab.documentId, {
          markdown,
          revision: tab.revision,
        })
        setDocuments((current) => current.map((item) => item.id === updated.id ? updated : item))
        setTabs((current) => patchTab(current, key, {
          markdown: updated.markdown ?? markdown,
          revision: updated.revision,
          status: 'saved',
        }))
      }
    } catch (error) {
      setTabs((current) => patchTab(current, key, { status: 'error' }))
      Message.error(error instanceof Error ? error.message : '自动保存失败，本地草稿已保留')
    }
  }, [projectId, session])

  function changeMarkdown(key: string, markdown: string) {
    setTabs((current) => patchTab(current, key, { markdown, status: 'dirty' }))
    const previous = timersRef.current.get(key)
    if (previous) window.clearTimeout(previous)
    timersRef.current.set(key, window.setTimeout(() => {
      timersRef.current.delete(key)
      void persistTab(key, markdown)
    }, 800))
  }

  async function addFolder(parentId = '') {
    const name = window.prompt('目录名称')
    if (!name?.trim()) return
    try {
      const created = await createDocumentFolder(session, projectId, name.trim(), parentId)
      setDocuments((current) => [...current, created])
    } catch (error) {
      Message.error(error instanceof Error ? error.message : '目录创建失败')
    }
  }

  async function renameNode(document: DocumentDTO) {
    const name = window.prompt('重命名', document.name)
    if (!name?.trim() || name.trim() === document.name) return
    try {
      const updated = await updateDocument(session, projectId, document.id, { name: name.trim() })
      setDocuments((current) => current.map((item) => item.id === updated.id ? updated : item))
      setTabs((current) => current.map((tab) => tab.documentId === updated.id ? { ...tab, name: updated.name } : tab))
    } catch (error) {
      Message.error(error instanceof Error ? error.message : '重命名失败')
    }
  }

  async function removeNode(document: DocumentDTO) {
    if (!window.confirm(`确定删除“${document.name}”${document.kind === 'folder' ? '及其全部内容' : ''}吗？`)) return
    try {
      await deleteDocument(session, projectId, document.id)
      const removed = descendantIds(documents, document.id)
      setDocuments((current) => current.filter((item) => !removed.has(item.id)))
      setTabs((current) => current.filter((tab) => !tab.documentId || !removed.has(tab.documentId)))
      if (activeTab?.documentId && removed.has(activeTab.documentId)) setActiveKey('')
    } catch (error) {
      Message.error(error instanceof Error ? error.message : '删除失败')
    }
  }

  async function moveNode(documentId: string, parentId: string) {
    const document = documents.find((item) => item.id === documentId)
    if (!document || document.parentId === (parentId || null)) return
    try {
      const updated = await updateDocument(session, projectId, documentId, { parentId })
      setDocuments((current) => current.map((item) => item.id === updated.id ? updated : item))
    } catch (error) {
      Message.error(error instanceof Error ? error.message : '移动失败')
    }
  }

  function enqueueFiles(files: FileList | File[]) {
    const accepted = Array.from(files).filter((file) => {
      if (file.size <= 50 * 1024 * 1024) return true
      Message.error(`${file.name} 超过 50 MB，未加入上传队列`)
      return false
    })
    setUploads((current) => [
      ...current,
      ...accepted.map((file) => ({
        id: crypto.randomUUID(),
        file,
        progress: 0,
        status: 'queued' as const,
      })),
    ])
  }

  useEffect(() => {
    const running = uploads.filter((item) => item.status === 'uploading').length
    const queued = uploads.filter((item) => item.status === 'queued').slice(0, Math.max(0, 3 - running))
    for (const item of queued) {
      setUploads((current) => current.map((candidate) => candidate.id === item.id ? { ...candidate, status: 'uploading' } : candidate))
      const control = uploadDocument(session, projectId, item.file, '', (progress) => {
        setUploads((current) => current.map((candidate) => candidate.id === item.id ? { ...candidate, progress } : candidate))
      })
      uploadControlsRef.current.set(item.id, control)
      void control.promise.then((document) => {
        setDocuments((current) => [...current, document])
        setUploads((current) => current.map((candidate) => candidate.id === item.id ? { ...candidate, progress: 100, status: 'done' } : candidate))
        void openDocument(document)
      }).catch((error) => {
        setUploads((current) => current.map((candidate) => candidate.id === item.id
          ? { ...candidate, status: 'error', error: error instanceof Error ? error.message : '上传失败' }
          : candidate))
      }).finally(() => {
        uploadControlsRef.current.delete(item.id)
        window.setTimeout(() => {
          setUploads((current) => current.filter((candidate) => candidate.id !== item.id || candidate.status !== 'done'))
        }, 1800)
      })
    }
  }, [openDocument, projectId, session, uploads])

  function closeTab(tab: DocumentTab) {
    if ((tab.status === 'dirty' || tab.status === 'error') && !window.confirm(`“${tab.name}”尚未成功保存，仍要关闭吗？`)) return
    const index = tabs.findIndex((item) => item.key === tab.key)
    const next = tabs.filter((item) => item.key !== tab.key)
    if (tab.key !== activeKey) {
      setTabs(next)
      return
    }
    const replacement = next[Math.max(0, index - 1)]
    if (replacement) {
      setTabs(next)
      setActiveKey(replacement.key)
      return
    }
    const key = `draft:${crypto.randomUUID()}`
    setTabs([{
      key,
      name: '新建文件1.md',
      extension: '.md',
      mimeType: 'text/markdown; charset=utf-8',
      markdown: '',
      revision: 0,
      status: 'clean',
      mode: 'edit',
    }])
    setActiveKey(key)
  }

  async function downloadActive() {
    if (!activeTab?.documentId) return
    try {
      const blob = await fetchDocumentBlob(session, projectId, activeTab.documentId, true)
      downloadBlob(blob, activeTab.name)
    } catch (error) {
      Message.error(error instanceof Error ? error.message : '下载失败')
    }
  }

  async function shareActive(days: 1 | 7 | 30) {
    if (!activeTab?.documentId) {
      Message.warning('请先保存文档')
      return
    }
    try {
      const share = await createDocumentShare(session, projectId, activeTab.documentId, days)
      if (share.url) {
        try {
          await navigator.clipboard.writeText(share.url)
          Message.success(`${days} 天有效的预览链接已复制`)
        } catch {
          window.prompt('复制分享链接', share.url)
        }
      }
    } catch (error) {
      Message.error(error instanceof Error ? error.message : '分享链接创建失败')
    }
  }

  function beginResize(event: React.MouseEvent) {
    const startX = event.clientX
    const startWidth = treeWidth
    function move(moveEvent: MouseEvent) {
      setTreeWidth(Math.min(420, Math.max(210, startWidth + moveEvent.clientX - startX)))
    }
    function finish() {
      window.removeEventListener('mousemove', move)
      window.removeEventListener('mouseup', finish)
    }
    window.addEventListener('mousemove', move)
    window.addEventListener('mouseup', finish)
  }

  const uploadProgress = uploads.length === 0
    ? 0
    : Math.round(uploads.reduce((sum, item) => sum + item.progress, 0) / uploads.length)

  return (
    <section
      className={`documents-canvas mobile-${mobilePane}`}
      onClick={() => setContextMenu(null)}
      onDragOver={(event) => event.preventDefault()}
      onDrop={(event) => {
        event.preventDefault()
        if (event.dataTransfer.files.length > 0) enqueueFiles(event.dataTransfer.files)
      }}
    >
      {!treeCollapsed && (
        <aside className="documents-tree-pane" style={{ width: treeWidth }}>
          <div className="documents-tree-tools">
            <Tooltip content="新建目录"><Button type="text" icon={<IconFolderAdd />} onClick={() => void addFolder()} /></Tooltip>
            <Tooltip content="新建 Markdown"><Button type="text" icon={<IconPlus />} onClick={() => createDraft()} /></Tooltip>
            <span className="documents-tree-spacer" />
            <Tooltip content="上传文件"><Button type="text" icon={<IconUpload />} onClick={() => fileInputRef.current?.click()} /></Tooltip>
            <Tooltip content="折叠文件树"><Button type="text" icon={<IconLeft />} onClick={() => setTreeCollapsed(true)} /></Tooltip>
            <input ref={fileInputRef} hidden multiple type="file" onChange={(event) => event.target.files && enqueueFiles(event.target.files)} />
          </div>
          <div
            className="documents-tree"
            onDragOver={(event) => event.preventDefault()}
            onDrop={(event) => {
              const id = event.dataTransfer.getData('application/x-senlin-document')
              if (id) {
                event.stopPropagation()
                void moveNode(id, '')
              }
            }}
          >
            {loading ? <Spin /> : rootDocuments.length === 0 ? <Empty description="暂无文件" /> : rootDocuments.map((node) => (
              <TreeNode
                key={node.document.id}
                node={node}
                depth={0}
                expanded={expanded}
                activeDocumentId={activeTab?.documentId}
                onToggle={(id) => setExpanded((current) => toggleSet(current, id))}
                onOpen={(document) => void openDocument(document)}
                onMove={(id, parent) => void moveNode(id, parent)}
                onContextMenu={(event, document) => {
                  event.preventDefault()
                  setContextMenu({ x: event.clientX, y: event.clientY, document })
                }}
              />
            ))}
          </div>
          {uploads.length > 0 && (
            <div className="document-upload-summary">
              <div><span>上传 {uploads.filter((item) => item.status === 'done').length}/{uploads.length}</span><span>{uploadProgress}%</span></div>
              <Progress percent={uploadProgress} showText={false} size="small" />
              {uploads.filter((item) => item.status === 'uploading' || item.status === 'error').map((item) => (
                <div className="document-upload-row" key={item.id}>
                  <span title={item.error}>{item.file.name}</span>
                  <span>{item.status === 'error' ? '失败' : `${item.progress}%`}</span>
                  {item.status === 'uploading' ? (
                    <button onClick={() => uploadControlsRef.current.get(item.id)?.cancel()}>取消</button>
                  ) : (
                    <button onClick={() => setUploads((current) => current.map((candidate) => candidate.id === item.id
                      ? { ...candidate, progress: 0, status: 'queued', error: undefined }
                      : candidate))}>重试</button>
                  )}
                </div>
              ))}
            </div>
          )}
        </aside>
      )}
      {!treeCollapsed && <div className="documents-resizer" onMouseDown={beginResize} />}

      <main className="documents-editor-pane">
        <div className="documents-tabs-bar">
          {treeCollapsed && <Button type="text" icon={<IconRight />} onClick={() => setTreeCollapsed(false)} />}
          <div className="documents-tabs">
            {tabs.map((tab) => (
              <button className={tab.key === activeKey ? 'document-tab active' : 'document-tab'} key={tab.key} onClick={() => setActiveKey(tab.key)}>
                {fileIcon(tab.extension, tab.mimeType)}
                <span>{tab.name}</span>
                <i className={`save-dot ${tab.status}`} title={saveLabel(tab.status)} />
                <IconClose onClick={(event) => { event.stopPropagation(); closeTab(tab) }} />
              </button>
            ))}
          </div>
          {activeTab && (
            <div className="documents-tab-actions">
              {activeTab.extension === '.md' && (
                <Tooltip content={activeTab.mode === 'edit' ? '预览' : '编辑'}>
                  <Button
                    type="text"
                    icon={activeTab.mode === 'edit' ? <IconEye /> : <IconEdit />}
                    onClick={() => setTabs((current) => patchTab(current, activeTab.key, { mode: activeTab.mode === 'edit' ? 'preview' : 'edit' }))}
                  />
                </Tooltip>
              )}
              {activeTab.extension === '.md' && (
                <Tooltip content="保存"><Button type="text" icon={<IconSave />} onClick={() => void persistTab(activeTab.key)} /></Tooltip>
              )}
              {activeTab.documentId && activeTab.extension === '.md' ? (
                <Dropdown
                  trigger="click"
                  droplist={<Menu onClickMenuItem={(format: string) => void exportDocument(session, projectId, activeTab.documentId!, format as 'md' | 'pdf' | 'docx')}>
                    <Menu.Item key="md">导出 Markdown</Menu.Item>
                    <Menu.Item key="pdf">导出 PDF</Menu.Item>
                    <Menu.Item key="docx">导出 Word</Menu.Item>
                  </Menu>}
                >
                  <Button type="text" icon={<IconDownload />} />
                </Dropdown>
              ) : activeTab.documentId ? (
                <Tooltip content="下载原文件"><Button type="text" icon={<IconDownload />} onClick={() => void downloadActive()} /></Tooltip>
              ) : null}
              <Dropdown
                trigger="click"
                droplist={<Menu onClickMenuItem={(days) => void shareActive(Number(days) as 1 | 7 | 30)}>
                  <Menu.Item key="1">有效期 1 天</Menu.Item>
                  <Menu.Item key="7">有效期 7 天</Menu.Item>
                  <Menu.Item key="30">有效期 30 天</Menu.Item>
                </Menu>}
              >
                <Tooltip content="分享预览"><Button type="text" icon={<IconShareAlt />} /></Tooltip>
              </Dropdown>
            </div>
          )}
        </div>

        <div className="documents-mobile-switch">
          <Button icon={<IconLeft />} onClick={() => setMobilePane('tree')}>文件</Button>
        </div>

        <div className="document-editor-body">
          {!activeTab ? <Empty description="没有打开的文件" /> : activeTab.extension === '.md' && activeTab.mode === 'edit' ? (
            <DocumentMarkdownEditor
              value={activeTab.markdown}
              dark={theme === 'dark'}
              onChange={(value) => changeMarkdown(activeTab.key, value)}
              onSave={() => void persistTab(activeTab.key)}
            />
          ) : activeTab.extension === '.md' ? (
            <MarkdownPreview markdown={activeTab.markdown} />
          ) : activeTab.documentId ? (
            <SmartPreview session={session} projectId={projectId} tab={activeTab} />
          ) : null}
        </div>
      </main>

      {contextMenu && (
        <div className="document-context-menu" style={{ left: contextMenu.x, top: contextMenu.y }}>
          {contextMenu.document.kind === 'folder' && (
            <>
              <button onClick={() => void addFolder(contextMenu.document.id)}><IconFolderAdd />新建子目录</button>
              <button onClick={() => createDraft('新建文件1.md', '', contextMenu.document.id)}><IconPlus />新建 Markdown</button>
            </>
          )}
          <button onClick={() => void renameNode(contextMenu.document)}><IconEdit />重命名</button>
          <button className="danger" onClick={() => void removeNode(contextMenu.document)}><IconDelete />删除</button>
        </div>
      )}
    </section>
  )
}

type TreeEntry = { document: DocumentDTO; children: TreeEntry[] }

function TreeNode({
  node,
  depth,
  expanded,
  activeDocumentId,
  onToggle,
  onOpen,
  onMove,
  onContextMenu,
}: {
  node: TreeEntry
  depth: number
  expanded: Set<string>
  activeDocumentId?: string
  onToggle: (id: string) => void
  onOpen: (document: DocumentDTO) => void
  onMove: (documentId: string, parentId: string) => void
  onContextMenu: (event: React.MouseEvent, document: DocumentDTO) => void
}) {
  const { document } = node
  const open = expanded.has(document.id)
  return (
    <>
      <button
        className={document.id === activeDocumentId ? 'document-tree-row active' : 'document-tree-row'}
        style={{ paddingLeft: 8 + depth * 16 }}
        draggable
        onDragStart={(event) => event.dataTransfer.setData('application/x-senlin-document', document.id)}
        onDragOver={(event) => { if (document.kind === 'folder') event.preventDefault() }}
        onDrop={(event) => {
          if (document.kind !== 'folder') return
          event.preventDefault()
          event.stopPropagation()
          const id = event.dataTransfer.getData('application/x-senlin-document')
          if (id) onMove(id, document.id)
        }}
        onContextMenu={(event) => onContextMenu(event, document)}
        onClick={() => document.kind === 'folder' ? onToggle(document.id) : onOpen(document)}
        onDoubleClick={() => onOpen(document)}
      >
        {document.kind === 'folder' ? <span className="tree-chevron">{open ? '⌄' : '›'}</span> : <span className="tree-chevron" />}
        {document.kind === 'folder' ? <IconFolder /> : fileIcon(document.extension, document.mimeType)}
        <span>{document.name}</span>
      </button>
      {document.kind === 'folder' && open && node.children.map((child) => (
        <TreeNode
          key={child.document.id}
          node={child}
          depth={depth + 1}
          expanded={expanded}
          activeDocumentId={activeDocumentId}
          onToggle={onToggle}
          onOpen={onOpen}
          onMove={onMove}
          onContextMenu={onContextMenu}
        />
      ))}
    </>
  )
}

function MarkdownPreview({ markdown }: { markdown: string }) {
  return (
    <article className="document-markdown-preview">
      <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSanitize]}>{markdown}</ReactMarkdown>
    </article>
  )
}

function SmartPreview({ session, projectId, tab }: { session: ApiSession; projectId: string; tab: DocumentTab }) {
  const [url, setUrl] = useState('')
  const [text, setText] = useState('')
  const [html, setHtml] = useState('')
  const [rows, setRows] = useState<unknown[][]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState('')

  useEffect(() => {
    let active = true
    let objectURL = ''
    setLoading(true)
    setError('')
    const extension = tab.extension.toLowerCase()
    if (extension === '.xls') {
      void fetchLegacySheetPreview(session, projectId, tab.documentId!).then((preview) => {
        if (active) setRows(preview.rows)
      }).catch((reason) => {
        if (active) setError(reason instanceof Error ? reason.message : '预览加载失败')
      }).finally(() => {
        if (active) setLoading(false)
      })
      return () => {
        active = false
      }
    }
    void fetchDocumentBlob(session, projectId, tab.documentId!).then(async (blob) => {
      if (!active) return
      if (isText(extension, tab.mimeType)) {
        setText(await blob.text())
      } else if (extension === '.docx') {
        const mammoth = await import('mammoth')
        const result = await mammoth.default.convertToHtml({ arrayBuffer: await blob.arrayBuffer() })
        if (active) setHtml(DOMPurify.sanitize(result.value))
      } else if (extension === '.xlsx') {
        const { readSheet } = await import('read-excel-file/browser')
        const parsed = await readSheet(blob)
        if (active) setRows(parsed as unknown[][])
      } else {
        const previewBlob = extension === '.svg'
          ? new Blob([DOMPurify.sanitize(await blob.text(), { USE_PROFILES: { svg: true, svgFilters: true } })], { type: 'image/svg+xml' })
          : blob
        objectURL = URL.createObjectURL(previewBlob)
        setUrl(objectURL)
      }
    }).catch((reason) => {
      if (active) setError(reason instanceof Error ? reason.message : '预览加载失败')
    }).finally(() => {
      if (active) setLoading(false)
    })
    return () => {
      active = false
      if (objectURL) URL.revokeObjectURL(objectURL)
    }
  }, [projectId, session, tab.documentId, tab.extension, tab.mimeType])

  if (loading) return <Spin />
  if (error) return <Empty description={error} />
  if (tab.mimeType.startsWith('image/')) return <div className="document-media-preview"><img src={url} alt={tab.name} /></div>
  if (tab.mimeType.startsWith('audio/')) return <div className="document-media-preview"><audio src={url} controls /></div>
  if (tab.mimeType.startsWith('video/')) return <div className="document-media-preview"><video src={url} controls /></div>
  if (tab.extension === '.pdf') return <iframe className="document-pdf-preview" src={url} title={tab.name} />
  if (tab.extension === '.docx') return <article className="document-office-preview" dangerouslySetInnerHTML={{ __html: html }} />
  if (tab.extension === '.xlsx' || tab.extension === '.xls') return (
    <div className="document-sheet-preview"><table><tbody>{rows.map((row, rowIndex) => (
      <tr key={rowIndex}>{row.map((cell, cellIndex) => <td key={cellIndex}>{String(cell ?? '')}</td>)}</tr>
    ))}</tbody></table></div>
  )
  if (text) return <pre className="document-text-preview">{text}</pre>
  return <Empty description="此格式暂不支持在线预览，可使用右上角下载" />
}

function buildTree(documents: DocumentDTO[]) {
  const entries = new Map<string, TreeEntry>()
  for (const document of documents) entries.set(document.id, { document, children: [] })
  const roots: TreeEntry[] = []
  for (const entry of entries.values()) {
    const parent = entry.document.parentId ? entries.get(entry.document.parentId) : null
    if (parent) parent.children.push(entry)
    else roots.push(entry)
  }
  const sort = (items: TreeEntry[]) => {
    items.sort((left, right) => {
      if (left.document.kind !== right.document.kind) return left.document.kind === 'folder' ? -1 : 1
      return left.document.name.localeCompare(right.document.name, 'zh-CN', { numeric: true, sensitivity: 'base' })
    })
    items.forEach((item) => sort(item.children))
  }
  sort(roots)
  return roots
}

function tabFromDocument(document: DocumentDTO): DocumentTab {
  return {
    key: document.id,
    documentId: document.id,
    parentId: document.parentId ?? '',
    name: document.name,
    extension: document.extension.toLowerCase(),
    mimeType: document.mimeType,
    markdown: document.markdown ?? '',
    revision: document.revision,
    status: 'clean',
    mode: document.extension.toLowerCase() === '.md' ? 'edit' : 'preview',
  }
}

function patchTab(tabs: DocumentTab[], key: string, patch: Partial<DocumentTab>) {
  return tabs.map((tab) => tab.key === key ? { ...tab, ...patch } : tab)
}

function toggleSet(current: Set<string>, value: string) {
  const next = new Set(current)
  if (next.has(value)) next.delete(value)
  else next.add(value)
  return next
}

function descendantIds(documents: DocumentDTO[], root: string) {
  const ids = new Set([root])
  let changed = true
  while (changed) {
    changed = false
    for (const document of documents) {
      if (document.parentId && ids.has(document.parentId) && !ids.has(document.id)) {
        ids.add(document.id)
        changed = true
      }
    }
  }
  return ids
}

function storageKey(projectId: string) {
  return `senlin:documents:tabs:${projectId}`
}

function draftStorageKey(projectId: string) {
  return `senlin:documents:drafts:${projectId}`
}

function readStoredTabs(projectId: string): string[] {
  try {
    const value: unknown = JSON.parse(localStorage.getItem(storageKey(projectId)) ?? '[]')
    return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
  } catch {
    return []
  }
}

function readStoredDrafts(projectId: string): PersistedDocumentDraft[] {
  try {
    const value: unknown = JSON.parse(localStorage.getItem(draftStorageKey(projectId)) ?? '[]')
    if (!Array.isArray(value)) return []
    return value.filter(isPersistedDocumentDraft)
  } catch {
    return []
  }
}

function isPersistedDocumentDraft(value: unknown): value is PersistedDocumentDraft {
  if (!value || typeof value !== 'object') return false
  const draft = value as Partial<PersistedDocumentDraft>
  return typeof draft.key === 'string' && typeof draft.name === 'string' && typeof draft.extension === 'string' &&
    typeof draft.mimeType === 'string' && typeof draft.markdown === 'string' && typeof draft.revision === 'number' &&
    (draft.status === 'dirty' || draft.status === 'saving' || draft.status === 'error') &&
    (draft.mode === 'edit' || draft.mode === 'preview') &&
    (draft.documentId === undefined || typeof draft.documentId === 'string') &&
    (draft.parentId === undefined || typeof draft.parentId === 'string')
}

function restoreDocumentDraft(tab: DocumentTab, draft?: PersistedDocumentDraft): DocumentTab {
  if (!draft) return tab
  return { ...tab, markdown: draft.markdown, revision: draft.revision, status: draft.status === 'saving' ? 'dirty' : draft.status, mode: draft.mode }
}

function saveLabel(status: SaveStatus) {
  return { clean: '已保存', dirty: '未保存', saving: '保存中', saved: '已保存', error: '保存失败' }[status]
}

function fileIcon(extension: string, mimeType: string) {
  if (extension === '.pdf') return <IconFilePdf />
  if (mimeType.startsWith('image/')) return <IconFileImage />
  if (mimeType.startsWith('audio/')) return <IconFileAudio />
  if (mimeType.startsWith('video/')) return <IconFileVideo />
  return <IconFile />
}

function isText(extension: string, mimeType: string) {
  return mimeType.startsWith('text/') || [
    '.txt', '.json', '.yaml', '.yml', '.xml', '.csv', '.ts', '.tsx', '.js', '.jsx', '.go', '.py', '.css', '.html',
  ].includes(extension)
}
