import {
  ArrowLeft,
  Bot,
  ChevronLeft,
  ChevronRight,
  Download,
  ExternalLink,
  FilePlus2,
  FileUp,
  FolderOpen,
  GitCompareArrows,
  PencilLine,
  RefreshCcw,
  RotateCcw,
  Save,
  Trash2,
  WandSparkles,
  X,
} from 'lucide-react'
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'sonner'

import { FormField } from '@/components/form-field'
import { LazyDiffEditor } from '@/components/lazy-monaco'
import { MediaUrlControls } from '@/components/media-url-controls'
import { MarkdownPreview } from '@/components/markdown-preview'
import {
  MarkdownWorkbench,
  configureMonaco,
  editorTheme,
  sharedOptions,
  type MarkdownWorkbenchMode,
  type MarkdownWorkbenchPanel,
} from '@/components/markdown-workbench'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Select } from '@/components/ui/select'
import { Skeleton } from '@/components/ui/skeleton'
import { Textarea } from '@/components/ui/textarea'
import { adminApi, ApiError } from '@/lib/api'
import {
  emptyToNull,
  formatDateTime,
  formatPostStatus,
  formatPostType,
  formatPostVisibility,
  postTagsToList,
} from '@/lib/admin-format'
import { buildMarkdownDocument, parseMarkdownDocument } from '@/lib/markdown-document'
import { countLineDiff, normalizeMarkdown } from '@/lib/markdown-diff'
import { applySelectedDiffHunks, computeDiffHunks, type DiffHunk } from '@/lib/markdown-merge'
import {
  consumePolishWindowResult,
  readPolishWindowResult,
  saveDraftWindowSnapshot,
  type DraftWindowSnapshot,
  type PolishWindowResult,
} from '@/lib/post-draft-window'
import { buildFrontendUrl } from '@/lib/frontend-url'
import { cn } from '@/lib/utils'
import type {
  AdminPostMetadataResponse,
  CreatePostPayload,
  PostRecord,
} from '@/lib/types'

type PostFormState = {
  id: number
  title: string
  slug: string
  description: string
  category: string
  postType: string
  image: string
  imagesText: string
  pinned: boolean
  status: string
  visibility: string
  publishAt: string
  unpublishAt: string
  canonicalUrl: string
  noindex: boolean
  ogImage: string
  redirectFromText: string
  redirectTo: string
  tags: string
  markdown: string
  savedMarkdown: string
  path: string
  createdAt: string
  updatedAt: string
  savedMeta: {
    title: string
    description: string
    category: string
    postType: string
    image: string
    imagesText: string
    pinned: boolean
    status: string
    visibility: string
    publishAt: string
    unpublishAt: string
    canonicalUrl: string
    noindex: boolean
    ogImage: string
    redirectFromText: string
    redirectTo: string
    tags: string
  }
}

type CreatePostFormState = {
  title: string
  slug: string
  description: string
  category: string
  postType: string
  image: string
  imagesText: string
  pinned: boolean
  status: string
  visibility: string
  publishAt: string
  unpublishAt: string
  canonicalUrl: string
  noindex: boolean
  ogImage: string
  redirectFromText: string
  redirectTo: string
  tags: string
  markdown: string
}

type PolishSessionState = {
  sourceMarkdown: string
  polishedMarkdown: string
  selectedIds: Set<string>
}

type MetadataProposalField = 'title' | 'slug' | 'description' | 'category' | 'tags'

type MetadataProposalSelection = Record<MetadataProposalField, boolean>

type MetadataSnapshot = {
  title: string
  slug: string
  description: string
  category: string
  tags: string[]
}

type MetadataProposalState = {
  current: MetadataSnapshot
  suggested: MetadataSnapshot
  selected: MetadataProposalSelection
}

type MetadataDialogTarget = 'editor' | 'create'

type MetadataDialogState = {
  target: MetadataDialogTarget
  title: string
  path: string
  proposal: MetadataProposalState
}

type MetadataDraftSource = Pick<
  CreatePostFormState,
  'title' | 'slug' | 'description' | 'category' | 'tags'
>

const editorMetadataProposalFields: MetadataProposalField[] = [
  'title',
  'description',
  'category',
  'tags',
]
const createMetadataProposalFields: MetadataProposalField[] = [
  'title',
  'slug',
  'description',
  'category',
  'tags',
]
const defaultCreateForm: CreatePostFormState = {
  title: '',
  slug: '',
  description: '',
  category: '',
  postType: 'article',
  image: '',
  imagesText: '',
  pinned: false,
  status: 'draft',
  visibility: 'public',
  publishAt: '',
  unpublishAt: '',
  canonicalUrl: '',
  noindex: false,
  ogImage: '',
  redirectFromText: '',
  redirectTo: '',
  tags: '',
  markdown: '# 未命名文章\n',
}

const defaultWorkbenchPanels: MarkdownWorkbenchPanel[] = ['edit']
const orderedWorkbenchPanels: MarkdownWorkbenchPanel[] = ['edit', 'preview', 'diff']
const POSTS_PAGE_SIZE_OPTIONS = [12, 24, 48] as const
const ADMIN_BASENAME =
  ((import.meta.env.VITE_ADMIN_BASENAME as string | undefined)?.trim() || '').replace(/\/$/, '')
const POLISH_RESULT_STORAGE_PREFIX = 'termi-admin-post-polish-result:'

function buildAdminRoute(path: string) {
  const normalizedPath = path.startsWith('/') ? path : `/${path}`
  return `${ADMIN_BASENAME}${normalizedPath}` || normalizedPath
}

function formatWorkbenchPanelLabel(panel: MarkdownWorkbenchPanel) {
  switch (panel) {
    case 'preview':
      return '预览'
    case 'diff':
      return '对比'
    case 'edit':
    default:
      return '编辑'
  }
}

function normalizeWorkbenchPanels(panels: MarkdownWorkbenchPanel[]) {
  const nextPanels = orderedWorkbenchPanels.filter((panel) => panels.includes(panel))
  return nextPanels.length ? nextPanels : [...defaultWorkbenchPanels]
}

function formatWorkbenchStateLabel(
  mode: MarkdownWorkbenchMode,
  panels: MarkdownWorkbenchPanel[],
) {
  if (mode === 'polish') {
    return 'AI 润色'
  }

  return `已打开：${normalizeWorkbenchPanels(panels)
    .map((panel) => formatWorkbenchPanelLabel(panel))
    .join(' / ')}`
}

function buildVirtualPostPath(slug: string) {
  const normalizedSlug = slug.trim() || 'new-post'
  return `article://posts/${normalizedSlug}`
}

function buildInlineImagePrefix(value: string) {
  const normalized = value
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9-]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 64)

  return `post-inline-images/${normalized || 'draft'}`
}

function parseImageList(value: string) {
  return value
    .split('\n')
    .map((item) => item.trim())
    .filter(Boolean)
}

function parseTagList(value: string) {
  return value
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean)
}

function resolveCoverPreviewUrl(value: string) {
  const trimmed = value.trim()
  if (!trimmed) {
    return ''
  }

  if (/^(?:https?:\/\/|data:|blob:)/i.test(trimmed)) {
    return trimmed
  }

  if (typeof window === 'undefined') {
    return trimmed
  }

  if (trimmed.startsWith('/')) {
    return buildFrontendUrl(trimmed)
  }

  return trimmed
}

function normalizeMetadataText(value: string) {
  return value.trim()
}

function normalizeMetadataTags(tags: string[]) {
  return tags
    .map((item) => item.trim())
    .filter(Boolean)
}

function createEmptyMetadataSelection(): MetadataProposalSelection {
  return {
    title: false,
    slug: false,
    description: false,
    category: false,
    tags: false,
  }
}

function metadataTagsEqual(left: string[], right: string[]) {
  const normalizedLeft = normalizeMetadataTags(left)
  const normalizedRight = normalizeMetadataTags(right)

  return (
    normalizedLeft.length === normalizedRight.length &&
    normalizedLeft.every((item, index) => item === normalizedRight[index])
  )
}

function metadataProposalFieldsForTarget(target: MetadataDialogTarget) {
  return target === 'editor' ? editorMetadataProposalFields : createMetadataProposalFields
}

function buildMetadataSnapshot(form: MetadataDraftSource): MetadataSnapshot {
  return {
    title: form.title,
    slug: form.slug,
    description: form.description,
    category: form.category,
    tags: parseTagList(form.tags),
  }
}

function buildMetadataSelection(
  current: MetadataSnapshot,
  suggested: MetadataSnapshot,
  fields: MetadataProposalField[],
): MetadataProposalSelection {
  const selection = createEmptyMetadataSelection()

  fields.forEach((field) => {
    switch (field) {
      case 'title':
        selection.title =
          normalizeMetadataText(current.title) !== normalizeMetadataText(suggested.title)
        break
      case 'slug':
        selection.slug =
          normalizeMetadataText(current.slug) !== normalizeMetadataText(suggested.slug)
        break
      case 'description':
        selection.description =
          normalizeMetadataText(current.description) !==
          normalizeMetadataText(suggested.description)
        break
      case 'category':
        selection.category =
          normalizeMetadataText(current.category) !== normalizeMetadataText(suggested.category)
        break
      case 'tags':
        selection.tags = !metadataTagsEqual(current.tags, suggested.tags)
        break
      default:
        break
    }
  })

  return selection
}

function buildMetadataProposal(
  form: MetadataDraftSource,
  generated: AdminPostMetadataResponse,
  target: MetadataDialogTarget,
): MetadataProposalState {
  const fields = metadataProposalFieldsForTarget(target)
  const current = buildMetadataSnapshot(form)
  const suggested: MetadataSnapshot = {
    title: generated.title.trim(),
    slug: generated.slug.trim(),
    description: generated.description.trim(),
    category: generated.category.trim(),
    tags: normalizeMetadataTags(generated.tags),
  }

  return {
    current,
    suggested,
    selected: buildMetadataSelection(current, suggested, fields),
  }
}

function metadataFieldLabel(field: MetadataProposalField) {
  switch (field) {
    case 'title':
      return '标题'
    case 'slug':
      return 'Slug'
    case 'description':
      return '摘要'
    case 'category':
      return '分类'
    case 'tags':
      return '标签'
    default:
      return field
  }
}

function metadataFieldChanged(
  proposal: MetadataProposalState,
  field: MetadataProposalField,
) {
  switch (field) {
    case 'title':
      return (
        normalizeMetadataText(proposal.current.title) !==
        normalizeMetadataText(proposal.suggested.title)
      )
    case 'slug':
      return (
        normalizeMetadataText(proposal.current.slug) !==
        normalizeMetadataText(proposal.suggested.slug)
      )
    case 'description':
      return (
        normalizeMetadataText(proposal.current.description) !==
        normalizeMetadataText(proposal.suggested.description)
      )
    case 'category':
      return (
        normalizeMetadataText(proposal.current.category) !==
        normalizeMetadataText(proposal.suggested.category)
      )
    case 'tags':
      return !metadataTagsEqual(proposal.current.tags, proposal.suggested.tags)
    default:
      return false
  }
}

function countSelectedMetadataFields(
  selection: MetadataProposalSelection,
  fields: MetadataProposalField[],
) {
  return fields.filter((field) => selection[field]).length
}

function countChangedMetadataFields(
  proposal: MetadataProposalState,
  fields: MetadataProposalField[],
) {
  return fields.filter((field) => metadataFieldChanged(proposal, field)).length
}

function stripFrontmatter(markdown: string) {
  const normalized = markdown.replace(/\r\n/g, '\n')
  if (!normalized.startsWith('---\n')) {
    return normalized.trimStart()
  }

  const endIndex = normalized.indexOf('\n---\n', 4)
  if (endIndex === -1) {
    return normalized.trimStart()
  }

  return normalized.slice(endIndex + 5).trimStart()
}

