import { useCallback, useEffect, useRef, useState } from 'react'
import { EditorView, basicSetup } from 'codemirror'
import { Compartment, EditorState, Prec } from '@codemirror/state'
import { keymap } from '@codemirror/view'
import { LanguageDescription } from '@codemirror/language'
import { languages } from '@codemirror/language-data'
import { oneDark } from '@codemirror/theme-one-dark'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import { clipboardSetText, fsReadPreview, fsWriteText, onFsChanged, revealInExplorer } from '../ipc'
import type { FilePreviewData } from '../types'
import { ConfirmDialog } from './Modal'

interface Props {
  path: string
  dark: boolean
  onDirtyChange: (dirty: boolean) => void
}

function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}

const langCompartment = new Compartment()
const themeCompartment = new Compartment()

/** 与应用配色对齐的编辑器基础样式（深色另叠加 oneDark 高亮） */
function baseTheme(dark: boolean) {
  return EditorView.theme(
    {
      '&': { backgroundColor: 'var(--term-bg)', fontSize: '13px', height: '100%' },
      '.cm-content': { fontFamily: 'var(--font-mono)' },
      '.cm-gutters': {
        backgroundColor: 'var(--bg-inset)',
        color: 'var(--text-faint)',
        border: 'none'
      },
      '&.cm-focused': { outline: 'none' }
    },
    { dark }
  )
}

/**
 * 文件预览/编辑：文本走 CodeMirror（按扩展名语法高亮），
 * Markdown 默认渲染、可切到源码编辑，图片直接显示。
 */