function extractPostStatus(markdown: string) {
  const normalized = markdown.replace(/\r\n/g, '\n')
  if (!normalized.startsWith('---\n')) {
    return 'published'
  }

  const endIndex = normalized.indexOf('\n---\n', 4)
  if (endIndex === -1) {
    return 'published'
  }

  const frontmatter = normalized.slice(4, endIndex)
  const statusMatch = frontmatter.match(/^status:\s*(.+)\s*$/m)
  if (statusMatch?.[1]) {
    return statusMatch[1].replace(/^['"]|['"]$/g, '').trim() || 'published'
  }

  const publishedMatch = frontmatter.match(/^published:\s*(true|false)\s*$/m)
  if (publishedMatch) {
    return publishedMatch[1] === 'false' ? 'draft' : 'published'
  }

  return 'published'
}

function buildMarkdownForSave(form: PostFormState) {
  const lines = [
    '---',
    `title: ${JSON.stringify(form.title.trim() || form.slug)}`,
    `slug: ${form.slug}`,
  ]

  if (form.description.trim()) {
    lines.push(`description: ${JSON.stringify(form.description.trim())}`)
  }
  if (form.category.trim()) {
    lines.push(`category: ${JSON.stringify(form.category.trim())}`)
  }

  lines.push(`post_type: ${JSON.stringify(form.postType.trim() || 'article')}`)
  lines.push(`pinned: ${form.pinned ? 'true' : 'false'}`)
  lines.push(`status: ${JSON.stringify(form.status.trim() || extractPostStatus(form.markdown))}`)
  lines.push(`visibility: ${JSON.stringify(form.visibility.trim() || 'public')}`)
  lines.push(`noindex: ${form.noindex ? 'true' : 'false'}`)

  if (form.publishAt.trim()) {
    lines.push(`publish_at: ${JSON.stringify(form.publishAt.trim())}`)
  }

  if (form.unpublishAt.trim()) {
    lines.push(`unpublish_at: ${JSON.stringify(form.unpublishAt.trim())}`)
  }

  if (form.image.trim()) {
    lines.push(`image: ${JSON.stringify(form.image.trim())}`)
  }

  const images = parseImageList(form.imagesText)
  if (images.length) {
    lines.push('images:')
    images.forEach((image) => {
      lines.push(`  - ${JSON.stringify(image)}`)
    })
  }

  const tags = form.tags
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean)
  if (tags.length) {
    lines.push('tags:')
    tags.forEach((tag) => {
      lines.push(`  - ${JSON.stringify(tag)}`)
    })
  }

  if (form.canonicalUrl.trim()) {
    lines.push(`canonical_url: ${JSON.stringify(form.canonicalUrl.trim())}`)
  }

  if (form.ogImage.trim()) {
    lines.push(`og_image: ${JSON.stringify(form.ogImage.trim())}`)
  }

  const redirectFrom = parseImageList(form.redirectFromText)
  if (redirectFrom.length) {
    lines.push('redirect_from:')
    redirectFrom.forEach((item) => {
      lines.push(`  - ${JSON.stringify(item)}`)
    })
  }

  if (form.redirectTo.trim()) {
    lines.push(`redirect_to: ${JSON.stringify(form.redirectTo.trim())}`)
  }

  return `${lines.join('\n')}\n---\n\n${stripFrontmatter(form.markdown).trim()}\n`
}

function buildEditorState(post: PostRecord, markdown: string, path: string): PostFormState {
  const tags = postTagsToList(post.tags).join(', ')
  const imagesText = (post.images ?? []).join('\n')

  return {
    id: post.id,
    title: post.title ?? '',
    slug: post.slug,
    description: post.description ?? '',
    category: post.category ?? '',
    postType: post.post_type ?? 'article',
    image: post.image ?? '',
    imagesText,
    pinned: Boolean(post.pinned),
    status: post.status ?? extractPostStatus(markdown),
    visibility: post.visibility ?? 'public',
    publishAt: post.publish_at ?? '',
    unpublishAt: post.unpublish_at ?? '',
    canonicalUrl: post.canonical_url ?? '',
    noindex: Boolean(post.noindex),
    ogImage: post.og_image ?? '',
    redirectFromText: (post.redirect_from ?? []).join('\n'),
    redirectTo: post.redirect_to ?? '',
    tags,
    markdown,
    savedMarkdown: markdown,
    path,
    createdAt: post.created_at,
    updatedAt: post.updated_at,
    savedMeta: {
      title: post.title ?? '',
      description: post.description ?? '',
      category: post.category ?? '',
      postType: post.post_type ?? 'article',
      image: post.image ?? '',
      imagesText,
      pinned: Boolean(post.pinned),
      status: post.status ?? extractPostStatus(markdown),
      visibility: post.visibility ?? 'public',
      publishAt: post.publish_at ?? '',
      unpublishAt: post.unpublish_at ?? '',
      canonicalUrl: post.canonical_url ?? '',
      noindex: Boolean(post.noindex),
      ogImage: post.og_image ?? '',
      redirectFromText: (post.redirect_from ?? []).join('\n'),
      redirectTo: post.redirect_to ?? '',
      tags,
    },
  }
}

function hasMetadataDraftChanges(form: PostFormState) {
  return (
    form.title !== form.savedMeta.title ||
    form.description !== form.savedMeta.description ||
    form.category !== form.savedMeta.category ||
    form.postType !== form.savedMeta.postType ||
    form.image !== form.savedMeta.image ||
    form.imagesText !== form.savedMeta.imagesText ||
    form.pinned !== form.savedMeta.pinned ||
    form.status !== form.savedMeta.status ||
    form.visibility !== form.savedMeta.visibility ||
    form.publishAt !== form.savedMeta.publishAt ||
    form.unpublishAt !== form.savedMeta.unpublishAt ||
    form.canonicalUrl !== form.savedMeta.canonicalUrl ||
    form.noindex !== form.savedMeta.noindex ||
    form.ogImage !== form.savedMeta.ogImage ||
    form.redirectFromText !== form.savedMeta.redirectFromText ||
    form.redirectTo !== form.savedMeta.redirectTo ||
    form.tags !== form.savedMeta.tags
  )
}

function buildDraftMarkdownForWindow(form: PostFormState) {
  return hasMetadataDraftChanges(form) ? buildMarkdownForSave(form) : form.markdown
}

function buildCreatePayload(form: CreatePostFormState): CreatePostPayload {
  return {
    title: form.title.trim(),
    slug: emptyToNull(form.slug),
    description: emptyToNull(form.description),
    content: form.markdown,
    category: emptyToNull(form.category),
    tags: form.tags
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean),
    postType: emptyToNull(form.postType) ?? 'article',
    image: emptyToNull(form.image),
    images: parseImageList(form.imagesText),
    pinned: form.pinned,
    status: emptyToNull(form.status) ?? 'draft',
    visibility: emptyToNull(form.visibility) ?? 'public',
    publishAt: emptyToNull(form.publishAt),
    unpublishAt: emptyToNull(form.unpublishAt),
    canonicalUrl: emptyToNull(form.canonicalUrl),
    noindex: form.noindex,
    ogImage: emptyToNull(form.ogImage),
    redirectFrom: parseImageList(form.redirectFromText),
    redirectTo: emptyToNull(form.redirectTo),
  }
}

function buildCreateMarkdownForWindow(form: CreatePostFormState) {
  return buildMarkdownDocument(
    {
      title: form.title.trim(),
      slug: form.slug.trim() || 'new-post',
      description: form.description.trim(),
      category: form.category.trim(),
      postType: form.postType.trim() || 'article',
      image: form.image.trim(),
      images: parseImageList(form.imagesText),
      pinned: form.pinned,
      status: form.status.trim() || 'draft',
      visibility: form.visibility.trim() || 'public',
      publishAt: form.publishAt.trim(),
      unpublishAt: form.unpublishAt.trim(),
      canonicalUrl: form.canonicalUrl.trim(),
      noindex: form.noindex,
      ogImage: form.ogImage.trim(),
      redirectFrom: parseImageList(form.redirectFromText),
      redirectTo: form.redirectTo.trim(),
      tags: form.tags
        .split(',')
        .map((item) => item.trim())
        .filter(Boolean),
    },
    stripFrontmatter(form.markdown),
  )
}

function applyPolishedEditorState(form: PostFormState, markdown: string): PostFormState {
  const parsed = parseMarkdownDocument(markdown)
  const nextMarkdown = buildMarkdownDocument(
    {
      title: parsed.meta.title || form.title,
      slug: form.slug,
      description: parsed.meta.description || form.description,
      category: parsed.meta.category || form.category,
      postType: parsed.meta.postType || form.postType,
      image: parsed.meta.image || form.image,
      images: parsed.meta.images.length ? parsed.meta.images : parseImageList(form.imagesText),
      pinned: parsed.meta.pinned,
      status: parsed.meta.status || form.status,
      visibility: parsed.meta.visibility || form.visibility,
      publishAt: parsed.meta.publishAt || form.publishAt,
      unpublishAt: parsed.meta.unpublishAt || form.unpublishAt,
      canonicalUrl: parsed.meta.canonicalUrl || form.canonicalUrl,
      noindex: parsed.meta.noindex,
      ogImage: parsed.meta.ogImage || form.ogImage,
      redirectFrom: parsed.meta.redirectFrom.length
        ? parsed.meta.redirectFrom
        : parseImageList(form.redirectFromText),
      redirectTo: parsed.meta.redirectTo || form.redirectTo,
      tags: parsed.meta.tags.length
        ? parsed.meta.tags
        : form.tags
            .split(',')
            .map((item) => item.trim())
            .filter(Boolean),
    },
    parsed.body,
  )

  return {
    ...form,
    title: parsed.meta.title || form.title,
    description: parsed.meta.description || form.description,
    category: parsed.meta.category || form.category,
    postType: parsed.meta.postType || form.postType,
    image: parsed.meta.image || form.image,
    imagesText: parsed.meta.images.length ? parsed.meta.images.join('\n') : form.imagesText,
    pinned: parsed.meta.pinned,
    status: parsed.meta.status || form.status,
    visibility: parsed.meta.visibility || form.visibility,
    publishAt: parsed.meta.publishAt || form.publishAt,
    unpublishAt: parsed.meta.unpublishAt || form.unpublishAt,
    canonicalUrl: parsed.meta.canonicalUrl || form.canonicalUrl,
    noindex: parsed.meta.noindex,
    ogImage: parsed.meta.ogImage || form.ogImage,
    redirectFromText: parsed.meta.redirectFrom.length
      ? parsed.meta.redirectFrom.join('\n')
      : form.redirectFromText,
    redirectTo: parsed.meta.redirectTo || form.redirectTo,
    tags: parsed.meta.tags.length ? parsed.meta.tags.join(', ') : form.tags,
    markdown: nextMarkdown,
  }
}

function applyPolishedCreateState(form: CreatePostFormState, markdown: string): CreatePostFormState {
  const parsed = parseMarkdownDocument(markdown)

  return {
    ...form,
    title: parsed.meta.title || form.title,
    slug: parsed.meta.slug || form.slug,
    description: parsed.meta.description || form.description,
    category: parsed.meta.category || form.category,
    postType: parsed.meta.postType || form.postType,
    image: parsed.meta.image || form.image,
    imagesText: parsed.meta.images.length ? parsed.meta.images.join('\n') : form.imagesText,
    pinned: parsed.meta.pinned,
    status: parsed.meta.status || form.status,
    visibility: parsed.meta.visibility || form.visibility,
    publishAt: parsed.meta.publishAt || form.publishAt,
    unpublishAt: parsed.meta.unpublishAt || form.unpublishAt,
    canonicalUrl: parsed.meta.canonicalUrl || form.canonicalUrl,
    noindex: parsed.meta.noindex,
    ogImage: parsed.meta.ogImage || form.ogImage,
    redirectFromText: parsed.meta.redirectFrom.length
      ? parsed.meta.redirectFrom.join('\n')
      : form.redirectFromText,
    redirectTo: parsed.meta.redirectTo || form.redirectTo,
    tags: parsed.meta.tags.length ? parsed.meta.tags.join(', ') : form.tags,
    markdown: parsed.body || stripFrontmatter(markdown),
  }
}

function downloadMarkdownFile(filename: string, content: string) {
  const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const anchor = document.createElement('a')
  anchor.href = url
  anchor.download = filename
  anchor.click()
  URL.revokeObjectURL(url)
}