export function FilePreview({ path, dark, onDirtyChange }: Props) {
  const [data, setData] = useState<FilePreviewData | null>(null)
  const [error, setError] = useState('')
  const [dirty, setDirty] = useState(false)
  const [mdMode, setMdMode] = useState<'render' | 'edit'>('render')
  const [externalChanged, setExternalChanged] = useState(false)
  const [conflict, setConflict] = useState(false)
  const [copied, setCopied] = useState(false)

  const hostRef = useRef<HTMLDivElement>(null)
  const viewRef = useRef<EditorView | null>(null)
  const mtimeRef = useRef(0)
  const dirtyRef = useRef(false)
  dirtyRef.current = dirty
  const dataRef = useRef<FilePreviewData | null>(null)
  dataRef.current = data

  const setDirtyBoth = useCallback(
    (d: boolean) => {
      setDirty(d)
      onDirtyChange(d)
    },
    [onDirtyChange]
  )

  const load = useCallback(async () => {
    setError('')
    try {
      const d = await fsReadPreview(path)
      mtimeRef.current = d.mtimeMs
      setData(d)
      setDirtyBoth(false)
      setExternalChanged(false)
      setMdMode('render')
    } catch (e) {
      setData(null)
      setError(String(e))
    }
  }, [path, setDirtyBoth])

  useEffect(() => {
    void load()
  }, [load])

  // 外部修改：未编辑时自动重载，已编辑时显示提醒条
  useEffect(() => {
    let unlisten: (() => void) | null = null
    void onFsChanged(() => {
      if (dirtyRef.current) setExternalChanged(true)
      else void load()
    }).then((fn) => {
      unlisten = fn
    })
    return () => unlisten?.()
  }, [load])

  const save = useCallback(
    async (force: boolean) => {
      // 编辑器存在时取实时文档；渲染模式下取切换时同步过的内容
      const content = viewRef.current?.state.doc.toString() ?? dataRef.current?.content
      if (content === undefined) return
      try {
        const newMtime = await fsWriteText(path, content, force ? 0 : mtimeRef.current)
        mtimeRef.current = newMtime
        setDirtyBoth(false)
        setExternalChanged(false)
        setConflict(false)
        setData((d) => (d ? { ...d, content, mtimeMs: newMtime } : d))
      } catch (e) {
        if (String(e).includes('conflict')) setConflict(true)
        else setError(String(e))
      }
    },
    [path, setDirtyBoth]
  )
  const saveRef = useRef(save)
  saveRef.current = save

  const isEditorVisible =
    data && (data.kind === 'text' || (data.kind === 'markdown' && mdMode === 'edit'))

  // 创建/销毁 CodeMirror（文件内容或可见性变化时重建）
  useEffect(() => {
    const host = hostRef.current
    if (!host || !isEditorVisible || !data) return

    const view = new EditorView({
      parent: host,
      state: EditorState.create({
        doc: data.content,
        extensions: [
          Prec.high(
            keymap.of([
              {
                key: 'Mod-s',
                run: () => {
                  void saveRef.current(false)
                  return true
                }
              }
            ])
          ),
          basicSetup,
          langCompartment.of([]),
          themeCompartment.of(dark ? [baseTheme(true), oneDark] : [baseTheme(false)]),
          EditorView.updateListener.of((update) => {
            if (update.docChanged) setDirtyBoth(true)
          }),
          EditorView.lineWrapping
        ]
      })
    })
    viewRef.current = view

    // 按文件名匹配语言包（按需异步加载）
    const filename = path.split('\\').pop() ?? ''
    const desc = LanguageDescription.matchFilename(languages, filename)
    if (desc) {
      void desc.load().then((lang) => {
        if (viewRef.current === view) {
          view.dispatch({ effects: langCompartment.reconfigure(lang) })
        }
      })
    }

    return () => {
      viewRef.current = null
      view.destroy()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data, isEditorVisible])

  // 主题切换
  useEffect(() => {
    viewRef.current?.dispatch({
      effects: themeCompartment.reconfigure(dark ? [baseTheme(true), oneDark] : [baseTheme(false)])
    })
  }, [dark])

  const filename = path.split('\\').pop() ?? path

  /** Markdown 渲染：marked 转 HTML 后经 DOMPurify 净化 */
  const renderMarkdown = (src: string): string =>
    DOMPurify.sanitize(marked.parse(src, { async: false }))

  return (
    <div className="file-preview">
      <div className="preview-toolbar">
        <span className="preview-name" title={path}>
          {filename}
        </span>
        {data && <span className="badge">{formatSize(data.size)}</span>}
        {data?.kind === 'markdown' && (
          <div className="preview-mode">
            <button
              className={`sidebar-tab ${mdMode === 'render' ? 'active' : ''}`}
              onClick={() => {
                // 切回渲染时带上编辑器中未保存的内容
                const view = viewRef.current
                if (view) {
                  const doc = view.state.doc.toString()
                  setData((d) => (d ? { ...d, content: doc } : d))
                }
                setMdMode('render')
              }}
            >
              渲染
            </button>
            <button
              className={`sidebar-tab ${mdMode === 'edit' ? 'active' : ''}`}
              onClick={() => setMdMode('edit')}
            >
              编辑
            </button>
          </div>
        )}
        <div className="spacer" style={{ flex: 1 }} />
        {(isEditorVisible || (data?.kind === 'markdown' && dirty)) && (
          <button className="btn small primary" disabled={!dirty} onClick={() => void save(false)}>
            保存 (Ctrl+S)
          </button>
        )}
        <button
          className="btn small"
          onClick={async () => {
            await clipboardSetText(path)
            setCopied(true)
            setTimeout(() => setCopied(false), 1500)
          }}
        >
          {copied ? '已复制' : '复制路径'}
        </button>
        <button className="btn small" onClick={() => void revealInExplorer(path)}>
          所在目录
        </button>
      </div>

      {externalChanged && (
        <div className="preview-warn">
          文件已被外部修改。
          <button className="btn small" style={{ marginLeft: 8 }} onClick={() => void load()}>
            放弃我的修改并重载
          </button>
        </div>
      )}
      {error && <div className="preview-warn danger">{error}</div>}

      <div className="preview-body">
        {!data && !error && <div className="empty-hint">加载中…</div>}
        {data?.kind === 'image' && (
          <div className="preview-image">
            <img src={`data:${data.mime};base64,${data.content}`} alt={filename} />
          </div>
        )}
        {(data?.kind === 'binary' || data?.kind === 'toolarge') && (
          <div className="empty-hint">
            {data.kind === 'binary' ? '二进制文件，不支持预览' : '文件过大，不支持预览'}（
            {formatSize(data.size)}）
          </div>
        )}
        {data?.kind === 'markdown' && mdMode === 'render' && (
          <div
            className="md-render"
            // 内容来自本地文件并经 DOMPurify 净化
            dangerouslySetInnerHTML={{ __html: renderMarkdown(data.content) }}
          />
        )}
        {isEditorVisible && <div ref={hostRef} className="code-editor" />}
      </div>

      {conflict && (
        <ConfirmDialog
          title="保存冲突"
          message={'文件在你编辑期间被外部修改过（可能是 Claude 正在改它）。\n覆盖会丢失外部的修改。'}
          confirmText="仍要覆盖"
          danger
          onCancel={() => setConflict(false)}
          onConfirm={() => void save(true)}
        />
      )}
    </div>
  )
}