export function PostsPage() {
  const navigate = useNavigate()
  const { slug } = useParams()
  const importInputRef = useRef<HTMLInputElement | null>(null)
  const folderImportInputRef = useRef<HTMLInputElement | null>(null)
  const [posts, setPosts] = useState<PostRecord[]>([])
  const [loading, setLoading] = useState(true)
  const [refreshing, setRefreshing] = useState(false)
  const [editorLoading, setEditorLoading] = useState(false)
  const [saving, setSaving] = useState(false)
  const [creating, setCreating] = useState(false)
  const [deleting, setDeleting] = useState(false)
  const [importing, setImporting] = useState(false)
  const [generatingMetadata, setGeneratingMetadata] = useState(false)
  const [generatingEditorMetadataProposal, setGeneratingEditorMetadataProposal] =
    useState(false)
  const [generatingEditorCover, setGeneratingEditorCover] = useState(false)
  const [generatingCreateCover, setGeneratingCreateCover] = useState(false)
  const [localizingEditorImages, setLocalizingEditorImages] = useState(false)
  const [localizingCreateImages, setLocalizingCreateImages] = useState(false)
  const [generatingEditorPolish, setGeneratingEditorPolish] = useState(false)
  const [generatingCreatePolish, setGeneratingCreatePolish] = useState(false)
  const [editor, setEditor] = useState<PostFormState | null>(null)
  const [metadataDialog, setMetadataDialog] = useState<MetadataDialogState | null>(null)
  const [editorMode, setEditorMode] = useState<MarkdownWorkbenchMode>('workspace')
  const [editorPanels, setEditorPanels] = useState<MarkdownWorkbenchPanel[]>(defaultWorkbenchPanels)
  const [createDialogOpen, setCreateDialogOpen] = useState(false)
  const [createMode, setCreateMode] = useState<MarkdownWorkbenchMode>('workspace')
  const [createPanels, setCreatePanels] = useState<MarkdownWorkbenchPanel[]>(defaultWorkbenchPanels)
  const [createForm, setCreateForm] = useState<CreatePostFormState>(defaultCreateForm)
  const [editorPolish, setEditorPolish] = useState<PolishSessionState | null>(null)
  const [createPolish, setCreatePolish] = useState<PolishSessionState | null>(null)
  const [searchTerm, setSearchTerm] = useState('')
  const [typeFilter, setTypeFilter] = useState('all')
  const [pinnedFilter, setPinnedFilter] = useState('all')
  const [currentPage, setCurrentPage] = useState(1)
  const [pageSize, setPageSize] = useState<number>(POSTS_PAGE_SIZE_OPTIONS[0])
  const [sortKey, setSortKey] = useState('updated_at_desc')
  const [totalPosts, setTotalPosts] = useState(0)
  const [totalPages, setTotalPages] = useState(1)
  const editorPolishDraftKeyRef = useRef<string | null>(null)
  const createPolishDraftKeyRef = useRef<string | null>(null)

  const { sortBy, sortOrder } = useMemo(() => {
    switch (sortKey) {
      case 'created_at_asc':
        return { sortBy: 'created_at', sortOrder: 'asc' }
      case 'created_at_desc':
        return { sortBy: 'created_at', sortOrder: 'desc' }
      case 'title_asc':
        return { sortBy: 'title', sortOrder: 'asc' }
      case 'title_desc':
        return { sortBy: 'title', sortOrder: 'desc' }
      case 'updated_at_asc':
        return { sortBy: 'updated_at', sortOrder: 'asc' }
      default:
        return { sortBy: 'updated_at', sortOrder: 'desc' }
    }
  }, [sortKey])

  const loadPosts = useCallback(async (showToast = false) => {
    try {
      if (showToast) {
        setRefreshing(true)
      }

      const next = await adminApi.listPostsPage({
        search: searchTerm.trim() || undefined,
        postType: typeFilter === 'all' ? undefined : typeFilter,
        pinned:
          pinnedFilter === 'all'
            ? undefined
            : pinnedFilter === 'pinned',
        includePrivate: true,
        includeRedirects: true,
        preview: true,
        page: currentPage,
        pageSize,
        sortBy,
        sortOrder,
      })
      startTransition(() => {
        setPosts(next.items)
        setTotalPosts(next.total)
        setTotalPages(next.total_pages)
        if (next.page !== currentPage) {
          setCurrentPage(next.page)
        }
      })

      if (showToast) {
        toast.success('文章列表已刷新。')
      }
    } catch (error) {
      if (error instanceof ApiError && error.status === 401) {
        return
      }
      toast.error(error instanceof ApiError ? error.message : '无法加载文章列表。')
    } finally {
      setLoading(false)
      setRefreshing(false)
    }
  }, [currentPage, pageSize, pinnedFilter, searchTerm, sortBy, sortOrder, typeFilter])

  const loadEditor = useCallback(
    async (nextSlug: string) => {
      try {
        setEditorLoading(true)
        const [post, markdown] = await Promise.all([
          adminApi.getPostBySlug(nextSlug),
          adminApi.getPostMarkdown(nextSlug),
        ])

        startTransition(() => {
          setEditor(buildEditorState(post, markdown.markdown, markdown.path))
          setMetadataDialog(null)
          setEditorMode('workspace')
          setEditorPanels(defaultWorkbenchPanels)
          setEditorPolish(null)
        })
      } catch (error) {
        toast.error(error instanceof ApiError ? error.message : '无法打开这篇文章。')
        navigate('/posts', { replace: true })
      } finally {
        setEditorLoading(false)
      }
    },
    [navigate],
  )

  useEffect(() => {
    void loadPosts(false)
  }, [loadPosts])

  useEffect(() => {
    if (folderImportInputRef.current) {
      folderImportInputRef.current.setAttribute('webkitdirectory', '')
      folderImportInputRef.current.setAttribute('directory', '')
    }
  }, [])

  useEffect(() => {
    setEditorMode('workspace')
    setEditorPanels(defaultWorkbenchPanels)
    editorPolishDraftKeyRef.current = null

    if (!slug) {
      setEditor(null)
      setMetadataDialog(null)
      setEditorPolish(null)
      return
    }

    setCreateDialogOpen(false)
    void loadEditor(slug)
  }, [loadEditor, slug])

  useEffect(() => {
    if (!createDialogOpen) {
      createPolishDraftKeyRef.current = null
    }
  }, [createDialogOpen])

  useEffect(() => {
    if (!metadataDialog && !slug && !createDialogOpen) {
      return
    }

    const previousOverflow = document.body.style.overflow
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        if (metadataDialog) {
          setMetadataDialog(null)
          return
        }

        if (slug) {
          navigate('/posts', { replace: true })
          return
        }

        if (createDialogOpen) {
          setCreateDialogOpen(false)
        }
      }
    }

    document.body.style.overflow = 'hidden'
    window.addEventListener('keydown', handleKeyDown)

    return () => {
      document.body.style.overflow = previousOverflow
      window.removeEventListener('keydown', handleKeyDown)
    }
  }, [createDialogOpen, metadataDialog, navigate, slug])

  useEffect(() => {
    setCurrentPage(1)
  }, [pageSize, pinnedFilter, searchTerm, sortKey, typeFilter])

  const safeCurrentPage = Math.min(currentPage, totalPages)

  useEffect(() => {
    setCurrentPage((current) => Math.min(current, totalPages))
  }, [totalPages])

  const paginatedPosts = posts

  const paginationItems = useMemo(() => {
    const maxVisiblePages = 5
    const halfWindow = Math.floor(maxVisiblePages / 2)
    let startPage = Math.max(1, safeCurrentPage - halfWindow)
    let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1)

    if (endPage - startPage + 1 < maxVisiblePages) {
      startPage = Math.max(1, endPage - maxVisiblePages + 1)
    }

    return Array.from({ length: endPage - startPage + 1 }, (_, index) => startPage + index)
  }, [safeCurrentPage, totalPages])

  const pageStart = totalPosts ? (safeCurrentPage - 1) * pageSize + 1 : 0
  const pageEnd = totalPosts ? Math.min(safeCurrentPage * pageSize, totalPosts) : 0
  const pinnedPostCount = useMemo(
    () => posts.filter((post) => Boolean(post.pinned)).length,
    [posts],
  )

  const markdownDirty = useMemo(() => {
    if (!editor) {
      return false
    }

    return (
      normalizeMarkdown(buildDraftMarkdownForWindow(editor)) !==
      normalizeMarkdown(editor.savedMarkdown)
    )
  }, [editor])

  const createMarkdownDirty = useMemo(
    () =>
      normalizeMarkdown(buildCreateMarkdownForWindow(createForm)) !==
      normalizeMarkdown(buildCreateMarkdownForWindow(defaultCreateForm)),
    [createForm],
  )

  const buildEditorDraftSnapshot = useCallback((): Omit<DraftWindowSnapshot, 'createdAt'> | null => {
    if (!editor) {
      return null
    }

    return {
      title: editor.title.trim() || editor.slug,
      slug: editor.slug,
      path: editor.path,
      markdown: buildDraftMarkdownForWindow(editor),
      savedMarkdown: editor.savedMarkdown,
    }
  }, [editor])

  const buildCreateDraftSnapshot = useCallback((): Omit<DraftWindowSnapshot, 'createdAt'> => {
    const fallbackSlug = createForm.slug.trim() || 'new-post'

    return {
      title: createForm.title.trim() || createForm.slug.trim() || '新建草稿',
      slug: fallbackSlug,
      path: buildVirtualPostPath(fallbackSlug),
      markdown: buildCreateMarkdownForWindow(createForm),
      savedMarkdown: buildCreateMarkdownForWindow(defaultCreateForm),
    }
  }, [createForm])

  const openDraftWorkbenchWindow = useCallback(
    (
      path: string,
      snapshot: Omit<DraftWindowSnapshot, 'createdAt'>,
      extraQuery?: Record<string, string>,
    ) => {
      const draftKey = saveDraftWindowSnapshot(snapshot)
      const url = new URL(buildAdminRoute(path), window.location.origin)
      url.searchParams.set('draftKey', draftKey)

      Object.entries(extraQuery ?? {}).forEach(([key, value]) => {
        if (value) {
          url.searchParams.set(key, value)
        }
      })

      const popup = window.open(
        url.toString(),
        '_blank',
        'popup=yes,width=1560,height=980,resizable=yes,scrollbars=yes',
      )

      if (!popup) {
        toast.error('浏览器拦截了独立工作台窗口，请允许当前站点打开新窗口后重试。')
        return null
      }

      popup.focus()
      return draftKey
    },
    [],
  )

  const applyExternalPolishResult = useCallback(
    (result: PolishWindowResult) => {
      if (result.target === 'editor') {
        if (!editor) {
          return false
        }

        startTransition(() => {
          setEditor((current) =>
            current ? applyPolishedEditorState(current, result.markdown) : current,
          )
          setEditorPolish(null)
          setEditorMode('workspace')
        })
        toast.success('独立 AI 润色结果已回填到当前文章。')
        return true
      }

      if (!createDialogOpen) {
        return false
      }

      startTransition(() => {
        setCreateForm((current) => applyPolishedCreateState(current, result.markdown))
        setCreatePolish(null)
        setCreateMode('workspace')
      })
      toast.success('独立 AI 润色结果已回填到新建草稿。')
      return true
    },
    [createDialogOpen, editor],
  )

  const flushPendingPolishResult = useCallback(
    (draftKey: string | null) => {
      const pending = readPolishWindowResult(draftKey)
      if (!pending || !applyExternalPolishResult(pending)) {
        return false
      }

      consumePolishWindowResult(draftKey)
      return true
    },
    [applyExternalPolishResult],
  )

  useEffect(() => {
    const tryFlushAll = () => {
      if (flushPendingPolishResult(editorPolishDraftKeyRef.current)) {
        editorPolishDraftKeyRef.current = null
      }
      if (flushPendingPolishResult(createPolishDraftKeyRef.current)) {
        createPolishDraftKeyRef.current = null
      }
    }

    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== window.location.origin || !event.data) {
        return
      }

      const payload = event.data as Partial<PolishWindowResult> & { type?: string }
      if (
        payload.type !== 'termi-admin-post-polish-apply' ||
        typeof payload.draftKey !== 'string' ||
        typeof payload.markdown !== 'string'
      ) {
        return
      }

      const result: PolishWindowResult = {
        draftKey: payload.draftKey,
        markdown: payload.markdown,
        target: payload.target === 'create' ? 'create' : 'editor',
        createdAt: typeof payload.createdAt === 'number' ? payload.createdAt : Date.now(),
      }

      if (!applyExternalPolishResult(result)) {
        return
      }

      consumePolishWindowResult(result.draftKey)
      if (result.target === 'editor') {
        editorPolishDraftKeyRef.current = null
      } else {
        createPolishDraftKeyRef.current = null
      }
    }

    const handleStorage = (event: StorageEvent) => {
      if (!event.key?.startsWith(POLISH_RESULT_STORAGE_PREFIX)) {
        return
      }

      tryFlushAll()
    }

    window.addEventListener('message', handleMessage)
    window.addEventListener('storage', handleStorage)
    window.addEventListener('focus', tryFlushAll)
    tryFlushAll()

    return () => {
      window.removeEventListener('message', handleMessage)
      window.removeEventListener('storage', handleStorage)
      window.removeEventListener('focus', tryFlushAll)
    }
  }, [applyExternalPolishResult, flushPendingPolishResult])

  const compareStats = useMemo(() => {
    if (!editor) {
      return {
        additions: 0,
        deletions: 0,
      }
    }

    return countLineDiff(editor.savedMarkdown, buildDraftMarkdownForWindow(editor))
  }, [editor])
  const editorCoverPreviewUrl = useMemo(
    () => resolveCoverPreviewUrl(editor?.image ?? ''),
    [editor?.image],
  )
  const createCoverPreviewUrl = useMemo(
    () => resolveCoverPreviewUrl(createForm.image),
    [createForm.image],
  )

  const saveEditor = useCallback(async () => {
    if (!editor) {
      return
    }

    if (!editor.title.trim()) {
      toast.error('保存前必须填写标题。')
      return
    }

    try {
      setSaving(true)
      const persistedMarkdown = buildMarkdownForSave(editor)
      const updatedPost = await adminApi.updatePost(editor.id, {
        title: editor.title.trim(),
        slug: editor.slug,
        description: emptyToNull(editor.description),
        category: emptyToNull(editor.category),
        tags: editor.tags
          .split(',')
          .map((item) => item.trim())
          .filter(Boolean),
        postType: emptyToNull(editor.postType) ?? 'article',
        image: emptyToNull(editor.image),
        images: parseImageList(editor.imagesText),
        pinned: editor.pinned,
        status: emptyToNull(editor.status) ?? 'draft',
        visibility: emptyToNull(editor.visibility) ?? 'public',
        publishAt: emptyToNull(editor.publishAt),
        unpublishAt: emptyToNull(editor.unpublishAt),
        canonicalUrl: emptyToNull(editor.canonicalUrl),
        noindex: editor.noindex,
        ogImage: emptyToNull(editor.ogImage),
        redirectFrom: parseImageList(editor.redirectFromText),
        redirectTo: emptyToNull(editor.redirectTo),
      })

      const updatedMarkdown = await adminApi.updatePostMarkdown(editor.slug, persistedMarkdown)

      startTransition(() => {
        setEditor(buildEditorState(updatedPost, updatedMarkdown.markdown, updatedMarkdown.path))
        setEditorPolish(null)
      })

      await loadPosts(false)
      toast.success('文章已保存。')
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : '无法保存文章。')
    } finally {
      setSaving(false)
    }
  }, [editor, loadPosts])

  const importMarkdownFiles = useCallback(
    async (fileList: FileList | null) => {
      const files = Array.from(fileList ?? []).filter((file) =>
        /\.(md|markdown)$/i.test(file.webkitRelativePath || file.name),
      )
      if (!files.length) {
        return
      }

      try {
        setImporting(true)
        const result = await adminApi.importPosts(files)
        await loadPosts(false)
        toast.success(`已导入 ${result.count} 篇 Markdown。`)

        const firstSlug = result.slugs[0]
        if (firstSlug) {
          navigate(`/posts/${firstSlug}`)
        }
      } catch (error) {
        toast.error(error instanceof ApiError ? error.message : '导入 Markdown 失败。')
      } finally {
        setImporting(false)
        if (importInputRef.current) {
          importInputRef.current.value = ''
        }
        if (folderImportInputRef.current) {
          folderImportInputRef.current.value = ''
        }
      }
    },
    [loadPosts, navigate],
  )

  const generateCreateMetadata = useCallback(async () => {
    const sourceMarkdown = buildCreateMarkdownForWindow(createForm)
    if (!stripFrontmatter(sourceMarkdown).trim()) {
      toast.error('先写一点正文，再让 AI 帮你补元数据。')
      return
    }

    try {
      setGeneratingMetadata(true)
      const generated = await adminApi.generatePostMetadata(sourceMarkdown)
      const nextProposal = buildMetadataProposal(createForm, generated, 'create')
      const changedCount = countChangedMetadataFields(
        nextProposal,
        createMetadataProposalFields,
      )

      startTransition(() => {
        setMetadataDialog({
          target: 'create',
          title: createForm.title.trim() || createForm.slug.trim() || '新建草稿',
          path: buildVirtualPostPath(createForm.slug),
          proposal: nextProposal,
        })
      })

      if (changedCount) {
        toast.success(`AI 已生成 ${changedCount} 项元数据建议，可以先对比再回填。`)
      } else {
        toast.success('AI 已完成分析，这一版元数据和当前内容基本一致。')
      }
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : 'AI 元数据生成失败。')
    } finally {
      setGeneratingMetadata(false)
    }
  }, [createForm])

  const generateEditorMetadataProposal = useCallback(async () => {
    if (!editor) {
      return
    }

    const sourceMarkdown = buildDraftMarkdownForWindow(editor)
    if (!stripFrontmatter(sourceMarkdown).trim()) {
      toast.error('先准备一点正文，再让 AI 给这篇旧文章补元数据。')
      return
    }

    try {
      setGeneratingEditorMetadataProposal(true)
      const generated = await adminApi.generatePostMetadata(sourceMarkdown)
      const nextProposal = buildMetadataProposal(editor, generated, 'editor')
      const changedCount = countChangedMetadataFields(
        nextProposal,
        editorMetadataProposalFields,
      )

      startTransition(() => {
        setMetadataDialog({
          target: 'editor',
          title: editor.title.trim() || editor.slug,
          path: editor.path,
          proposal: nextProposal,
        })
      })

      if (changedCount) {
        toast.success(`AI 已生成 ${changedCount} 项元数据建议，可以逐项合并。`)
      } else {
        toast.success('AI 已完成分析，这一版元数据和当前内容基本一致。')
      }
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : 'AI 元数据提案生成失败。')
    } finally {
      setGeneratingEditorMetadataProposal(false)
    }
  }, [editor])

  const generateEditorCover = useCallback(async () => {
    if (!editor) {
      return
    }

    try {
      setGeneratingEditorCover(true)
      const result = await adminApi.generatePostCoverImage({
        title: editor.title,
        description: emptyToNull(editor.description),
        category: emptyToNull(editor.category),
        tags: parseTagList(editor.tags),
        postType: emptyToNull(editor.postType) ?? 'article',
        slug: editor.slug,
        markdown: buildDraftMarkdownForWindow(editor),
      })

      startTransition(() => {
        setEditor((current) =>
          current ? { ...current, image: result.image_url } : current,
        )
      })
      toast.success('AI 封面图已生成，并回填到封面图 URL。')
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : 'AI 封面图生成失败。')
    } finally {
      setGeneratingEditorCover(false)
    }
  }, [editor])

  const generateCreateCover = useCallback(async () => {
    try {
      setGeneratingCreateCover(true)
      const result = await adminApi.generatePostCoverImage({
        title: createForm.title,
        description: emptyToNull(createForm.description),
        category: emptyToNull(createForm.category),
        tags: parseTagList(createForm.tags),
        postType: emptyToNull(createForm.postType) ?? 'article',
        slug: emptyToNull(createForm.slug),
        markdown: buildCreateMarkdownForWindow(createForm),
      })

      startTransition(() => {
        setCreateForm((current) => ({ ...current, image: result.image_url }))
      })
      toast.success('AI 封面图已生成，并回填到封面图 URL。')
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : 'AI 封面图生成失败。')
    } finally {
      setGeneratingCreateCover(false)
    }
  }, [createForm])

  const localizeEditorMarkdownImages = useCallback(async () => {
    if (!editor) {
      return
    }

    const sourceMarkdown = buildDraftMarkdownForWindow(editor)
    if (!stripFrontmatter(sourceMarkdown).trim()) {
      toast.error('先准备一点正文，再执行正文图片本地化。')
      return
    }

    try {
      setLocalizingEditorImages(true)
      const result = await adminApi.localizePostMarkdownImages({
        markdown: sourceMarkdown,
        prefix: buildInlineImagePrefix(editor.slug),
      })

      if (!result.localized_count && !result.failed_count) {
        toast.message('正文里没有检测到需要本地化的远程图片。')
        return
      }

      startTransition(() => {
        setEditor((current) =>
          current ? applyPolishedEditorState(current, result.markdown) : current,
        )
      })

      if (result.localized_count && result.failed_count) {
        toast.warning(
          `已替换 ${result.localized_count} 处正文图片，另有 ${result.failed_count} 个链接处理失败。`,
        )
      } else if (result.localized_count) {
        toast.success(`已把 ${result.localized_count} 处正文图片替换为媒体库地址。`)
      } else {
        toast.warning(`没有成功替换正文图片，失败 ${result.failed_count} 个链接。`)
      }
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : '正文图片本地化失败。')
    } finally {
      setLocalizingEditorImages(false)
    }
  }, [editor])

  const localizeCreateMarkdownImages = useCallback(async () => {
    const sourceMarkdown = buildCreateMarkdownForWindow(createForm)
    if (!stripFrontmatter(sourceMarkdown).trim()) {
      toast.error('先准备一点正文，再执行正文图片本地化。')
      return
    }

    try {
      setLocalizingCreateImages(true)
      const result = await adminApi.localizePostMarkdownImages({
        markdown: sourceMarkdown,
        prefix: buildInlineImagePrefix(createForm.slug || createForm.title),
      })

      if (!result.localized_count && !result.failed_count) {
        toast.message('正文里没有检测到需要本地化的远程图片。')
        return
      }

      startTransition(() => {
        setCreateForm((current) => applyPolishedCreateState(current, result.markdown))
      })

      if (result.localized_count && result.failed_count) {
        toast.warning(
          `已替换 ${result.localized_count} 处正文图片，另有 ${result.failed_count} 个链接处理失败。`,
        )
      } else if (result.localized_count) {
        toast.success(`已把 ${result.localized_count} 处正文图片替换为媒体库地址。`)
      } else {
        toast.warning(`没有成功替换正文图片，失败 ${result.failed_count} 个链接。`)
      }
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : '正文图片本地化失败。')
    } finally {
      setLocalizingCreateImages(false)
    }
  }, [createForm])

  const openEditorPreviewWindow = useCallback(() => {
    const snapshot = buildEditorDraftSnapshot()
    if (!snapshot) {
      toast.error('请先打开一篇文章，再启动独立预览窗口。')
      return
    }

    openDraftWorkbenchWindow(`/posts/${encodeURIComponent(snapshot.slug)}/preview`, snapshot)
  }, [buildEditorDraftSnapshot, openDraftWorkbenchWindow])

  const openEditorCompareWindow = useCallback(() => {
    const snapshot = buildEditorDraftSnapshot()
    if (!snapshot) {
      toast.error('请先打开一篇文章，再启动独立对比窗口。')
      return
    }

    openDraftWorkbenchWindow(`/posts/${encodeURIComponent(snapshot.slug)}/compare`, snapshot)
  }, [buildEditorDraftSnapshot, openDraftWorkbenchWindow])

  const openEditorPolishWindow = useCallback(() => {
    const snapshot = buildEditorDraftSnapshot()
    if (!snapshot) {
      toast.error('请先打开一篇文章，再启动独立 AI 润色工作台。')
      return
    }

    const draftKey = openDraftWorkbenchWindow('/posts/polish', snapshot, {
      target: 'editor',
    })

    if (draftKey) {
      editorPolishDraftKeyRef.current = draftKey
    }
  }, [buildEditorDraftSnapshot, openDraftWorkbenchWindow])

  const openCreatePreviewWindow = useCallback(() => {
    openDraftWorkbenchWindow('/posts/preview', buildCreateDraftSnapshot())
  }, [buildCreateDraftSnapshot, openDraftWorkbenchWindow])

  const openCreateCompareWindow = useCallback(() => {
    openDraftWorkbenchWindow('/posts/compare', buildCreateDraftSnapshot())
  }, [buildCreateDraftSnapshot, openDraftWorkbenchWindow])

  const openCreatePolishWindow = useCallback(() => {
    const draftKey = openDraftWorkbenchWindow('/posts/polish', buildCreateDraftSnapshot(), {
      target: 'create',
    })

    if (draftKey) {
      createPolishDraftKeyRef.current = draftKey
    }
  }, [buildCreateDraftSnapshot, openDraftWorkbenchWindow])

  const editorPolishHunks = useMemo(
    () =>
      editorPolish
        ? computeDiffHunks(editorPolish.sourceMarkdown, editorPolish.polishedMarkdown)
        : [],
    [editorPolish],
  )

  const createPolishHunks = useMemo(
    () =>
      createPolish
        ? computeDiffHunks(createPolish.sourceMarkdown, createPolish.polishedMarkdown)
        : [],
    [createPolish],
  )

  const editorMergedMarkdown = useMemo(
    () =>
      editorPolish
        ? applySelectedDiffHunks(
            editorPolish.sourceMarkdown,
            editorPolishHunks,
            editorPolish.selectedIds,
          )
        : '',
    [editorPolish, editorPolishHunks],
  )

  const createMergedMarkdown = useMemo(
    () =>
      createPolish
        ? applySelectedDiffHunks(
            createPolish.sourceMarkdown,
            createPolishHunks,
            createPolish.selectedIds,
          )
        : '',
    [createPolish, createPolishHunks],
  )

  const generateEditorPolish = useCallback(async () => {
    if (!editor) {
      return
    }

    const sourceMarkdown = buildDraftMarkdownForWindow(editor)

    try {
      setGeneratingEditorPolish(true)
      setEditorMode('polish')
      const result = await adminApi.polishPostMarkdown(sourceMarkdown)
      const nextHunks = computeDiffHunks(sourceMarkdown, result.polished_markdown)
      startTransition(() => {
        setEditorPolish({
          sourceMarkdown,
          polishedMarkdown: result.polished_markdown,
          selectedIds: new Set(nextHunks.map((hunk) => hunk.id)),
        })
      })
      toast.success(`AI 已生成润色稿，共识别 ${nextHunks.length} 个改动块。`)
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : 'AI 润色失败。')
    } finally {
      setGeneratingEditorPolish(false)
    }
  }, [editor])

  const generateCreatePolish = useCallback(async () => {
    const sourceMarkdown = buildCreateMarkdownForWindow(createForm)

    try {
      setGeneratingCreatePolish(true)
      setCreateMode('polish')
      const result = await adminApi.polishPostMarkdown(sourceMarkdown)
      const nextHunks = computeDiffHunks(sourceMarkdown, result.polished_markdown)
      startTransition(() => {
        setCreatePolish({
          sourceMarkdown,
          polishedMarkdown: result.polished_markdown,
          selectedIds: new Set(nextHunks.map((hunk) => hunk.id)),
        })
      })
      toast.success(`AI 已生成润色稿，共识别 ${nextHunks.length} 个改动块。`)
    } catch (error) {
      toast.error(error instanceof ApiError ? error.message : 'AI 润色失败。')
    } finally {
      setGeneratingCreatePolish(false)
    }
  }, [createForm])

  const renderPolishPanel = useCallback(
    ({
      sourceMarkdown,
      mergedMarkdown,
      hunks,
      selectedIds,
      generating,
      onGenerate,
      onToggle,
      onApply,
      onReset,
      onAcceptAll,
      title,
      description,
      modelKey,
    }: {
      sourceMarkdown: string
      mergedMarkdown: string
      hunks: DiffHunk[]
      selectedIds: Set<string>
      generating: boolean
      onGenerate: () => void
      onToggle: (id: string) => void
      onApply: () => void
      onReset: () => void
      onAcceptAll: () => void
      title: string
      description: string
      modelKey: string
    }) => (
      <div className="grid h-full gap-4 bg-[#111111] p-4 xl:grid-cols-[1.08fr_0.92fr]">
        <div className="overflow-hidden rounded-3xl border border-slate-800 bg-[#161616]">
          <div className="flex items-center justify-between border-b border-slate-800 bg-[#141414] px-4 py-3">
            <div>
              <p className="text-sm font-medium text-slate-100">{title}</p>
              <p className="mt-1 text-xs text-slate-400">{description}</p>
            </div>
            <div className="flex flex-wrap gap-2">
              <Button
                size="sm"
                variant="outline"
                onClick={onGenerate}
                disabled={generating}
                className="border-slate-700 bg-[#202020] text-slate-100 hover:bg-[#292929]"
              >
                <WandSparkles className="mr-1 h-4 w-4" />
                {generating ? '润色中...' : hunks.length ? '重新润色' : '生成润色稿'}
              </Button>
              <Button
                size="sm"
                variant="outline"
                onClick={onAcceptAll}
                disabled={!hunks.length}
                className="border-slate-700 bg-[#202020] text-slate-100 hover:bg-[#292929]"
              >
                全部采用
              </Button>
              <Button
                size="sm"
                variant="outline"
                onClick={onReset}
                disabled={!hunks.length}
                className="border-slate-700 bg-[#202020] text-slate-100 hover:bg-[#292929]"
              >
                全部还原
              </Button>
              <Button size="sm" onClick={onApply} disabled={!hunks.length}>
                应用到编辑器
              </Button>
            </div>
          </div>

          <div className="h-[calc(100%-61px)]">
            <LazyDiffEditor
              height="100%"
              language="markdown"
              original={sourceMarkdown}
              modified={mergedMarkdown || sourceMarkdown}
              originalModelPath={`inplace-polish-original-${modelKey}`}
              modifiedModelPath={`inplace-polish-merged-${modelKey}`}
              keepCurrentOriginalModel
              keepCurrentModifiedModel
              theme={editorTheme}
              beforeMount={configureMonaco}
              options={{
                ...sharedOptions,
                originalEditable: false,
                readOnly: true,
                renderSideBySide: true,
              }}
            />
          </div>
        </div>

        <div className="grid min-h-0 gap-4 xl:grid-rows-[1.08fr_0.92fr]">
          <div className="overflow-hidden rounded-3xl border border-slate-800 bg-[#161616]">
            <div className="border-b border-slate-800 bg-[#141414] px-4 py-3">
              <p className="text-sm font-medium text-slate-100">选择要合并的改动块</p>
              <p className="mt-1 text-xs text-slate-400">
                现在不是弹窗对比了，直接在同一个 Monaco 工作区里挑选要保留的 AI 修改。
              </p>
            </div>
            <div className="h-[calc(100%-61px)] overflow-y-auto p-4">
              {!hunks.length ? (
                <div className="rounded-2xl border border-dashed border-slate-700 px-4 py-8 text-sm text-slate-400">
                  先生成一版 AI 润色稿，这里才会出现可选改动块。
                </div>
              ) : (
                <div className="space-y-3">
                  {hunks.map((hunk, index) => {
                    const accepted = selectedIds.has(hunk.id)

                    return (
                      <button
                        key={hunk.id}
                        type="button"
                        onClick={() => onToggle(hunk.id)}
                        className={cn(
                          'w-full rounded-2xl border px-4 py-4 text-left transition',
                          accepted
                            ? 'border-emerald-500/40 bg-emerald-500/12'
                            : 'border-slate-700 bg-[#1b1b1b] hover:border-slate-500',
                        )}
                      >
                        <div className="flex items-center justify-between gap-3">
                          <div>
                            <p className="text-sm font-medium text-slate-100">
                              改动块 {index + 1}
                            </p>
                            <p className="mt-1 text-xs text-slate-400">
                              {accepted ? '已采用这块改动' : '点击后采用这块改动'}
                            </p>
                          </div>
                          <Badge variant={accepted ? 'success' : 'secondary'}>
                            {accepted ? '采用中' : '保留原文'}
                          </Badge>
                        </div>
                        <p className="mt-3 line-clamp-4 text-xs leading-6 text-slate-300">
                          {hunk.preview}
                        </p>
                      </button>
                    )
                  })}
                </div>
              )}
            </div>
          </div>

          <div className="overflow-hidden rounded-3xl border border-slate-200 bg-white">
            <MarkdownPreview markdown={mergedMarkdown || sourceMarkdown} />
          </div>
        </div>
      </div>
    ),
    [],
  )

  const toggleEditorPolishHunk = useCallback((id: string) => {
    setEditorPolish((current) => {
      if (!current) {
        return current
      }

      const next = new Set(current.selectedIds)
      if (next.has(id)) {
        next.delete(id)
      } else {
        next.add(id)
      }

      return { ...current, selectedIds: next }
    })
  }, [])

  const toggleCreatePolishHunk = useCallback((id: string) => {
    setCreatePolish((current) => {
      if (!current) {
        return current
      }

      const next = new Set(current.selectedIds)
      if (next.has(id)) {
        next.delete(id)
      } else {
        next.add(id)
      }

      return { ...current, selectedIds: next }
    })
  }, [])

  const acceptAllEditorPolish = useCallback(() => {
    setEditorPolish((current) =>
      current
        ? { ...current, selectedIds: new Set(editorPolishHunks.map((hunk) => hunk.id)) }
        : current,
    )
  }, [editorPolishHunks])

  const acceptAllCreatePolish = useCallback(() => {
    setCreatePolish((current) =>
      current
        ? { ...current, selectedIds: new Set(createPolishHunks.map((hunk) => hunk.id)) }
        : current,
    )
  }, [createPolishHunks])

  const resetEditorPolish = useCallback(() => {
    setEditorPolish((current) => (current ? { ...current, selectedIds: new Set() } : current))
  }, [])

  const resetCreatePolish = useCallback(() => {
    setCreatePolish((current) => (current ? { ...current, selectedIds: new Set() } : current))
  }, [])

  const applyEditorPolish = useCallback(() => {
    if (!editor || !editorPolish) {
      return
    }

    startTransition(() => {
      setEditor(applyPolishedEditorState(editor, editorMergedMarkdown))
      setEditorPolish(null)
      setEditorMode('workspace')
    })
    toast.success('AI 润色结果已合并到当前文章。')
  }, [editor, editorMergedMarkdown, editorPolish])

  const applyCreatePolish = useCallback(() => {
    if (!createPolish) {
      return
    }

    startTransition(() => {
      setCreateForm((current) => applyPolishedCreateState(current, createMergedMarkdown))
      setCreatePolish(null)
      setCreateMode('workspace')
    })
    toast.success('AI 润色结果已合并到新建草稿。')
  }, [createMergedMarkdown, createPolish])

  const toggleMetadataField = useCallback((field: MetadataProposalField) => {
    setMetadataDialog((current) => {
      if (!current || !metadataFieldChanged(current.proposal, field)) {
        return current
      }

      return {
        ...current,
        proposal: {
          ...current.proposal,
          selected: {
            ...current.proposal.selected,
            [field]: !current.proposal.selected[field],
          },
        },
      }
    })
  }, [])

  const updateMetadataSuggestedField = useCallback(
    (field: MetadataProposalField, value: string) => {
      setMetadataDialog((current) => {
        if (!current) {
          return current
        }

        const nextSuggested = {
          ...current.proposal.suggested,
          title:
            field === 'title' ? value : current.proposal.suggested.title,
          slug: field === 'slug' ? value : current.proposal.suggested.slug,
          description:
            field === 'description' ? value : current.proposal.suggested.description,
          category:
            field === 'category' ? value : current.proposal.suggested.category,
          tags:
            field === 'tags'
              ? normalizeMetadataTags(parseTagList(value))
              : current.proposal.suggested.tags,
        }
        const nextProposal: MetadataProposalState = {
          ...current.proposal,
          suggested: nextSuggested,
          selected: {
            ...current.proposal.selected,
          },
        }

        if (!metadataFieldChanged(nextProposal, field)) {
          nextProposal.selected[field] = false
        }

        return {
          ...current,
          proposal: nextProposal,
        }
      })
    },
    [],
  )

  const acceptAllMetadata = useCallback(() => {
    setMetadataDialog((current) =>
      current
        ? {
            ...current,
            proposal: {
              ...current.proposal,
              selected: buildMetadataSelection(
                current.proposal.current,
                current.proposal.suggested,
                metadataProposalFieldsForTarget(current.target),
              ),
            },
          }
        : current,
    )
  }, [])

  const resetMetadataProposal = useCallback(() => {
    setMetadataDialog((current) =>
      current
        ? {
            ...current,
            proposal: {
              ...current.proposal,
              selected: createEmptyMetadataSelection(),
            },
          }
        : current,
    )
  }, [])

  const applyMetadataProposal = useCallback(() => {
    if (!metadataDialog) {
      return
    }

    const fields = metadataProposalFieldsForTarget(metadataDialog.target)
    if (!countSelectedMetadataFields(metadataDialog.proposal.selected, fields)) {
      toast.error('先勾选至少一项要合并的元数据。')
      return
    }

    const { selected, suggested } = metadataDialog.proposal

    startTransition(() => {
      if (metadataDialog.target === 'editor') {
        setEditor((current) =>
          current
            ? {
                ...current,
                title: selected.title ? suggested.title : current.title,
                description: selected.description
                  ? suggested.description
                  : current.description,
                category: selected.category ? suggested.category : current.category,
                tags: selected.tags ? suggested.tags.join(', ') : current.tags,
              }
            : current,
        )
      } else {
        setCreateForm((current) => ({
          ...current,
          title: selected.title ? suggested.title : current.title,
          slug: selected.slug ? suggested.slug : current.slug,
          description: selected.description
            ? suggested.description
            : current.description,
          category: selected.category ? suggested.category : current.category,
          tags: selected.tags ? suggested.tags.join(', ') : current.tags,
        }))
      }

      setMetadataDialog(null)
    })
    toast.success(
      metadataDialog.target === 'editor'
        ? '选中的 AI 元数据建议已合并到当前草稿。'
        : '选中的 AI 元数据建议已回填到新建草稿。',
    )
  }, [metadataDialog])

  const metadataDialogFields = metadataDialog
    ? metadataProposalFieldsForTarget(metadataDialog.target)
    : editorMetadataProposalFields

  const closeEditorDialog = useCallback(() => {
    navigate('/posts', { replace: true })
  }, [navigate])

  const closeCreateDialog = useCallback(() => {
    setCreateDialogOpen(false)
  }, [])

  const openCreateDialog = useCallback(() => {
    navigate('/posts', { replace: true })
    setCreateMode('workspace')
    setCreatePanels(defaultWorkbenchPanels)
    setCreateDialogOpen(true)
  }, [navigate])

  return (
    <div className="space-y-6">
      <input
        ref={importInputRef}
        type="file"
        accept=".md,.markdown,text/markdown,text/plain"
        multiple
        className="hidden"
        onChange={(event) => {
          void importMarkdownFiles(event.target.files)
        }}
      />
      <input
        ref={folderImportInputRef}
        type="file"
        multiple
        className="hidden"
        onChange={(event) => {
          void importMarkdownFiles(event.target.files)
        }}
      />

      <div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
        <div className="space-y-3">
          <Badge variant="secondary">文章</Badge>
          <div>
            <h2 className="text-3xl font-semibold tracking-tight">内容库</h2>
            <p className="mt-2 max-w-3xl text-sm leading-7 text-muted-foreground">
              现在可以直接在后台使用 VS Code 风格编辑器维护 Markdown，支持整文件夹导入、AI 元数据对比回填、AI 封面图生成，并把预览、diff 和 AI 润色全部收进同一个 Monaco 工作台里。
            </p>
          </div>
        </div>

        <div className="flex flex-wrap items-center gap-3">
          <Button variant="outline" onClick={openCreateDialog} data-testid="posts-open-create">
            <FilePlus2 className="h-4 w-4" />
            新建草稿
          </Button>
          <Button
            variant="outline"
            onClick={() => importInputRef.current?.click()}
            disabled={importing}
          >
            <FileUp className="h-4 w-4" />
            {importing ? '导入中...' : '导入 Markdown'}
          </Button>
          <Button
            variant="outline"
            onClick={() => folderImportInputRef.current?.click()}
            disabled={importing}
          >
            <FolderOpen className="h-4 w-4" />
            {importing ? '导入中...' : '导入文件夹'}
          </Button>
          <Button variant="secondary" onClick={() => void loadPosts(true)} disabled={refreshing}>
            <RefreshCcw className="h-4 w-4" />
            {refreshing ? '刷新中...' : '刷新'}
          </Button>
        </div>
      </div>

      <div className="space-y-5">
        <Card className="rounded-[2rem]">
          <CardHeader className="gap-4 border-b border-border/70 bg-background/70 pb-5">
            <div className="flex items-start justify-between gap-3">
              <div>
                <CardTitle>文章列表</CardTitle>
                <CardDescription>
                  保持列表浏览，搜索、筛选、翻页都在这里完成；新建和编辑统一在页内窗口里处理。
                </CardDescription>
              </div>
              <Badge variant="outline">{paginatedPosts.length} / {totalPosts}</Badge>
            </div>
            <div className="grid gap-3">
              <div className="flex flex-col gap-3 lg:flex-row">
                <Input
                  data-testid="posts-search"
                  className="flex-1"
                  placeholder="搜索标题、slug、分类、标签或摘要"
                  value={searchTerm}
                  onChange={(event) => setSearchTerm(event.target.value)}
                />
                {searchTerm ? (
                  <Button variant="outline" onClick={() => setSearchTerm('')}>
                    <X className="h-4 w-4" />
                    清空搜索
                  </Button>
                ) : null}
              </div>
              <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
                <Select value={typeFilter} onChange={(event) => setTypeFilter(event.target.value)}>
                  <option value="all">全部类型</option>
                  <option value="article">文章</option>
                  <option value="note">笔记</option>
                  <option value="page">页面</option>
                  <option value="snippet">片段</option>
                </Select>
                <Select
                  value={pinnedFilter}
                  onChange={(event) => setPinnedFilter(event.target.value)}
                >
                  <option value="all">全部置顶状态</option>
                  <option value="pinned">仅看置顶</option>
                  <option value="regular">仅看普通</option>
                </Select>
                <Select
                  value={String(pageSize)}
                  onChange={(event) => setPageSize(Number(event.target.value))}
                >
                  {POSTS_PAGE_SIZE_OPTIONS.map((option) => (
                    <option key={option} value={option}>
                      每页 {option} 条
                    </option>
                  ))}
                </Select>
                <Select value={sortKey} onChange={(event) => setSortKey(event.target.value)}>
                  <option value="updated_at_desc">最近更新优先</option>
                  <option value="created_at_desc">最新创建优先</option>
                  <option value="created_at_asc">最早创建优先</option>
                  <option value="title_asc">标题 A → Z</option>
                  <option value="title_desc">标题 Z → A</option>
                </Select>
              </div>
            </div>
            <div className="flex flex-wrap gap-2">
              <Badge variant="secondary">匹配 {totalPosts}</Badge>
              <Badge variant="outline">当前页置顶 {pinnedPostCount}</Badge>
              <Badge variant="outline">
                第 {safeCurrentPage} / {totalPages} 页
              </Badge>
              <Badge variant="outline">
                {editor ? '正在编辑' : createDialogOpen ? '正在新建' : '列表浏览'}
              </Badge>
            </div>
          </CardHeader>
          <CardContent className="pt-5 xl:flex xl:min-h-0 xl:flex-col xl:overflow-hidden">
            {loading ? (
              <Skeleton className="h-[620px] rounded-3xl" />
            ) : (
              <div className="space-y-4 xl:flex xl:min-h-0 xl:flex-1 xl:flex-col xl:overflow-hidden">
                <div className="space-y-2 xl:min-h-0 xl:flex-1 xl:overflow-y-auto xl:pr-1">
                  {paginatedPosts.map((post) => {
                    const active = post.slug === slug

                    return (
                      <button
                        key={post.id}
                        type="button"
                        data-testid={`post-item-${post.slug}`}
                        onClick={() => navigate(`/posts/${post.slug}`)}
                        className={cn(
                          'w-full rounded-[1.45rem] border px-4 py-3.5 text-left transition-all',
                          active
                            ? 'border-primary/30 bg-primary/10 shadow-[0_18px_42px_rgba(37,99,235,0.16)]'
                            : 'border-border/70 bg-background/60 hover:border-border hover:bg-accent/40',
                        )}
                      >
                        <div className="flex items-start justify-between gap-3">
                          <div className="min-w-0 flex-1">
                            <div className="flex flex-wrap items-center gap-2">
                              <span className="truncate font-medium">
                                {post.title ?? '未命名文章'}
                              </span>
                              <Badge variant="secondary">{formatPostType(post.post_type)}</Badge>
                              {post.pinned ? <Badge variant="success">置顶</Badge> : null}
                            </div>
                            <p className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
                              {post.slug}
                            </p>
                          </div>
                          <span className="shrink-0 text-[11px] text-muted-foreground">
                            {formatDateTime(post.updated_at)}
                          </span>
                        </div>
                        <p className="mt-2 line-clamp-2 text-sm leading-6 text-muted-foreground">
                          {post.description ?? '暂无摘要。'}
                        </p>
                        <div className="mt-2.5 flex flex-wrap gap-2">
                          <Badge variant="outline">{post.category ?? '未分类'}</Badge>
                        </div>
                      </button>
                    )
                  })}

                  {!totalPosts ? (
                    <div className="rounded-[1.8rem] border border-dashed border-border/80 px-5 py-12 text-center text-sm text-muted-foreground">
                      当前筛选条件下没有匹配的文章。
                    </div>
                  ) : null}
                </div>

                {totalPosts ? (
                  <div className="rounded-[1.5rem] border border-border/70 bg-background/65 px-4 py-3">
                    <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
                      <p className="text-sm text-muted-foreground">
                        当前显示第 {pageStart} - {pageEnd} 条，共 {totalPosts} 条结果。
                      </p>
                      <div className="flex flex-wrap items-center gap-2">
                        <Button
                          variant="outline"
                          size="sm"
                          onClick={() => setCurrentPage((current) => Math.max(1, current - 1))}
                          disabled={safeCurrentPage === 1}
                        >
                          <ChevronLeft className="h-4 w-4" />
                          上一页
                        </Button>
                        {paginationItems.map((page) => (
                          <Button
                            key={page}
                            variant={page === safeCurrentPage ? 'secondary' : 'outline'}
                            size="sm"
                            onClick={() => setCurrentPage(page)}
                          >
                            {page}
                          </Button>
                        ))}
                        <Button
                          variant="outline"
                          size="sm"
                          onClick={() =>
                            setCurrentPage((current) => Math.min(totalPages, current + 1))
                          }
                          disabled={safeCurrentPage === totalPages}
                        >
                          下一页
                          <ChevronRight className="h-4 w-4" />
                        </Button>
                      </div>
                    </div>
                  </div>
                ) : null}
              </div>
            )}
          </CardContent>
        </Card>

        {editorLoading && slug ? (
          <Skeleton className="h-[1400px] rounded-3xl" />
        ) : editor ? (
          <div className="fixed inset-0 z-40 overflow-y-auto bg-slate-950/60 px-4 py-5 backdrop-blur-sm xl:px-8 xl:py-8">
            <div className="mx-auto grid max-w-[1820px] gap-5 xl:grid-cols-[minmax(0,1fr)_284px] 2xl:grid-cols-[minmax(0,1fr)_300px]">
              <Card className="xl:col-span-2 overflow-hidden rounded-[2rem] border-border/70 bg-background/92 shadow-[0_32px_100px_rgba(15,23,42,0.24)]">
                <div className="flex flex-col gap-4 px-5 py-5 lg:flex-row lg:items-center lg:justify-between">
                  <div className="space-y-3">
                    <div className="flex flex-wrap items-center gap-2">
                      <Badge variant="secondary">页内编辑窗口</Badge>
                      <Badge variant="outline">Esc 关闭</Badge>
                      {markdownDirty ? <Badge variant="warning">未保存</Badge> : null}
                    </div>
                    <div>
                      <h3 className="text-2xl font-semibold tracking-tight">编辑文章</h3>
                      <p className="mt-2 text-sm leading-6 text-muted-foreground">
                        文章列表保留在背后，随时可以返回；面板切换和 AI 润色统一放在 Monaco 顶栏里。
                      </p>
                    </div>
                  </div>
                  <div className="flex flex-wrap items-center gap-2">
                    <Button variant="outline" onClick={closeEditorDialog} data-testid="post-editor-close">
                      <ArrowLeft className="h-4 w-4" />
                      返回文章列表
                    </Button>
                    <Button variant="ghost" onClick={closeEditorDialog}>
                      <X className="h-4 w-4" />
                      关闭窗口
                    </Button>
                  </div>
                </div>
              </Card>

              <div className="order-2 space-y-5 xl:sticky xl:top-28 xl:self-start">
              <Card className="overflow-hidden rounded-[2rem] border-border/70 bg-background/80">
                <CardHeader className="gap-4 border-b border-border/70 bg-background/72">
                  <div className="flex flex-wrap items-center gap-2">
                    <CardTitle className="text-xl">{editor.title || editor.slug}</CardTitle>
                    <Badge variant="secondary">{formatPostType(editor.postType)}</Badge>
                    <Badge variant="outline">{formatPostStatus(editor.status)}</Badge>
                    <Badge variant="outline">{formatPostVisibility(editor.visibility)}</Badge>
                    {editor.pinned ? <Badge variant="success">置顶</Badge> : null}
                    {editor.noindex ? <Badge variant="warning">noindex</Badge> : null}
                    {markdownDirty ? <Badge variant="warning">未保存</Badge> : null}
                  </div>
                  <CardDescription className="font-mono text-xs">{editor.slug}</CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                  <div className="flex flex-wrap gap-2">
                    <Badge variant="success">+{compareStats.additions}</Badge>
                    <Badge variant="danger">-{compareStats.deletions}</Badge>
                    <Badge variant="outline">{editor.markdown.length} 字符</Badge>
                    <Badge variant="outline">{editor.markdown.split(/\r?\n/).length} 行</Badge>
                  </div>
                  <div className="rounded-2xl border border-border/70 bg-background/70 p-4">
                    <p className="text-sm text-muted-foreground">
                      创建于 {formatDateTime(editor.createdAt)} · 更新于 {formatDateTime(editor.updatedAt)}
                    </p>
                  </div>
                </CardContent>
              </Card>

              <Card className="rounded-[2rem]">
                <CardHeader className="pb-4">
                  <CardTitle className="text-base">文章属性</CardTitle>
                  <CardDescription>把 Medium 那种最常改的字段集中在一个安静的检查器里。</CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                  <FormField label="标题">
                    <Input
                      data-testid="post-editor-title"
                      value={editor.title}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, title: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                  <FormField
                    label="Slug"
                    hint="为避免 Markdown 路径漂移，当前不允许直接修改 slug。"
                  >
                    <Input value={editor.slug} disabled />
                  </FormField>
                  <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
                    <FormField label="分类">
                      <Input
                        value={editor.category}
                        onChange={(event) =>
                          setEditor((current) =>
                            current ? { ...current, category: event.target.value } : current,
                          )
                        }
                      />
                    </FormField>
                    <FormField label="文章类型">
                      <Select
                        value={editor.postType}
                        onChange={(event) =>
                          setEditor((current) =>
                            current ? { ...current, postType: event.target.value } : current,
                          )
                        }
                      >
                        <option value="article">文章</option>
                        <option value="note">笔记</option>
                        <option value="page">页面</option>
                        <option value="snippet">片段</option>
                      </Select>
                    </FormField>
                  </div>
                  <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
                    <FormField label="发布状态">
                      <Select
                        value={editor.status}
                        onChange={(event) =>
                          setEditor((current) =>
                            current ? { ...current, status: event.target.value } : current,
                          )
                        }
                      >
                        <option value="draft">草稿</option>
                        <option value="published">发布</option>
                        <option value="offline">下线</option>
                      </Select>
                    </FormField>
                    <FormField label="可见性">
                      <Select
                        value={editor.visibility}
                        onChange={(event) =>
                          setEditor((current) =>
                            current ? { ...current, visibility: event.target.value } : current,
                          )
                        }
                      >
                        <option value="public">公开</option>
                        <option value="unlisted">不公开（直链）</option>
                        <option value="private">私有（仅后台预览）</option>
                      </Select>
                    </FormField>
                  </div>
                  <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
                    <FormField label="定时发布">
                      <Input
                        type="datetime-local"
                        value={editor.publishAt}
                        onChange={(event) =>
                          setEditor((current) =>
                            current ? { ...current, publishAt: event.target.value } : current,
                          )
                        }
                      />
                    </FormField>
                    <FormField label="下线时间">
                      <Input
                        type="datetime-local"
                        value={editor.unpublishAt}
                        onChange={(event) =>
                          setEditor((current) =>
                            current ? { ...current, unpublishAt: event.target.value } : current,
                          )
                        }
                      />
                    </FormField>
                  </div>
                  <FormField label="标签" hint="多个标签请用英文逗号分隔。">
                    <Input
                      value={editor.tags}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, tags: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                  <FormField label="摘要">
                    <Textarea
                      value={editor.description}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, description: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                </CardContent>
              </Card>

              <Card className="rounded-[2rem]">
                <CardHeader className="pb-4">
                  <CardTitle className="text-base">媒体与发布</CardTitle>
                  <CardDescription>
                    参考 Ghost 的做法，把封面和发布开关放到正文之外，减少主编辑区噪音。
                  </CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                  <FormField label="封面图 URL" hint="可直接填写外链，也可以上传 / 抓取到媒体库后自动回填。">
                    <div className="space-y-3">
                      <Input
                        value={editor.image}
                        onChange={(event) =>
                          setEditor((current) =>
                            current ? { ...current, image: event.target.value } : current,
                          )
                        }
                      />
                      <MediaUrlControls
                        value={editor.image}
                        onChange={(image) =>
                          setEditor((current) => (current ? { ...current, image } : current))
                        }
                        prefix="post-covers/"
                        contextLabel="文章封面上传"
                        mode="cover"
                        remoteTitle={editor.title || editor.slug || '文章封面'}
                        dataTestIdPrefix="post-editor-cover"
                      />
                    </div>
                  </FormField>
                  <div className="flex flex-wrap items-center gap-3">
                    <Button
                      variant="outline"
                      onClick={() => void generateEditorCover()}
                      disabled={generatingEditorCover}
                    >
                      <WandSparkles className="h-4 w-4" />
                      {generatingEditorCover
                        ? '生成封面中...'
                        : editor.image.trim()
                          ? '重新生成 AI 封面'
                          : 'AI 生成封面'}
                    </Button>
                    {editorCoverPreviewUrl ? (
                      <Button variant="outline" asChild>
                        <a href={editorCoverPreviewUrl} target="_blank" rel="noreferrer">
                          预览封面
                        </a>
                      </Button>
                    ) : null}
                  </div>
                  {editorCoverPreviewUrl ? (
                    <div className="overflow-hidden rounded-[1.4rem] border border-border/70 bg-background/70">
                      <div className="border-b border-border/70 px-4 py-3">
                        <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                          当前封面预览
                        </p>
                      </div>
                      <img
                        src={editorCoverPreviewUrl}
                        alt={editor.title || editor.slug}
                        className="aspect-[16/9] w-full object-cover"
                      />
                    </div>
                  ) : null}
                  <FormField label="推文多图" hint="每行一个图片 URL，适合 tweet / 动态类内容。">
                    <Textarea
                      value={editor.imagesText}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, imagesText: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                  <FormField label="Canonical URL" hint="留空则使用默认文章地址。">
                    <Input
                      value={editor.canonicalUrl}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, canonicalUrl: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                  <FormField label="OG 图 URL" hint="留空则前台自动生成 SVG 分享图。">
                    <Input
                      value={editor.ogImage}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, ogImage: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                  <FormField label="旧地址重定向" hint="每行一个旧 slug，不带 /articles/ 前缀。">
                    <Textarea
                      value={editor.redirectFromText}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, redirectFromText: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                  <FormField label="强制跳转目标" hint="适合旧文跳新文；留空表示当前 slug 为主地址。">
                    <Input
                      value={editor.redirectTo}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, redirectTo: event.target.value } : current,
                        )
                      }
                    />
                  </FormField>
                  <label className="flex items-start gap-3 rounded-2xl border border-border/70 bg-background/60 p-4">
                    <input
                      type="checkbox"
                      checked={editor.noindex}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, noindex: event.target.checked } : current,
                        )
                      }
                      className="mt-1 h-4 w-4 rounded border-input text-primary focus:ring-ring"
                    />
                    <div>
                      <div className="font-medium">搜索引擎不收录（noindex）</div>
                      <div className="text-sm text-muted-foreground">
                        适合活动页、临时内容或只想保留直链访问的文章。
                      </div>
                    </div>
                  </label>
                  <label className="flex items-start gap-3 rounded-2xl border border-border/70 bg-background/60 p-4">
                    <input
                      type="checkbox"
                      checked={editor.pinned}
                      onChange={(event) =>
                        setEditor((current) =>
                          current ? { ...current, pinned: event.target.checked } : current,
                        )
                      }
                      className="mt-1 h-4 w-4 rounded border-input text-primary focus:ring-ring"
                    />
                    <div>
                      <div className="font-medium">置顶这篇文章</div>
                      <p className="mt-1 text-sm leading-6 text-muted-foreground">
                        置顶内容会在前台获得更高的展示优先级。
                      </p>
                    </div>
                  </label>
                </CardContent>
              </Card>
            </div>

            <Card className="order-1 min-w-0 overflow-hidden rounded-[2rem]">
              <CardHeader className="flex flex-col gap-5 border-b border-border/70 bg-background/70 xl:flex-row xl:items-start xl:justify-between">
                <div>
                  <div className="flex flex-wrap items-center gap-2">
                    <CardTitle>文章工作台</CardTitle>
                    <Badge variant="outline">
                      {formatWorkbenchStateLabel(editorMode, editorPanels)}
                    </Badge>
                  </div>
                  <CardDescription>
                    正文是主角；面板切换和 AI 润色在下方 Monaco 顶栏处理，这里只留保存与版本相关动作。
                  </CardDescription>
                </div>
                <div className="flex flex-wrap items-center gap-2">
                  <Button
                    variant="outline"
                    onClick={() => void generateEditorMetadataProposal()}
                    disabled={saving || generatingEditorMetadataProposal}
                  >
                    <Bot className="h-4 w-4" />
                    {generatingEditorMetadataProposal ? '分析中...' : 'AI 元信息'}
                  </Button>
                  <Button variant="outline" onClick={openEditorPreviewWindow}>
                    <ExternalLink className="h-4 w-4" />
                    独立预览
                  </Button>
                  <Button variant="outline" onClick={openEditorCompareWindow}>
                    <GitCompareArrows className="h-4 w-4" />
                    独立对比
                  </Button>
                  <Button variant="outline" onClick={openEditorPolishWindow}>
                    <WandSparkles className="h-4 w-4" />
                    独立润色
                  </Button>
                  <Button
                    variant="outline"
                    onClick={() => void localizeEditorMarkdownImages()}
                    disabled={saving || localizingEditorImages}
                  >
                    <Download className="h-4 w-4" />
                    {localizingEditorImages ? '本地化中...' : '正文图本地化'}
                  </Button>
                  <Button
                    variant="outline"
                    onClick={() => {
                      downloadMarkdownFile(`${editor.slug}.md`, buildDraftMarkdownForWindow(editor))
                      toast.success('Markdown 已导出。')
                    }}
                  >
                    <Download className="h-4 w-4" />
                    导出
                  </Button>
                  <Button
                    variant="outline"
                    onClick={() =>
                      setEditor((current) =>
                        current
                          ? {
                              ...current,
                              title: current.savedMeta.title,
                              description: current.savedMeta.description,
                              category: current.savedMeta.category,
                              postType: current.savedMeta.postType,
                              image: current.savedMeta.image,
                              imagesText: current.savedMeta.imagesText,
                              pinned: current.savedMeta.pinned,
                              tags: current.savedMeta.tags,
                              markdown: current.savedMarkdown,
                            }
                          : current,
                      )
                    }
                    disabled={!markdownDirty}
                  >
                    <RotateCcw className="h-4 w-4" />
                    恢复
                  </Button>
                  <Button onClick={() => void saveEditor()} disabled={saving} data-testid="post-editor-save">
                    <Save className="h-4 w-4" />
                    {saving ? '保存中...' : '保存'}
                  </Button>
                  <Button
                    data-testid="post-editor-delete"
                    variant="danger"
                    onClick={async () => {
                      if (!window.confirm(`确定删除“${editor.title || editor.slug}”吗？`)) {
                        return
                      }

                      try {
                        setDeleting(true)
                        await adminApi.deletePost(editor.slug)
                        toast.success('文章已删除。')
                        await loadPosts(false)
                        navigate('/posts', { replace: true })
                      } catch (error) {
                        toast.error(error instanceof ApiError ? error.message : '无法删除文章。')
                      } finally {
                        setDeleting(false)
                      }
                    }}
                    disabled={deleting}
                  >
                    <Trash2 className="h-4 w-4" />
                    {deleting ? '删除中...' : '删除'}
                  </Button>
                </div>
              </CardHeader>
              <CardContent className="space-y-4 pt-5">
                <div className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-[1.5rem] border border-border/70 bg-background/55 px-4 py-3 text-sm text-muted-foreground">
                  <span>当前草稿与已保存版本会一起参与对比</span>
                  <span>编辑区可切换为预览 / Diff / AI 润色</span>
                </div>

                <MarkdownWorkbench
                  value={editor.markdown}
                  originalValue={editor.savedMarkdown}
                  diffValue={buildDraftMarkdownForWindow(editor)}
                  path={editor.path}
                  workspaceHeightClassName="h-[clamp(620px,74dvh,920px)]"
                  mode={editorMode}
                  visiblePanels={editorPanels}
                  availablePanels={['edit', 'preview', 'diff']}
                  preview={<MarkdownPreview markdown={editor.markdown} />}
                  polishPanel={renderPolishPanel({
                    sourceMarkdown:
                      editorPolish?.sourceMarkdown ?? buildDraftMarkdownForWindow(editor),
                    mergedMarkdown: editorPolish ? editorMergedMarkdown : '',
                    hunks: editorPolishHunks,
                    selectedIds: editorPolish?.selectedIds ?? new Set<string>(),
                    generating: generatingEditorPolish,
                    onGenerate: () => void generateEditorPolish(),
                    onToggle: toggleEditorPolishHunk,
                    onApply: applyEditorPolish,
                    onReset: resetEditorPolish,
                    onAcceptAll: acceptAllEditorPolish,
                    title: '当前文章 AI 润色工作台',
                    description:
                      '左侧是整篇文章的合并结果，右侧按改动块选择要保留的润色内容。',
                    modelKey: `editor-${editor.slug}`,
                  })}
                  originalLabel="已保存版本"
                  modifiedLabel="当前草稿"
                  onModeChange={setEditorMode}
                  onVisiblePanelsChange={setEditorPanels}
                  onChange={(next) =>
                    setEditor((current) => (current ? { ...current, markdown: next } : current))
                  }
                />
              </CardContent>
            </Card>
          </div>
          </div>
        ) : createDialogOpen ? (
          <div className="fixed inset-0 z-40 overflow-y-auto bg-slate-950/60 px-4 py-5 backdrop-blur-sm xl:px-8 xl:py-8">
            <div className="mx-auto grid max-w-[1820px] gap-5 xl:grid-cols-[minmax(0,1fr)_284px] 2xl:grid-cols-[minmax(0,1fr)_300px]">
              <Card className="xl:col-span-2 overflow-hidden rounded-[2rem] border-border/70 bg-background/92 shadow-[0_32px_100px_rgba(15,23,42,0.24)]">
                <div className="flex flex-col gap-4 px-5 py-5 lg:flex-row lg:items-center lg:justify-between">
                  <div className="space-y-3">
                    <div className="flex flex-wrap items-center gap-2">
                      <Badge variant="secondary">页内新建窗口</Badge>
                      <Badge variant="outline">Esc 关闭</Badge>
                      {createMarkdownDirty ? <Badge variant="warning">草稿已修改</Badge> : null}
                    </div>
                    <div>
                      <h3 className="text-2xl font-semibold tracking-tight">新建文章</h3>
                      <p className="mt-2 text-sm leading-6 text-muted-foreground">
                        列表留在背后，正文在这里独立起草；元信息检查器在右边，面板切换和 AI 润色在 Monaco 顶栏。
                      </p>
                    </div>
                  </div>
                  <div className="flex flex-wrap items-center gap-2">
                    <Button variant="outline" onClick={closeCreateDialog}>
                      <ArrowLeft className="h-4 w-4" />
                      返回文章列表
                    </Button>
                    <Button variant="ghost" onClick={closeCreateDialog}>
                      <X className="h-4 w-4" />
                      关闭窗口
                    </Button>
                  </div>
                </div>
              </Card>

              <div className="order-2 space-y-5 xl:sticky xl:top-28 xl:self-start">
              <Card className="overflow-hidden rounded-[2rem] border-border/70 bg-background/80">
                <CardHeader className="gap-4 border-b border-border/70 bg-background/72">
                  <div className="flex flex-wrap items-center gap-2">
                    <CardTitle className="text-xl">新建文章</CardTitle>
                    {createMarkdownDirty ? <Badge variant="warning">草稿已修改</Badge> : null}
                  </div>
                  <CardDescription>
                    用更接近 Medium 的写作流来起草内容，右边只放必要设置。
                  </CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                  <div className="flex flex-wrap gap-2">
                    <Badge variant="outline">{createForm.markdown.length} 字符</Badge>
                    <Badge variant="outline">{createForm.markdown.split(/\r?\n/).length} 行</Badge>
                    <Badge variant="secondary">AI 元信息对比回填</Badge>
                    <Badge variant="outline">AI 封面</Badge>
                    <Badge variant="outline">{formatPostStatus(createForm.status)}</Badge>
                    <Badge variant="outline">{formatPostVisibility(createForm.visibility)}</Badge>
                  </div>
                  <div className="rounded-2xl border border-border/70 bg-background/70 p-4 text-sm leading-6 text-muted-foreground">
                    先写正文，再让 AI 生成标题、摘要、分类、标签和 slug；确认后顺手生成封面图，最后再润色和创建草稿。
                  </div>
                </CardContent>
              </Card>

              <Card className="rounded-[2rem]">
                <CardHeader className="pb-4">
                  <CardTitle className="text-base">元信息</CardTitle>
                  <CardDescription>最常改的字段集中在这里，适合写作时快速扫一眼。</CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                  <FormField label="标题">
                    <Input
                      data-testid="post-create-title"
                      value={createForm.title}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, title: event.target.value }))
                      }
                    />
                  </FormField>
                  <FormField label="Slug" hint="留空则根据标题自动生成。">
                    <Input
                      data-testid="post-create-slug"
                      value={createForm.slug}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, slug: event.target.value }))
                      }
                    />
                  </FormField>
                  <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
                    <FormField label="分类">
                      <Input
                        value={createForm.category}
                        onChange={(event) =>
                          setCreateForm((current) => ({ ...current, category: event.target.value }))
                        }
                      />
                    </FormField>
                    <FormField label="文章类型">
                      <Select
                        value={createForm.postType}
                        onChange={(event) =>
                          setCreateForm((current) => ({ ...current, postType: event.target.value }))
                        }
                      >
                        <option value="article">文章</option>
                        <option value="note">笔记</option>
                        <option value="page">页面</option>
                        <option value="snippet">片段</option>
                      </Select>
                    </FormField>
                  </div>
                  <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
                    <FormField label="发布状态">
                      <Select
                        value={createForm.status}
                        onChange={(event) =>
                          setCreateForm((current) => ({ ...current, status: event.target.value }))
                        }
                      >
                        <option value="draft">草稿</option>
                        <option value="published">发布</option>
                        <option value="offline">下线</option>
                      </Select>
                    </FormField>
                    <FormField label="可见性">
                      <Select
                        value={createForm.visibility}
                        onChange={(event) =>
                          setCreateForm((current) => ({ ...current, visibility: event.target.value }))
                        }
                      >
                        <option value="public">公开</option>
                        <option value="unlisted">不公开（直链）</option>
                        <option value="private">私有（仅后台预览）</option>
                      </Select>
                    </FormField>
                  </div>
                  <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
                    <FormField label="定时发布">
                      <Input
                        type="datetime-local"
                        value={createForm.publishAt}
                        onChange={(event) =>
                          setCreateForm((current) => ({ ...current, publishAt: event.target.value }))
                        }
                      />
                    </FormField>
                    <FormField label="下线时间">
                      <Input
                        type="datetime-local"
                        value={createForm.unpublishAt}
                        onChange={(event) =>
                          setCreateForm((current) => ({ ...current, unpublishAt: event.target.value }))
                        }
                      />
                    </FormField>
                  </div>
                  <FormField label="标签" hint="多个标签请用英文逗号分隔。">
                    <Input
                      value={createForm.tags}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, tags: event.target.value }))
                      }
                    />
                  </FormField>
                  <FormField label="摘要">
                    <Textarea
                      value={createForm.description}
                      onChange={(event) =>
                        setCreateForm((current) => ({
                          ...current,
                          description: event.target.value,
                        }))
                      }
                    />
                  </FormField>
                </CardContent>
              </Card>

              <Card className="rounded-[2rem]">
                <CardHeader className="pb-4">
                  <CardTitle className="text-base">媒体与发布</CardTitle>
                  <CardDescription>
                    参考 Ghost，把封面和发布设置放到独立检查器，不让正文区域挤满辅助信息。
                  </CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                  <FormField label="封面图 URL" hint="可直接填写外链，也可以上传 / 抓取到媒体库后自动回填。">
                    <div className="space-y-3">
                      <Input
                        value={createForm.image}
                        onChange={(event) =>
                          setCreateForm((current) => ({ ...current, image: event.target.value }))
                        }
                      />
                      <MediaUrlControls
                        value={createForm.image}
                        onChange={(image) =>
                          setCreateForm((current) => ({ ...current, image }))
                        }
                        prefix="post-covers/"
                        contextLabel="新建文章封面上传"
                        mode="cover"
                        remoteTitle={createForm.title || createForm.slug || '文章封面'}
                        dataTestIdPrefix="post-create-cover"
                      />
                    </div>
                  </FormField>
                  <div className="flex flex-wrap items-center gap-3">
                    <Button
                      variant="outline"
                      onClick={() => void generateCreateCover()}
                      disabled={generatingCreateCover}
                    >
                      <WandSparkles className="h-4 w-4" />
                      {generatingCreateCover
                        ? '生成封面中...'
                        : createForm.image.trim()
                          ? '重新生成 AI 封面'
                          : 'AI 生成封面'}
                    </Button>
                    {createCoverPreviewUrl ? (
                      <Button variant="outline" asChild>
                        <a href={createCoverPreviewUrl} target="_blank" rel="noreferrer">
                          预览封面
                        </a>
                      </Button>
                    ) : null}
                  </div>
                  {createCoverPreviewUrl ? (
                    <div className="overflow-hidden rounded-[1.4rem] border border-border/70 bg-background/70">
                      <div className="border-b border-border/70 px-4 py-3">
                        <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                          当前封面预览
                        </p>
                      </div>
                      <img
                        src={createCoverPreviewUrl}
                        alt={createForm.title || createForm.slug || '新建草稿封面'}
                        className="aspect-[16/9] w-full object-cover"
                      />
                    </div>
                  ) : null}
                  <FormField label="推文多图" hint="每行一个图片 URL，创建后前台会按多图渲染。">
                    <Textarea
                      value={createForm.imagesText}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, imagesText: event.target.value }))
                      }
                    />
                  </FormField>
                  <FormField label="Canonical URL" hint="留空时使用默认文章地址。">
                    <Input
                      value={createForm.canonicalUrl}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, canonicalUrl: event.target.value }))
                      }
                    />
                  </FormField>
                  <FormField label="OG 图 URL" hint="留空则由前台自动生成。">
                    <Input
                      value={createForm.ogImage}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, ogImage: event.target.value }))
                      }
                    />
                  </FormField>
                  <FormField label="旧地址重定向" hint="每行一个旧 slug。">
                    <Textarea
                      value={createForm.redirectFromText}
                      onChange={(event) =>
                        setCreateForm((current) => ({
                          ...current,
                          redirectFromText: event.target.value,
                        }))
                      }
                    />
                  </FormField>
                  <FormField label="强制跳转目标" hint="可选：创建即作为跳转占位。">
                    <Input
                      value={createForm.redirectTo}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, redirectTo: event.target.value }))
                      }
                    />
                  </FormField>
                  <label className="flex items-start gap-3 rounded-2xl border border-border/70 bg-background/60 p-4">
                    <input
                      type="checkbox"
                      checked={createForm.noindex}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, noindex: event.target.checked }))
                      }
                      className="mt-1 h-4 w-4 rounded border-input text-primary focus:ring-ring"
                    />
                    <div>
                      <div className="font-medium">创建时设置 noindex</div>
                      <div className="text-sm text-muted-foreground">
                        适合临时活动页、内测内容或不希望进 sitemap / RSS 的文章。
                      </div>
                    </div>
                  </label>
                  <label className="flex items-start gap-3 rounded-2xl border border-border/70 bg-background/60 p-4">
                    <input
                      type="checkbox"
                      checked={createForm.pinned}
                      onChange={(event) =>
                        setCreateForm((current) => ({ ...current, pinned: event.target.checked }))
                      }
                      className="mt-1 h-4 w-4 rounded border-input text-primary focus:ring-ring"
                    />
                    <div>
                      <div className="font-medium">创建时直接置顶</div>
                      <p className="mt-1 text-sm leading-6 text-muted-foreground">
                        如果希望内容一发布就优先展示，可以勾选这里。
                      </p>
                    </div>
                  </label>
                </CardContent>
              </Card>
            </div>

            <Card className="order-1 min-w-0 overflow-hidden rounded-[2rem]">
              <CardHeader className="flex flex-col gap-5 border-b border-border/70 bg-background/70 xl:flex-row xl:items-start xl:justify-between">
                <div>
                  <div className="flex flex-wrap items-center gap-2">
                    <CardTitle>新建草稿工作台</CardTitle>
                    <Badge variant="outline">
                      {formatWorkbenchStateLabel(createMode, createPanels)}
                    </Badge>
                  </div>
                  <CardDescription>
                    正文优先；面板切换和 AI 润色都放在下方 Monaco 顶栏，这里只保留草稿级动作。
                  </CardDescription>
                </div>
                <div className="flex flex-wrap items-center gap-2">
                  <Button variant="outline" onClick={openCreatePreviewWindow}>
                    <ExternalLink className="h-4 w-4" />
                    独立预览
                  </Button>
                  <Button variant="outline" onClick={openCreateCompareWindow}>
                    <GitCompareArrows className="h-4 w-4" />
                    独立对比
                  </Button>
                  <Button variant="outline" onClick={openCreatePolishWindow}>
                    <WandSparkles className="h-4 w-4" />
                    独立润色
                  </Button>
                  <Button
                    variant="outline"
                    onClick={() => void localizeCreateMarkdownImages()}
                    disabled={creating || localizingCreateImages}
                  >
                    <Download className="h-4 w-4" />
                    {localizingCreateImages ? '本地化中...' : '正文图本地化'}
                  </Button>
                  <Button
                    variant="outline"
                    onClick={() => {
                      const exportName = (createForm.slug.trim() || 'untitled-post').replace(
                        /\s+/g,
                        '-',
                      )
                      downloadMarkdownFile(`${exportName}.md`, buildCreateMarkdownForWindow(createForm))
                      toast.success('草稿 Markdown 已导出。')
                    }}
                  >
                    <Download className="h-4 w-4" />
                    导出
                  </Button>
                  <Button
                    variant="outline"
                    onClick={() => void generateCreateMetadata()}
                    disabled={creating || generatingMetadata}
                  >
                    <Bot className="h-4 w-4" />
                    {generatingMetadata ? '生成中...' : 'AI 元信息'}
                  </Button>
                  <Button
                    variant="outline"
                    onClick={() => {
                      setCreateForm(defaultCreateForm)
                      setCreatePolish(null)
                      setCreateMode('workspace')
                      setCreatePanels(defaultWorkbenchPanels)
                    }}
                    disabled={!createMarkdownDirty}
                  >
                    <RotateCcw className="h-4 w-4" />
                    恢复模板
                  </Button>
                  <Button
                    data-testid="post-create-submit"
                    onClick={async () => {
                      if (!createForm.title.trim()) {
                        toast.error('创建文章时必须填写标题。')
                        return
                      }

                      try {
                        setCreating(true)
                        const created = await adminApi.createPost(buildCreatePayload(createForm))
                        toast.success('草稿已创建。')
                        setCreateDialogOpen(false)
                        setCreateForm(defaultCreateForm)
                        setCreatePolish(null)
                        setCreateMode('workspace')
                        setCreatePanels(defaultWorkbenchPanels)
                        await loadPosts(false)
                        navigate(`/posts/${created.slug}`)
                      } catch (error) {
                        toast.error(error instanceof ApiError ? error.message : '无法创建草稿。')
                      } finally {
                        setCreating(false)
                      }
                    }}
                    disabled={creating}
                  >
                    <PencilLine className="h-4 w-4" />
                    {creating ? '创建中...' : '创建'}
                  </Button>
                </div>
              </CardHeader>
              <CardContent className="space-y-4 pt-5">
                <div className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-[1.5rem] border border-border/70 bg-background/55 px-4 py-3 text-sm text-muted-foreground">
                  <span>从正文出发生成元信息，再回到正文继续写作</span>
                  <span>草稿可随时切到预览 / Diff / AI 润色</span>
                </div>

                <MarkdownWorkbench
                  value={createForm.markdown}
                  originalValue={buildCreateMarkdownForWindow(defaultCreateForm)}
                  diffValue={buildCreateMarkdownForWindow(createForm)}
                  path={buildVirtualPostPath(createForm.slug)}
                  workspaceHeightClassName="h-[clamp(620px,74dvh,920px)]"
                  mode={createMode}
                  visiblePanels={createPanels}
                  preview={<MarkdownPreview markdown={createForm.markdown} />}
                  availablePanels={['edit', 'preview', 'diff']}
                  polishPanel={renderPolishPanel({
                    sourceMarkdown:
                      createPolish?.sourceMarkdown ?? buildCreateMarkdownForWindow(createForm),
                    mergedMarkdown: createPolish ? createMergedMarkdown : '',
                    hunks: createPolishHunks,
                    selectedIds: createPolish?.selectedIds ?? new Set<string>(),
                    generating: generatingCreatePolish,
                    onGenerate: () => void generateCreatePolish(),
                    onToggle: toggleCreatePolishHunk,
                    onApply: applyCreatePolish,
                    onReset: resetCreatePolish,
                    onAcceptAll: acceptAllCreatePolish,
                    title: '新建草稿 AI 润色工作台',
                    description:
                      '润色会同时覆盖元信息和正文，你可以像合并补丁一样逐块决定是否采用。',
                    modelKey: `create-${createForm.slug.trim() || 'new-post'}`,
                  })}
                  originalLabel="初始模板"
                  modifiedLabel="当前草稿"
                  onModeChange={setCreateMode}
                  onVisiblePanelsChange={setCreatePanels}
                  onChange={(next) =>
                    setCreateForm((current) => ({
                      ...current,
                      markdown: next,
                    }))
                  }
                />
              </CardContent>
            </Card>
          </div>
          </div>
        ) : null}
      </div>

      {metadataDialog ? (
        <div className="fixed inset-0 z-50 bg-slate-950/70 px-4 py-5 backdrop-blur-sm xl:px-8 xl:py-8">
          <div className="mx-auto flex h-full w-full max-w-7xl flex-col overflow-hidden rounded-[32px] border border-border/70 bg-background shadow-[0_40px_120px_rgba(15,23,42,0.45)]">
            <div className="flex flex-col gap-4 border-b border-border/70 bg-background/95 px-6 py-5 xl:flex-row xl:items-start xl:justify-between">
              <div className="space-y-3">
                <Badge variant="secondary">AI 元数据工作窗</Badge>
                <div>
                  <h3 className="text-2xl font-semibold tracking-tight">
                    {metadataDialog.target === 'editor'
                      ? '文章元信息对比与回填'
                      : '新建草稿元信息对比与回填'}
                  </h3>
                  <p className="mt-2 max-w-3xl text-sm leading-7 text-muted-foreground">
                    对比当前值和 AI 建议，按字段决定是否回填。右侧建议值支持直接修改，确认后只会回填到当前表单，不会自动保存。
                  </p>
                </div>
              </div>

              <div className="flex flex-wrap items-center gap-3">
                <Button variant="outline" onClick={acceptAllMetadata}>
                  全部勾选变更项
                </Button>
                <Button variant="outline" onClick={resetMetadataProposal}>
                  全部保留当前
                </Button>
                <Button
                  onClick={applyMetadataProposal}
                  disabled={
                    !countSelectedMetadataFields(
                      metadataDialog.proposal.selected,
                      metadataDialogFields,
                    )
                  }
                >
                  <Save className="h-4 w-4" />
                  回填已勾选项
                </Button>
                <Button
                  variant="ghost"
                  onClick={() => setMetadataDialog(null)}
                >
                  <X className="h-4 w-4" />
                  关闭对比窗
                </Button>
              </div>
            </div>

            <div className="grid min-h-0 flex-1 gap-0 xl:grid-cols-[320px_minmax(0,1fr)]">
              <aside className="border-b border-border/70 bg-muted/20 xl:border-b-0 xl:border-r">
                <div className="space-y-4 p-6">
                  <div className="rounded-[1.75rem] border border-border/70 bg-background/80 p-4">
                    <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                      当前内容
                    </p>
                    <p className="mt-3 text-base font-semibold">
                      {metadataDialog.title}
                    </p>
                  </div>

                  <div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
                    <div className="rounded-2xl border border-border/70 bg-background/80 p-4">
                      <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                        检测到变化
                      </p>
                      <p className="mt-3 text-3xl font-semibold">
                        {countChangedMetadataFields(
                          metadataDialog.proposal,
                          metadataDialogFields,
                        )}
                      </p>
                    </div>
                    <div className="rounded-2xl border border-border/70 bg-background/80 p-4">
                      <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                        已勾选合并
                      </p>
                      <p className="mt-3 text-3xl font-semibold">
                        {countSelectedMetadataFields(
                          metadataDialog.proposal.selected,
                          metadataDialogFields,
                        )}
                      </p>
                    </div>
                    <div className="rounded-2xl border border-border/70 bg-background/80 p-4">
                      <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                        说明
                      </p>
                      <p className="mt-3 text-sm leading-6 text-muted-foreground">
                        {metadataDialog.target === 'editor'
                          ? '标题、摘要、分类、标签支持单独回填；现有文章的 slug 继续锁定，避免 Markdown 路径漂移。'
                          : '标题、slug、摘要、分类和标签都支持单独回填；你也可以先修改 AI 建议再应用。'}
                      </p>
                    </div>
                  </div>
                </div>
              </aside>

              <div className="min-h-0 overflow-y-auto bg-background">
                <div className="space-y-4 p-6">
                  {metadataDialogFields.map((field) => {
                    const changed = metadataFieldChanged(metadataDialog.proposal, field)
                    const selected = metadataDialog.proposal.selected[field]
                    const currentValue = metadataDialog.proposal.current[field]
                    const suggestedValue = metadataDialog.proposal.suggested[field]
                    const isTagsField = field === 'tags'
                    const isDescriptionField = field === 'description'
                    const suggestedText = Array.isArray(suggestedValue)
                      ? suggestedValue.join(', ')
                      : suggestedValue

                    return (
                      <section
                        key={field}
                        className={cn(
                          'overflow-hidden rounded-[1.9rem] border transition-colors',
                          selected && changed
                            ? 'border-emerald-500/40 bg-emerald-500/5'
                            : 'border-border/70 bg-background/80',
                        )}
                      >
                        <div className="flex flex-col gap-3 border-b border-border/70 px-5 py-4 xl:flex-row xl:items-center xl:justify-between">
                          <div className="flex flex-wrap items-center gap-3">
                            <h4 className="text-base font-semibold">
                              {metadataFieldLabel(field)}
                            </h4>
                            <Badge
                              variant={
                                changed
                                  ? selected
                                    ? 'success'
                                    : 'warning'
                                  : 'outline'
                              }
                            >
                              {changed
                                ? selected
                                  ? '准备合并'
                                  : '保留当前'
                                : '无变化'}
                            </Badge>
                          </div>

                          <Button
                            variant={selected && changed ? 'default' : 'outline'}
                            size="sm"
                            onClick={() => toggleMetadataField(field)}
                            disabled={!changed}
                          >
                            {selected && changed ? '已选择这项' : changed ? '合并这项' : '无需处理'}
                          </Button>
                        </div>

                        <div className="grid gap-4 p-5 xl:grid-cols-2">
                          <div className="rounded-2xl border border-border/70 bg-muted/20 p-4">
                            <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                              当前值
                            </p>
                            {isTagsField ? (
                              Array.isArray(currentValue) && currentValue.length ? (
                                <div className="mt-3 flex flex-wrap gap-2">
                                  {currentValue.map((tag) => (
                                    <Badge key={`${field}-current-${tag}`} variant="outline">
                                      {tag}
                                    </Badge>
                                  ))}
                                </div>
                              ) : (
                                <p className="mt-3 text-sm text-muted-foreground">未填写</p>
                              )
                            ) : (
                              <p className="mt-3 whitespace-pre-wrap break-words text-sm leading-6">
                                {typeof currentValue === 'string' && currentValue.trim()
                                  ? currentValue
                                  : '未填写'}
                              </p>
                            )}
                          </div>

                          <div className="rounded-2xl border border-border/70 bg-background p-4">
                            <p className="text-[11px] uppercase tracking-[0.22em] text-muted-foreground">
                              AI 建议，可继续修改
                            </p>
                            {isTagsField ? (
                              <div className="mt-3 space-y-3">
                                <Input
                                  value={suggestedText}
                                  onChange={(event) =>
                                    updateMetadataSuggestedField(field, event.target.value)
                                  }
                                  placeholder="多个标签请用英文逗号分隔"
                                />
                                {Array.isArray(suggestedValue) && suggestedValue.length ? (
                                  <div className="flex flex-wrap gap-2">
                                    {suggestedValue.map((tag) => (
                                      <Badge
                                        key={`${field}-suggested-${tag}`}
                                        variant={selected && changed ? 'success' : 'secondary'}
                                      >
                                        {tag}
                                      </Badge>
                                    ))}
                                  </div>
                                ) : (
                                  <p className="text-sm text-muted-foreground">未填写</p>
                                )}
                              </div>
                            ) : isDescriptionField ? (
                              <Textarea
                                className="mt-3 min-h-28"
                                value={typeof suggestedValue === 'string' ? suggestedValue : ''}
                                onChange={(event) =>
                                  updateMetadataSuggestedField(field, event.target.value)
                                }
                                placeholder="可以继续润色 AI 生成的摘要"
                              />
                            ) : (
                              <Input
                                className="mt-3"
                                value={typeof suggestedValue === 'string' ? suggestedValue : ''}
                                onChange={(event) =>
                                  updateMetadataSuggestedField(field, event.target.value)
                                }
                                placeholder={`填写${metadataFieldLabel(field)}`}
                              />
                            )}
                            {!isTagsField && !isDescriptionField ? (
                              <p className="mt-3 whitespace-pre-wrap break-words text-sm leading-6 text-muted-foreground">
                                {typeof suggestedValue === 'string' && suggestedValue.trim()
                                  ? suggestedValue
                                  : '未填写'}
                              </p>
                            ) : null}
                            {isDescriptionField ? (
                              <p className="mt-3 text-sm leading-6 text-muted-foreground">
                                {typeof suggestedValue === 'string' && suggestedValue.trim()
                                  ? suggestedValue
                                  : '未填写'}
                              </p>
                            ) : null}
                          </div>
                        </div>
                      </section>
                    )
                  })}
                </div>
              </div>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  )
}
