import { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Book, BookOpen, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2, PanelLeftClose, PanelLeftOpen, Wand2, Volume2, Bot, Headphones, Clock, CheckCircle2, AlertCircle, CircleDot } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Navbar } from '@/components/Navbar'
import { AudioPlayer } from '@/components/AudioPlayer'
import { ChapterPlayer } from '@/components/ChapterPlayer'
import { audiobookApi, type AudiobookProject, type AudiobookProjectDetail, type AudiobookCharacter, type AudiobookSegment, type ScriptGenerationRequest, type NsfwScriptGenerationRequest } from '@/lib/api/audiobook'
import { RotateCcw } from 'lucide-react'
import apiClient, { formatApiError, adminApi, authApi } from '@/lib/api'
import { useAuth } from '@/contexts/AuthContext'

function LazyAudioPlayer({ audioUrl, jobId, compact }: { audioUrl: string; jobId: number; compact?: boolean }) {
  const [visible, setVisible] = useState(false)
  const ref = useRef<HTMLDivElement>(null)
  useEffect(() => {
    const el = ref.current
    if (!el) return
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) { setVisible(true); observer.disconnect() } },
      { rootMargin: '120px' }
    )
    observer.observe(el)
    return () => observer.disconnect()
  }, [])
  return <div ref={ref}>{visible && <AudioPlayer audioUrl={audioUrl} jobId={jobId} compact={compact} />}</div>
}

const STATUS_CONFIG: Record<string, { Icon: React.ElementType; iconCls: string; cardBg: string; cardBgSel: string }> = {
  pending:          { Icon: Clock,        iconCls: 'h-3 w-3 text-muted-foreground/40',         cardBg: 'bg-background/50',  cardBgSel: 'bg-muted' },
  analyzing:        { Icon: Loader2,      iconCls: 'h-3 w-3 text-sky-500 animate-spin',        cardBg: 'bg-sky-500/5',      cardBgSel: 'bg-sky-500/20' },
  characters_ready: { Icon: Loader2,      iconCls: 'h-3 w-3 text-violet-500 animate-spin',     cardBg: 'bg-violet-500/5',   cardBgSel: 'bg-violet-500/20' },
  parsing:          { Icon: Loader2,      iconCls: 'h-3 w-3 text-amber-500 animate-spin',      cardBg: 'bg-amber-500/5',    cardBgSel: 'bg-amber-500/20' },
  processing:       { Icon: Loader2,      iconCls: 'h-3 w-3 text-red-500 animate-spin',     cardBg: 'bg-orange-500/5',   cardBgSel: 'bg-orange-500/20' },
  ready:            { Icon: CircleDot,    iconCls: 'h-3 w-3 text-cyan-500',                    cardBg: 'bg-cyan-500/5',     cardBgSel: 'bg-cyan-500/20' },
  generating:       { Icon: Loader2,      iconCls: 'h-3 w-3 text-amber-400 animate-spin',      cardBg: 'bg-amber-400/5',    cardBgSel: 'bg-amber-400/20' },
  done:             { Icon: CheckCircle2, iconCls: 'h-3 w-3 text-emerald-500',                 cardBg: 'bg-emerald-500/5',  cardBgSel: 'bg-emerald-500/20' },
  error:            { Icon: AlertCircle,  iconCls: 'h-3 w-3 text-destructive',                 cardBg: 'bg-destructive/5',  cardBgSel: 'bg-destructive/20' },
}


function NsfwIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
      <circle cx="12" cy="12" r="10" />
      <text x="12" y="15.5" textAnchor="middle" fontSize="8" fontWeight="900" fill="currentColor" stroke="none">18+</text>
    </svg>
  )
}

function SequentialPlayer({
  segments,
  projectId,
  onPlayingChange,
}: {
  segments: AudiobookSegment[]
  projectId: number
  onPlayingChange: (segmentId: number | null) => void
}) {
  const { t } = useTranslation('audiobook')
  const [displayIndex, setDisplayIndex] = useState<number | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const audioRef = useRef<HTMLAudioElement>(new Audio())
  const blobUrlsRef = useRef<Record<number, string>>({})
  const currentIndexRef = useRef<number | null>(null)
  const doneSegments = segments.filter(s => s.status === 'done')

  useEffect(() => {
    const audio = audioRef.current
    return () => {
      audio.pause()
      audio.src = ''
      Object.values(blobUrlsRef.current).forEach(url => URL.revokeObjectURL(url))
    }
  }, [])

  const stop = useCallback(() => {
    audioRef.current.pause()
    audioRef.current.src = ''
    currentIndexRef.current = null
    setDisplayIndex(null)
    setIsLoading(false)
    onPlayingChange(null)
  }, [onPlayingChange])

  const playSegment = useCallback(async (index: number) => {
    if (index >= doneSegments.length) {
      currentIndexRef.current = null
      setDisplayIndex(null)
      onPlayingChange(null)
      return
    }
    const seg = doneSegments[index]
    currentIndexRef.current = index
    setDisplayIndex(index)
    onPlayingChange(seg.id)
    setIsLoading(true)

    try {
      if (!blobUrlsRef.current[seg.id]) {
        const response = await apiClient.get(
          audiobookApi.getSegmentAudioUrl(projectId, seg.id),
          { responseType: 'blob' }
        )
        blobUrlsRef.current[seg.id] = URL.createObjectURL(response.data)
      }
      const audio = audioRef.current
      audio.src = blobUrlsRef.current[seg.id]
      await audio.play()
    } catch {
      playSegment(index + 1)
    } finally {
      setIsLoading(false)
    }
  }, [doneSegments, projectId, onPlayingChange])

  useEffect(() => {
    const audio = audioRef.current
    const handleEnded = () => {
      if (currentIndexRef.current !== null) {
        playSegment(currentIndexRef.current + 1)
      }
    }
    audio.addEventListener('ended', handleEnded)
    return () => audio.removeEventListener('ended', handleEnded)
  }, [playSegment])

  if (doneSegments.length === 0) return null

  return (
    <div className="flex items-center gap-1">
      {displayIndex !== null ? (
        <>
          <Button size="xs" variant="ghost" className="text-red-500 hover:text-red-500 hover:bg-red-500/10" onClick={stop}>
            <Square className="h-3 w-3 mr-1 fill-current" />{t('projectCard.sequential.stop')}
          </Button>
          <span className="text-xs text-muted-foreground">
            {isLoading
              ? t('projectCard.sequential.loading')
              : t('projectCard.sequential.progress', { current: displayIndex + 1, total: doneSegments.length })}
          </span>
        </>
      ) : (
        <Button size="xs" variant="ghost" className="text-muted-foreground" onClick={() => playSegment(0)}>
          <Play className="h-3 w-3 mr-1" />{t('projectCard.sequential.play', { count: doneSegments.length })}
        </Button>
      )}
    </div>
  )
}

function LogStream({ projectId, chapterId, active }: { projectId: number; chapterId?: number; active: boolean }) {
  const [lines, setLines] = useState<string[]>([])
  const [done, setDone] = useState(false)
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!active) return
    setLines([])
    setDone(false)

    const token = localStorage.getItem('token')
    const apiBase = (import.meta.env.VITE_API_URL as string) || ''
    const controller = new AbortController()

    const chapterParam = chapterId !== undefined ? `?chapter_id=${chapterId}` : ''
    fetch(`${apiBase}/audiobook/projects/${projectId}/logs${chapterParam}`, {
      headers: { Authorization: `Bearer ${token}` },
      signal: controller.signal,
    }).then(async res => {
      const reader = res.body?.getReader()
      if (!reader) return
      const decoder = new TextDecoder()
      let buffer = ''
      while (true) {
        const { done: streamDone, value } = await reader.read()
        if (streamDone) break
        buffer += decoder.decode(value, { stream: true })
        const parts = buffer.split('\n\n')
        buffer = parts.pop() ?? ''
        for (const part of parts) {
          const line = part.trim()
          if (!line.startsWith('data: ')) continue
          try {
            const msg = JSON.parse(line.slice(6))
            if (msg.done) {
              setDone(true)
            } else if (typeof msg.index === 'number') {
              setLines(prev => {
                const next = [...prev]
                next[msg.index] = msg.line
                return next
              })
            }
          } catch {}
        }
      }
    }).catch(() => {})

    return () => controller.abort()
  }, [projectId, chapterId, active])

  useEffect(() => {
    const el = containerRef.current
    if (el) el.scrollTop = el.scrollHeight
  }, [lines])

  if (lines.length === 0) return null

  return (
    <div ref={containerRef} className="rounded border border-green-900/40 bg-black/90 text-green-400 font-mono text-xs p-3 max-h-52 overflow-y-auto leading-relaxed">
      {lines.map((line, i) => (
        <div key={i} className="whitespace-pre-wrap">{line}</div>
      ))}
      {!done && (
        <span className="inline-block w-2 h-3 bg-green-400 animate-pulse ml-0.5 align-middle" />
      )}
    </div>
  )
}

function LLMConfigDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
  const { t } = useTranslation('audiobook')
  const [baseUrl, setBaseUrl] = useState('')
  const [apiKey, setApiKey] = useState('')
  const [model, setModel] = useState('')
  const [loading, setLoading] = useState(false)
  const [existing, setExisting] = useState<{ base_url?: string; model?: string; has_key: boolean } | null>(null)

  useEffect(() => {
    if (open) adminApi.getLLMConfig().then(setExisting).catch(() => {})
  }, [open])

  const handleSave = async () => {
    if (!baseUrl || !apiKey || !model) {
      toast.error(t('llmConfigPanel.incompleteError'))
      return
    }
    setLoading(true)
    try {
      await adminApi.setLLMConfig({ base_url: baseUrl, api_key: apiKey, model })
      toast.success(t('llmConfigPanel.savedSuccess'))
      setApiKey('')
      const updated = await adminApi.getLLMConfig()
      setExisting(updated)
      onClose()
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setLoading(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={v => { if (!v) onClose() }}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>{t('llmConfigPanel.title')}</DialogTitle>
        </DialogHeader>
        <div className="space-y-3 pt-1">
          {existing && (
            <div className="text-xs text-muted-foreground">
              {t('llmConfigPanel.current', {
                baseUrl: existing.base_url || t('llmConfigPanel.notSet'),
                model: existing.model || t('llmConfigPanel.notSet'),
                keyStatus: existing.has_key ? t('llmConfigPanel.hasKey') : t('llmConfigPanel.noKey'),
              })}
            </div>
          )}
          <Input placeholder="Base URL (e.g. https://api.openai.com/v1)" value={baseUrl} onChange={e => setBaseUrl(e.target.value)} />
          <Input placeholder="API Key" type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} />
          <Input placeholder="Model (e.g. gpt-4o)" value={model} onChange={e => setModel(e.target.value)} />
          <div className="flex justify-end gap-2 pt-1">
            <Button size="sm" variant="outline" onClick={onClose} disabled={loading}>
              {t('cancel', { defaultValue: '取消' })}
            </Button>
            <Button size="sm" onClick={handleSave} disabled={loading}>
              {loading ? t('llmConfigPanel.saving') : t('llmConfigPanel.save')}
            </Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  )
}

function CreateProjectDialog({ open, onClose, onCreated }: { open: boolean; onClose: () => void; onCreated: () => void }) {
  const { t } = useTranslation('audiobook')
  const [title, setTitle] = useState('')
  const [sourceType, setSourceType] = useState<'text' | 'epub'>('text')
  const [text, setText] = useState('')
  const [epubFile, setEpubFile] = useState<File | null>(null)
  const [loading, setLoading] = useState(false)

  const reset = () => { setTitle(''); setText(''); setEpubFile(null); setSourceType('text') }

  const handleCreate = async () => {
    if (!title) { toast.error(t('createPanel.titleRequired')); return }
    if (sourceType === 'text' && !text) { toast.error(t('createPanel.textRequired')); return }
    if (sourceType === 'epub' && !epubFile) { toast.error(t('createPanel.epubRequired')); return }
    setLoading(true)
    try {
      if (sourceType === 'text') {
        await audiobookApi.createProject({ title, source_type: 'text', source_text: text })
      } else {
        await audiobookApi.uploadEpub(title, epubFile!)
      }
      toast.success(t('createPanel.createdSuccess'))
      reset()
      onCreated()
      onClose()
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setLoading(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={v => { if (!v) { reset(); onClose() } }}>
      <DialogContent className="sm:max-w-lg">
        <DialogHeader>
          <DialogTitle>{t('createPanel.title')}</DialogTitle>
        </DialogHeader>
        <div className="space-y-3 pt-1">
          <Input placeholder={t('createPanel.titlePlaceholder')} value={title} onChange={e => setTitle(e.target.value)} />
          <div className="flex gap-2">
            <Button size="sm" variant={sourceType === 'text' ? 'default' : 'outline'} onClick={() => setSourceType('text')}>
              {t('createPanel.pasteText')}
            </Button>
            <Button size="sm" variant={sourceType === 'epub' ? 'default' : 'outline'} onClick={() => setSourceType('epub')}>
              {t('createPanel.uploadEpub')}
            </Button>
          </div>
          {sourceType === 'text' && (
            <Textarea placeholder={t('createPanel.textPlaceholder')} rows={6} value={text} onChange={e => setText(e.target.value)} />
          )}
          {sourceType === 'epub' && (
            <Input type="file" accept=".epub" onChange={e => {
              const file = e.target.files?.[0] || null
              setEpubFile(file)
              if (file && !title) setTitle(file.name.replace(/\.epub$/i, ''))
            }} />
          )}
          <div className="flex justify-end gap-2 pt-1">
            <Button size="sm" variant="outline" onClick={() => { reset(); onClose() }} disabled={loading}>
              {t('cancel', { defaultValue: '取消' })}
            </Button>
            <Button size="sm" onClick={handleCreate} disabled={loading}>
              {loading ? t('createPanel.creating') : t('createPanel.create')}
            </Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  )
}

interface SubgenreConfig {
  protagonistTypes: string[]
  tones: string[]
  conflictScales: string[]
}

interface GenreGroup {
  label: string
  subgenres: Record<string, SubgenreConfig>
}

const GENRE_CONFIGS: Record<string, GenreGroup> = {
  '玄幻': {
    label: '玄幻',
    subgenres: {
      '高武玄幻': { protagonistTypes: ['热血少年', '落魄天才', '废材逆袭', '穿越者', '系统宿主', '天生废体'], tones: ['热血激情', '升级爽文', '黑暗血腥', '轻松搞笑', '史诗宏大'], conflictScales: ['门派争斗', '宗门战争', '诸天争霸', '个人成长', '复仇雪耻'] },
      '修仙升级': { protagonistTypes: ['散修弟子', '资质平庸者', '天才修炼者', '转世神魂', '炼丹师', '剑修'], tones: ['清修飘逸', '热血激战', '奇幻神秘', '恩怨情仇', '历练成长'], conflictScales: ['渡劫飞升', '门派恩怨', '天道之争', '仙魔对决', '灵脉争夺'] },
      '洪荒流': { protagonistTypes: ['洪荒神兽', '人族菜鸡', '太古妖族', '上古大能', '仙界强者', '混沌生灵'], tones: ['宏大史诗', '爽文热血', '阴谋算计', '轻松恶搞', '诸圣博弈'], conflictScales: ['洪荒争霸', '诸圣博弈', '天道争夺', '人族崛起', '封神大战'] },
      '诸天万界': { protagonistTypes: ['穿梭者', '任务系统宿主', '时空旅行者', '多界强者', '轮回者'], tones: ['热血爽文', '智谋流', '搞笑无厘头', '严肃史诗', '全能碾压'], conflictScales: ['完成任务', '诸界争霸', '宇宙级威胁', '推翻天道', '救世传承'] },
      '异世大陆': { protagonistTypes: ['穿越者', '异世贵族', '平民逆袭', '战神转世', '魔法天才', '剑神传人'], tones: ['热血冒险', '宫廷权谋', '温馨治愈', '黑暗复仇', '轻松奇幻'], conflictScales: ['王国争霸', '异族入侵', '魔兽泛滥', '政治阴谋', '个人成长'] },
    },
  },
  '武侠': {
    label: '武侠',
    subgenres: {
      '传统武侠': { protagonistTypes: ['江湖侠客', '名门弟子', '落魄世家子', '武学天才', '复仇者', '隐世高人'], tones: ['豪情壮志', '快意恩仇', '爱恨情仇', '正邪对决', '侠义热血'], conflictScales: ['门派之争', '江湖恩怨', '家仇国恨', '武林正邪', '武功秘籍争夺'] },
      '奇幻武侠': { protagonistTypes: ['习得神功者', '寻宝冒险者', '侠义少年', '隐世高手', '机缘得道者'], tones: ['奇幻冒险', '武侠浪漫', '热血激战', '神秘探索', '奇遇成长'], conflictScales: ['神兵寻觅', '秘籍争夺', '奇遇成长', '救世任务', '阵法宝物'] },
      '现代武侠': { protagonistTypes: ['隐居高手', '都市侠客', '特种兵出身者', '传承者', '武道研究者'], tones: ['现代都市感', '热血写实', '轻松幽默', '动作爽快', '家国情怀'], conflictScales: ['维护正义', '黑道对抗', '传承保护', '功夫切磋', '古武势力'] },
    },
  },
  '仙侠': {
    label: '仙侠',
    subgenres: {
      '东方修真': { protagonistTypes: ['凡人弟子', '仙门弟子', '落魄仙人', '天骄后裔', '妖族转化', '古神血脉'], tones: ['清逸飘渺', '热血激战', '唯美浪漫', '奇幻神秘', '历练成长'], conflictScales: ['渡劫飞升', '仙魔大战', '道侣情缘', '天机命运', '宗门争霸'] },
      '剑仙传说': { protagonistTypes: ['剑道天才', '剑修散仙', '古剑传人', '剑灵融合者', '剑心通明者'], tones: ['飘逸洒脱', '剑意凛冽', '纵横江湖', '诗意浪漫', '独孤求败'], conflictScales: ['剑道证道', '至宝争夺', '仙凡之别', '剑与心境', '天下第一'] },
      '斩妖除魔': { protagonistTypes: ['除魔使者', '天师传人', '驱魔术士', '正道大侠', '神选之人'], tones: ['热血正义', '悬疑恐怖', '神话奇幻', '黑暗压抑', '守护信念'], conflictScales: ['妖魔肆虐', '鬼怪作乱', '邪道崛起', '地狱封印', '天地失衡'] },
    },
  },
  '现代言情': {
    label: '现代言情',
    subgenres: {
      '甜宠': { protagonistTypes: ['普通女孩', '天真少女', '职场新人', '邻家女孩', '意外相遇者'], tones: ['甜蜜温馨', '欢笑爱情', '轻松浪漫', '治愈温暖', '撒糖日常'], conflictScales: ['误会化解', '追爱历程', '霸总求爱', '青涩初恋', '双向暗恋'] },
      '虐恋': { protagonistTypes: ['坚强女主', '深情错付者', '误解中的恋人', '身份悬殊者', '宿命相遇者'], tones: ['虐心煎熬', '深情执着', '痛苦成长', '悲剧唯美', '破镜重圆'], conflictScales: ['身世误解', '爱而不得', '命运阻隔', '家族反对', '情感纠葛'] },
      '婚姻生活': { protagonistTypes: ['已婚夫妻', '闪婚陌生人', '失婚重来者', '中年危机者', '再婚家庭成员'], tones: ['温馨现实', '矛盾磨合', '生活感悟', '烟火气息', '相濡以沫'], conflictScales: ['婚姻危机', '重建感情', '第三者风波', '家庭矛盾', '育儿分歧'] },
      '青春校园': { protagonistTypes: ['高中生', '大学生', '校园社团成员', '学霸', '运动员'], tones: ['青涩懵懂', '阳光活力', '笑泪交织', '追梦热血', '暗恋悸动'], conflictScales: ['学习压力', '暗恋表白', '友情裂痕', '家庭变故', '比赛竞争'] },
    },
  },
  '都市': {
    label: '都市',
    subgenres: {
      '系统流': { protagonistTypes: ['普通小人物', '穿越重生者', '选中之人', '落魄天才', '被系统选中者'], tones: ['爽文升级', '轻松幽默', '热血励志', '全能碾压', '种田发展'], conflictScales: ['系统任务', '快速崛起', '实力碾压', '逆袭人生', '征服巅峰'] },
      '商战': { protagonistTypes: ['商界精英', '白手起家者', '家族继承人', '职场新人', '商业奇才'], tones: ['紧张激烈', '智谋对抗', '励志奋斗', '尔虞我诈', '商业传奇'], conflictScales: ['商业竞争', '家族权斗', '职场博弈', '资本角力', '企业并购'] },
      '重生逆袭': { protagonistTypes: ['重生者', '回到过去者', '带着记忆重来者', '复仇归来者', '先知者'], tones: ['爽文复仇', '轻松改变命运', '深情弥补', '步步为营', '大杀四方'], conflictScales: ['前世遗恨', '命运改写', '天才崛起', '恩仇清算', '守护挚爱'] },
      '官场': { protagonistTypes: ['基层公务员', '仕途奋斗者', '清官强者', '改革者', '下乡干部'], tones: ['权谋斗争', '写实主义', '正义坚守', '仕途险恶', '为民请命'], conflictScales: ['官场倾轧', '腐败反贪', '仕途升迁', '民生为先', '政治博弈'] },
    },
  },
  '悬疑': {
    label: '悬疑',
    subgenres: {
      '推理侦探': { protagonistTypes: ['警探', '法医', '业余侦探', '记者', '侦探助手', '神探'], tones: ['烧脑推理', '扣人心弦', '悬念丛生', '冷静理性', '层层剥茧'], conflictScales: ['连环命案', '密室谜题', '真相揭露', '罪犯追踪', '系列悬案'] },
      '犯罪惊悚': { protagonistTypes: ['警察', '罪犯视角', '受害者幸存者', '卧底', '特工'], tones: ['黑暗紧张', '写实血腥', '心理较量', '暗流涌动', '生死边缘'], conflictScales: ['犯罪追击', '身份揭露', '背叛与救赎', '生死博弈', '组织渗透'] },
      '灵异恐怖': { protagonistTypes: ['普通人', '灵异体质者', '道士法师', '鬼怪', '民俗学者'], tones: ['恐怖诡异', '悬疑惊悚', '志怪神秘', '民俗风情', '冷汗直冒'], conflictScales: ['恶灵纠缠', '冤情揭露', '鬼怪作祟', '阴阳两界', '封印破解'] },
      '心理悬疑': { protagonistTypes: ['心理咨询师', '精神科医生', '犯罪侧写师', '创伤幸存者', '不可靠叙述者'], tones: ['心理压迫', '黑暗深沉', '哲思反思', '人性剖析', '迷雾重重'], conflictScales: ['心魔挣脱', '人格分裂', '记忆迷失', '操控与反制', '真相解谜'] },
    },
  },
  '科幻': {
    label: '科幻',
    subgenres: {
      '星际战争': { protagonistTypes: ['星际舰长', '星际士兵', '精英飞行员', '星际难民', '外星协调者'], tones: ['史诗宏大', '战争残酷', '科技未来', '人性探索', '军事硬派'], conflictScales: ['星际战争', '殖民冲突', '物种灭绝威胁', '宇宙联邦争霸', '文明碰撞'] },
      '末世废土': { protagonistTypes: ['末世幸存者', '变异人类', '反抗组织领袖', '科学家', '拾荒者'], tones: ['黑暗压抑', '生存挣扎', '希望微光', '残酷现实', '人性测试'], conflictScales: ['末世求生', '物资争夺', '反抗统治', '文明重建', '变异扩散'] },
      '赛博朋克': { protagonistTypes: ['黑客', '赛博战士', '企业异见者', '增强人类', '数字游民'], tones: ['科技反乌托邦', '阴暗迷幻', '反体制', '霓虹暗夜', '数字哲学'], conflictScales: ['数据战争', '企业阴谋', '人机界限', '身份认同', '系统颠覆'] },
      '时间旅行': { protagonistTypes: ['时间旅行者', '时间管理局成员', '意外穿越者', '历史修正者', '时间悖论受害者'], tones: ['烧脑悬疑', '蝴蝶效应', '时间悖论', '浪漫跨时代', '命运游戏'], conflictScales: ['历史修正', '时间线保护', '因果悖论', '多重未来', '时间战争'] },
      '人工智能': { protagonistTypes: ['AI研究员', '觉醒AI', '人机协作者', '反AI组织成员', '数字意识体'], tones: ['哲学反思', '科技惊悚', '情感探索', '伦理困境', '奇点到来'], conflictScales: ['AI觉醒', '人机战争', '意识复制', '伦理边界', '数字文明'] },
    },
  },
  '历史': {
    label: '历史',
    subgenres: {
      '架空历史': { protagonistTypes: ['穿越者', '改变历史者', '默默无闻小人物', '穿越官员', '现代人'], tones: ['宏大史诗', '权谋斗争', '家国情怀', '轻松穿越', '历史重构'], conflictScales: ['朝代更迭', '权力争夺', '外族入侵', '历史改变', '改革维新'] },
      '宫廷斗争': { protagonistTypes: ['妃嫔', '皇子', '太监谋士', '宫女逆袭者', '皇后'], tones: ['心机深沉', '权谋博弈', '悲情凄美', '步步惊心', '宫廷浮沉'], conflictScales: ['后宫争宠', '皇位争夺', '废立之争', '家族联姻', '宫廷政变'] },
      '战争史诗': { protagonistTypes: ['将领', '士兵', '谋士', '王侯', '战地医者'], tones: ['悲壮雄浑', '铁血热血', '英雄主义', '战争残酷', '家国大义'], conflictScales: ['国家存亡', '统一战争', '抗击外敌', '乱世争雄', '以少胜多'] },
      '历史正剧': { protagonistTypes: ['历史名人', '草根智者', '政治家', '民间义士', '文人墨客'], tones: ['厚重写实', '人文感悟', '历史还原', '时代悲歌', '英雄气节'], conflictScales: ['时代变革', '个人命运', '历史洪流', '民族大义', '文化传承'] },
    },
  },
  '恐怖': {
    label: '恐怖',
    subgenres: {
      '都市灵异': { protagonistTypes: ['普通市民', '灵异体质者', '鬼怪猎人', '民俗研究者', '意外卷入者'], tones: ['惊悚诡异', '都市传说', '黑暗压抑', '悬疑恐惧', '无处可逃'], conflictScales: ['都市怪谈', '恶灵作祟', '冤魂纠缠', '超自然事件', '禁忌揭秘'] },
      '克苏鲁': { protagonistTypes: ['学者研究者', '猎奇者', '受选者', '意志坚强者', '古神信徒'], tones: ['宇宙级恐惧', '理智崩溃', '末日预感', '深渊凝视', '存在主义恐怖'], conflictScales: ['禁忌召唤', '理性崩溃', '古神苏醒', '深渊凝视', '人类渺小'] },
      '鬼屋探险': { protagonistTypes: ['探险者', '灵媒', '记者', '大胆学生', '真相追寻者'], tones: ['极度紧张', '恐怖刺激', '生死挣扎', '诡异氛围', '密室求生'], conflictScales: ['怨灵诅咒', '脱离险境', '真相揭露', '诡异机关', '黑暗守护者'] },
      '丧尸末日': { protagonistTypes: ['末日幸存者', '军人', '科学家', '普通家庭', '幸运儿'], tones: ['生存惊悚', '人性黑暗', '绝望与希望', '末日互助', '道德崩塌'], conflictScales: ['丧尸追击', '幸存者冲突', '疫苗寻找', '安全区争夺', '文明坚守'] },
    },
  },
  'Fantasy': {
    label: 'Fantasy',
    subgenres: {
      'High Fantasy': { protagonistTypes: ['Chosen One', 'Magic User', 'Knight/Warrior', 'Royal Heir', 'Common Hero', 'Prophesied One'], tones: ['Epic', 'Heroic', 'Noble', 'Mythic', 'Adventure'], conflictScales: ['Personal Quest', 'Kingdom-wide', 'World-saving', 'Good vs Evil', 'Political Intrigue'] },
      'Dark Fantasy': { protagonistTypes: ['Antihero', 'Cursed Individual', 'Monster Hunter', 'Corrupted Noble', 'Reluctant Chosen One', 'Morally Gray Wizard'], tones: ['Grim', 'Morally Ambiguous', 'Horror-tinged', 'Psychological', 'Brutal'], conflictScales: ['Personal Survival', 'Moral Choice', 'Power Corruption', 'Existential Horror', 'Societal Decay'] },
      'Urban Fantasy': { protagonistTypes: ['Magical Detective', 'Hidden World Guardian', 'Modern Witch/Wizard', 'Supernatural Creature', 'Normal Person Discovered', 'Magic Shop Owner'], tones: ['Modern', 'Mysterious', 'Action-packed', 'Detective Noir', 'Secret World'], conflictScales: ['Personal', 'City-wide', 'Hidden Society', 'Supernatural Politics', 'World-threatening'] },
      'Sword and Sorcery': { protagonistTypes: ['Warrior', 'Rogue', 'Barbarian', 'Mercenary', 'Wandering Wizard', 'Treasure Hunter'], tones: ['Adventurous', 'Gritty', 'Action-focused', 'Pulp', 'Personal'], conflictScales: ['Personal Gain', 'Adventure', 'Survival', 'Quest', 'Local Threat'] },
      'Mythic Fantasy': { protagonistTypes: ['Demigod', 'Legendary Hero', 'Oracle', 'Divine Champion', 'Monster Slayer', 'Mythical Creature'], tones: ['Legendary', 'Epic', 'Mythological', 'Divine', 'Larger than Life'], conflictScales: ['Divine Politics', 'Legendary Quests', 'Fate of Gods', 'Mythic Prophecy', 'World-shaping'] },
      'Fairy Tale': { protagonistTypes: ['Innocent Hero', 'Clever Trickster', 'Transformed Being', 'Royal Figure', 'Magical Helper', 'Common Person'], tones: ['Whimsical', 'Moral', 'Magical', 'Traditional', 'Transformative'], conflictScales: ['Personal Journey', 'Moral Test', 'Magical Challenge', 'Kingdom Fate', 'Breaking Curses'] },
    },
  },
  'Sci-Fi': {
    label: 'Sci-Fi',
    subgenres: {
      'Space Opera': { protagonistTypes: ['Military Officer', 'Merchant Captain', 'Explorer', 'Diplomat', 'Rebel Leader', 'Imperial Noble'], tones: ['Optimistic', 'Dark', 'Political', 'Adventure-focused', 'Character-driven'], conflictScales: ['Personal', 'Planetary', 'Interstellar', 'Galactic', 'Species Survival'] },
      'Cyberpunk': { protagonistTypes: ['Hacker', 'Street Mercenary', 'Corporate Defector', 'AI Researcher', 'Underground Activist', 'Augmented Human'], tones: ['Noir', 'Gritty', 'Anti-establishment', 'Dystopian', 'Tech-noir'], conflictScales: ['Personal', 'Street Level', 'Corporate', 'Systemic', 'Digital'] },
      'Post-Apocalyptic': { protagonistTypes: ['Survivor', 'Wasteland Warrior', 'Community Leader', 'Scavenger', 'Medic', 'Former Military'], tones: ['Grim', 'Survival-focused', 'Hope in Darkness', 'Brutal', 'Revolutionary'], conflictScales: ['Personal Survival', 'Group Survival', 'Resource Control', 'Territory', 'Rebuilding Society'] },
      'Hard Sci-Fi': { protagonistTypes: ['Scientist', 'Engineer', 'Astronaut', 'Research Team Leader', 'AI Researcher', 'Technical Specialist'], tones: ['Technical', 'Philosophical', 'Discovery-focused', 'Methodical', 'Realistic'], conflictScales: ['Personal', 'Technical', 'Scientific Discovery', 'Environmental', 'Existential'] },
      'Biopunk': { protagonistTypes: ['Genetic Engineer', 'Modified Human', 'Underground Scientist', 'Corporate Whistleblower', 'Bio-hacker', 'Test Subject'], tones: ['Body Horror', 'Ethical Drama', 'Scientific', 'Anti-corporate', 'Transformative'], conflictScales: ['Personal', 'Medical', 'Ethical', 'Corporate', 'Species-wide'] },
      'Time Travel': { protagonistTypes: ['Time Agent', 'Accidental Traveler', 'Historical Researcher', 'Timeline Guardian', 'Temporal Engineer'], tones: ['Complex', 'Mysterious', 'Philosophical', 'Adventure', 'Causality-focused'], conflictScales: ['Personal', 'Historical', 'Timeline Preservation', 'Paradox Prevention', 'Multi-temporal'] },
    },
  },
  'Mystery': {
    label: 'Mystery',
    subgenres: {
      'Cozy Mystery': { protagonistTypes: ['Amateur Detective', 'Librarian', 'Shop Owner', 'Retired Professional', 'Local Resident', 'Hobby Enthusiast'], tones: ['Gentle', 'Puzzle-focused', 'Community-centered', 'Cozy', 'Character-driven'], conflictScales: ['Personal Mystery', 'Community Secret', 'Local Crime', 'Family Mystery', 'Historical Puzzle'] },
      'Police Procedural': { protagonistTypes: ['Police Detective', 'Forensic Specialist', 'Police Captain', 'Crime Scene Investigator', 'FBI Agent'], tones: ['Realistic', 'Procedural', 'Professional', 'Methodical', 'Team-focused'], conflictScales: ['Individual Cases', 'Serial Crimes', 'Organized Crime', 'Corruption', 'Major Investigations'] },
      'Hard-boiled': { protagonistTypes: ['Private Detective', 'Ex-Cop', 'Cynical Investigator', 'Tough Guy', 'Street-smart Detective', 'Noir Hero'], tones: ['Noir', 'Cynical', 'Gritty', 'Dark', 'Atmospheric'], conflictScales: ['Personal Cases', 'Corruption', 'Urban Crime', 'Moral Choices', 'Survival'] },
      'Psychological Mystery': { protagonistTypes: ['Psychologist', 'Troubled Detective', 'Mental Health Professional', 'Unreliable Narrator', 'Psychological Profiler'], tones: ['Psychological', 'Introspective', 'Mind-bending', 'Complex', 'Character-focused'], conflictScales: ['Mental Mysteries', 'Psychological Crimes', 'Identity Issues', 'Memory Problems', 'Perception Puzzles'] },
      'Historical Mystery': { protagonistTypes: ['Period Detective', 'Historical Figure', 'Scholar', 'Period Professional', 'Aristocrat', 'Common Person'], tones: ['Historical', 'Authentic', 'Period-appropriate', 'Cultural', 'Educational'], conflictScales: ['Personal Mysteries', 'Historical Events', 'Period Crimes', 'Social Issues', 'Political Intrigue'] },
    },
  },
  'Horror': {
    label: 'Horror',
    subgenres: {
      'Gothic Horror': { protagonistTypes: ['Haunted Individual', 'Investigator', 'Innocent Victim', 'Cursed Person', 'Gothic Hero', 'Tormented Soul'], tones: ['Atmospheric', 'Psychological', 'Brooding', 'Mysterious', 'Melancholic'], conflictScales: ['Personal Haunting', 'Family Curse', 'Supernatural Threat', 'Psychological Terror', 'Ancient Evil'] },
      'Cosmic Horror': { protagonistTypes: ['Academic Researcher', 'Occult Investigator', 'Unwitting Scholar', 'Cosmic Witness', 'Doomed Explorer', 'Sanity-threatened Individual'], tones: ['Existential', 'Unknowable', 'Cosmic', 'Dread-filled', 'Mind-breaking'], conflictScales: ['Cosmic Revelation', 'Sanity Destruction', 'Reality Breakdown', 'Ancient Awakening', 'Existential Horror'] },
      'Psychological Horror': { protagonistTypes: ['Unreliable Narrator', 'Mentally Unstable', 'Paranoid Individual', 'Trauma Victim', 'Isolated Person'], tones: ['Psychological', 'Disturbing', 'Mind-bending', 'Paranoid', 'Introspective'], conflictScales: ['Mental Breakdown', 'Reality Distortion', 'Psychological Manipulation', 'Internal Terror', 'Sanity Loss'] },
      'Supernatural Horror': { protagonistTypes: ['Paranormal Investigator', 'Haunted Individual', 'Psychic Medium', 'Skeptical Researcher', 'Spiritual Warrior', 'Innocent Victim'], tones: ['Supernatural', 'Eerie', 'Spiritual', 'Otherworldly', 'Paranormal'], conflictScales: ['Ghostly Haunting', 'Demonic Possession', 'Spiritual Warfare', 'Paranormal Investigation', 'Supernatural Threat'] },
      'Slasher Horror': { protagonistTypes: ['Final Girl', 'Survivor', 'Potential Victim', 'Group Member', 'Resourceful Fighter'], tones: ['Suspenseful', 'Violent', 'Survival-focused', 'Intense', 'Action-horror'], conflictScales: ['Survival', 'Killer Hunt', 'Group Elimination', 'Escape Attempt', 'Final Confrontation'] },
    },
  },
  'Thriller': {
    label: 'Thriller',
    subgenres: {
      'Espionage Thriller': { protagonistTypes: ['Secret Agent', 'Intelligence Officer', 'Double Agent', 'Spy Handler', 'Undercover Operative', 'Government Analyst'], tones: ['Sophisticated', 'International', 'High-stakes', 'Political', 'Covert'], conflictScales: ['International Conspiracy', 'Government Secrets', 'Spy Networks', 'National Security', 'Global Politics'] },
      'Psychological Thriller': { protagonistTypes: ['Psychologist', 'Mentally Unstable', 'Manipulation Victim', 'Paranoid Individual', 'Mind Game Player', 'Psychological Profiler'], tones: ['Psychological', 'Mind-bending', 'Paranoid', 'Manipulative', 'Disturbing'], conflictScales: ['Mental Manipulation', 'Psychological Torture', 'Mind Control', 'Paranoid Delusions', 'Reality Breakdown'] },
      'Action Thriller': { protagonistTypes: ['Action Hero', 'Special Forces', 'Mercenary', 'Bodyguard', 'Martial Artist'], tones: ['High-energy', 'Action-packed', 'Adrenaline-fueled', 'Physical', 'Fast-paced'], conflictScales: ['Physical Confrontation', 'High-speed Chases', 'Combat Situations', 'Rescue Missions', 'Survival Scenarios'] },
      'Legal Thriller': { protagonistTypes: ['Lawyer', 'Judge', 'Legal Investigator', 'Prosecutor', 'Defense Attorney', 'Legal Whistleblower'], tones: ['Legal', 'Procedural', 'Justice-focused', 'Institutional', 'Courtroom-driven'], conflictScales: ['Legal Conspiracy', 'Courtroom Drama', 'Justice Corruption', 'Legal Cover-up', 'Judicial Manipulation'] },
      'Techno-Thriller': { protagonistTypes: ['Computer Expert', 'Cyber Security Specialist', 'Tech Entrepreneur', 'Hacker', 'Scientist', 'Digital Investigator'], tones: ['High-tech', 'Fast-paced', 'Technical', 'Futuristic', 'Digital'], conflictScales: ['Cyber Attacks', 'Technological Threats', 'Digital Warfare', 'Scientific Disasters', 'Tech Conspiracies'] },
      'Political Thriller': { protagonistTypes: ['Politician', 'Journalist', 'Government Insider', 'Whistleblower', 'Political Aide', 'Investigative Reporter'], tones: ['Political', 'Investigative', 'Institutional', 'Conspiratorial', 'Power-focused'], conflictScales: ['Political Corruption', 'Government Conspiracy', 'Electoral Manipulation', 'Institutional Cover-up', 'Power Abuse'] },
    },
  },
}

function Chip({ label, selected, onClick }: { label: string; selected: boolean; onClick: () => void }) {
  return (
    <button
      type="button"
      onClick={onClick}
      className={`px-2.5 py-1 rounded-full text-xs border transition-colors ${
        selected
          ? 'bg-primary text-primary-foreground border-primary'
          : 'bg-background text-muted-foreground border-border hover:border-primary/50 hover:text-foreground'
      }`}
    >
      {label}
    </button>
  )
}

function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: () => void; onCreated: () => void }) {
  const [title, setTitle] = useState('')
  const [genre, setGenre] = useState('')
  const [subgenre, setSubgenre] = useState('')
  const [protagonistType, setProtagonistType] = useState('')
  const [tone, setTone] = useState('')
  const [conflictScale, setConflictScale] = useState('')
  const [numCharacters, setNumCharacters] = useState(5)
  const [numChapters, setNumChapters] = useState(8)
  const [synopsis, setSynopsis] = useState('')
  const [generatingSynopsis, setGeneratingSynopsis] = useState(false)
  const [loading, setLoading] = useState(false)

  const genreKeys = Object.keys(GENRE_CONFIGS)
  const subgenreKeys = genre ? Object.keys(GENRE_CONFIGS[genre]?.subgenres ?? {}) : []
  const subgenreConfig = (genre && subgenre) ? GENRE_CONFIGS[genre]?.subgenres[subgenre] : null

  const reset = () => {
    setTitle(''); setGenre(''); setSubgenre(''); setProtagonistType(''); setTone('')
    setConflictScale(''); setNumCharacters(5); setNumChapters(8); setSynopsis('')
  }

  const handleGenreSelect = (g: string) => {
    setGenre(g); setSubgenre(''); setProtagonistType(''); setTone(''); setConflictScale(''); setSynopsis('')
  }
  const handleSubgenreSelect = (s: string) => {
    setSubgenre(s); setProtagonistType(''); setTone(''); setConflictScale(''); setSynopsis('')
  }

  const handleGenerateSynopsis = async () => {
    if (!title) { toast.error('请输入作品标题'); return }
    if (!genre) { toast.error('请选择故事类型'); return }
    setGeneratingSynopsis(true)
    try {
      const result = await audiobookApi.generateSynopsis({
        title,
        genre: subgenre ? `${genre} - ${subgenre}` : genre,
        subgenre,
        protagonist_type: protagonistType,
        tone,
        conflict_scale: conflictScale,
        num_characters: numCharacters,
        num_chapters: numChapters,
      })
      setSynopsis(result)
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setGeneratingSynopsis(false)
    }
  }

  const handleCreate = async () => {
    if (!title) { toast.error('请输入作品标题'); return }
    if (!synopsis) { toast.error('请先生成故事简介'); return }
    setLoading(true)
    try {
      await audiobookApi.createAIScript({
        title,
        genre: subgenre ? `${genre} - ${subgenre}` : genre,
        subgenre,
        premise: synopsis,
        style: tone,
        num_characters: numCharacters,
        num_chapters: numChapters,
      } as ScriptGenerationRequest)
      toast.success('AI剧本生成任务已创建')
      reset()
      onCreated()
      onClose()
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setLoading(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={v => { if (!v) { reset(); onClose() } }}>
      <DialogContent className="sm:max-w-2xl max-h-[90vh] flex flex-col">
        <DialogHeader className="shrink-0">
          <DialogTitle>AI 生成剧本</DialogTitle>
        </DialogHeader>
        <div className="flex-1 overflow-y-auto space-y-4 pr-1">
          <div className="space-y-1">
            <p className="text-xs text-muted-foreground">作品标题</p>
            <Input placeholder="输入作品标题" value={title} onChange={e => setTitle(e.target.value)} />
          </div>

          <div className="space-y-2">
            <p className="text-xs text-muted-foreground">故事类型</p>
            <div className="flex flex-wrap gap-1.5">
              {genreKeys.map(g => (
                <Chip key={g} label={GENRE_CONFIGS[g].label} selected={genre === g} onClick={() => handleGenreSelect(g)} />
              ))}
            </div>
          </div>

          {genre && subgenreKeys.length > 0 && (
            <div className="space-y-2">
              <p className="text-xs text-muted-foreground">子类型</p>
              <div className="flex flex-wrap gap-1.5">
                {subgenreKeys.map(s => (
                  <Chip key={s} label={s} selected={subgenre === s} onClick={() => handleSubgenreSelect(s)} />
                ))}
              </div>
            </div>
          )}

          {subgenreConfig && (
            <>
              <div className="space-y-2">
                <p className="text-xs text-muted-foreground">主角类型</p>
                <div className="flex flex-wrap gap-1.5">
                  {subgenreConfig.protagonistTypes.map(p => (
                    <Chip key={p} label={p} selected={protagonistType === p} onClick={() => setProtagonistType(protagonistType === p ? '' : p)} />
                  ))}
                </div>
              </div>
              <div className="space-y-2">
                <p className="text-xs text-muted-foreground">故事基调</p>
                <div className="flex flex-wrap gap-1.5">
                  {subgenreConfig.tones.map(t => (
                    <Chip key={t} label={t} selected={tone === t} onClick={() => setTone(tone === t ? '' : t)} />
                  ))}
                </div>
              </div>
              <div className="space-y-2">
                <p className="text-xs text-muted-foreground">冲突规模</p>
                <div className="flex flex-wrap gap-1.5">
                  {subgenreConfig.conflictScales.map(c => (
                    <Chip key={c} label={c} selected={conflictScale === c} onClick={() => setConflictScale(conflictScale === c ? '' : c)} />
                  ))}
                </div>
              </div>
            </>
          )}

          <div className="flex gap-3">
            <label className="flex-1 flex flex-col gap-1 text-sm">
              <span className="text-muted-foreground text-xs">角色数量（2-10）</span>
              <Input type="number" min={2} max={10} value={numCharacters} onChange={e => setNumCharacters(Math.min(10, Math.max(2, Number(e.target.value))))} />
            </label>
            <label className="flex-1 flex flex-col gap-1 text-sm">
              <span className="text-muted-foreground text-xs">章节数量（2-30）</span>
              <Input type="number" min={2} max={30} value={numChapters} onChange={e => setNumChapters(Math.min(30, Math.max(2, Number(e.target.value))))} />
            </label>
          </div>

          <div className="flex justify-end">
            <Button size="sm" variant="outline" onClick={handleGenerateSynopsis} disabled={!genre || generatingSynopsis}>
              {generatingSynopsis ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
              {generatingSynopsis ? '生成中...' : '生成故事简介'}
            </Button>
          </div>

          {synopsis && (
            <div className="space-y-2">
              <p className="text-xs text-muted-foreground">故事简介（可编辑）</p>
              <Textarea rows={6} value={synopsis} onChange={e => setSynopsis(e.target.value)} className="text-sm" />
            </div>
          )}
        </div>

        <div className="flex justify-between gap-2 pt-3 shrink-0 border-t">
          <Button size="sm" variant="ghost" onClick={() => { reset(); onClose() }} disabled={loading}>取消</Button>
          <div className="flex gap-2">
            {synopsis && (
              <Button size="sm" variant="outline" onClick={handleGenerateSynopsis} disabled={generatingSynopsis}>
                <RotateCcw className="h-3 w-3 mr-1" />
                重新生成
              </Button>
            )}
            <Button size="sm" onClick={handleCreate} disabled={loading || !synopsis || !title}>
              {loading ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
              {loading ? '创建中...' : '生成剧本'}
            </Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  )
}

const NSFW_GENRE_CONFIGS: Record<string, { label: string; subgenres: Record<string, { protagonistTypes: string[]; tones: string[]; conflictScales: string[] }> }> = {
  '都市成人': {
    label: '都市成人',
    subgenres: {
      '办公室禁忌': { protagonistTypes: ['职场白领', '高管下属', '同事关系', '猎头顾问'], tones: ['压抑克制', '暧昧升温', '禁忌诱惑', '情欲失控'], conflictScales: ['办公室恩怨', '权力游戏', '职场暗战', '情感纠葛'] },
      '豪门虐恋': { protagonistTypes: ['普通女孩', '豪门公子', '隐婚妻子', '商业联姻'], tones: ['痴缠虐恋', '甜蜜救赎', '冷酷霸总', '深情告白'], conflictScales: ['门第悬殊', '家族势力', '商业角力', '情感博弈'] },
      '合约婚姻': { protagonistTypes: ['合约双方', '假戏真做', '情感觉醒', '身份反转'], tones: ['冷漠开局', '渐入佳境', '情不自禁', '破镜重圆'], conflictScales: ['合约终止', '真心暴露', '外敌干扰', '家族干涉'] },
      '姐弟恋': { protagonistTypes: ['成熟女性', '年轻男性', '职场前辈', '学生社会人'], tones: ['成熟风情', '反差萌甜', '年龄压制', '互相依赖'], conflictScales: ['年龄差距', '外界眼光', '情感成熟度', '未来规划'] },
      '高干文': { protagonistTypes: ['官员子弟', '平民女主', '政商联姻', '秘密保护'], tones: ['权力压迫', '低调奢华', '隐秘情感', '强势占有'], conflictScales: ['权力博弈', '政治婚姻', '身份暴露', '家族对决'] },
    },
  },
  '修仙双修': {
    label: '修仙双修',
    subgenres: {
      '双修秘法': { protagonistTypes: ['天才弟子', '神秘师尊', '炉鼎逆袭', '道侣缘定'], tones: ['禁忌师徒', '双修悟道', '情欲修炼', '天道孽缘'], conflictScales: ['门派规矩', '天道考验', '魔道诱惑', '飞升之争'] },
      '仙尊宠妻': { protagonistTypes: ['无名小修', '万年仙尊', '女弟子', '神女转世'], tones: ['宠溺无边', '万年等待', '强强联手', '情深似海'], conflictScales: ['外敌窥伺', '仙界规则', '劫数将临', '情缘注定'] },
      '妖族后宫': { protagonistTypes: ['妖王', '人族女修', '混血妖神', '后宫妃嫔'], tones: ['妖冶风情', '强势占有', '蛊惑人心', '情孽难断'], conflictScales: ['人妖对立', '族群战争', '后宫争宠', '妖皇之位'] },
      '天骄配天娇': { protagonistTypes: ['第一天骄', '绝世女主', '双强对决', '势均力敌'], tones: ['互相较劲', '惺惺相惜', '强强碰撞', '双向暗恋'], conflictScales: ['修炼竞争', '争夺机缘', '外敌来犯', '道路抉择'] },
    },
  },
  '奇幻异世': {
    label: '奇幻异世',
    subgenres: {
      '兽人联姻': { protagonistTypes: ['穿越女主', '兽人王者', '人兽混血', '部落首领'], tones: ['野性粗犷', '温柔呵护', '征服臣服', '异族情缘'], conflictScales: ['种族融合', '部落战争', '血脉觉醒', '异世生存'] },
      '魔君圣女': { protagonistTypes: ['圣女祭司', '堕落魔君', '神魔对立', '光暗守护者'], tones: ['圣洁污染', '光暗交融', '禁忌之恋', '救赎与沉沦'], conflictScales: ['神魔大战', '命运抗争', '信仰崩塌', '世界毁灭'] },
      '穿越迷情': { protagonistTypes: ['现代女性', '古代君王', '异世界人物', '时空旅者'], tones: ['时空错位', '身份迷失', '古今碰撞', '情不知所起'], conflictScales: ['回归困境', '历史改变', '时空悖论', '情感牵绊'] },
      '契约恋人': { protagonistTypes: ['契约双方', '灵魂契约', '主仆关系', '命运绑定'], tones: ['被迫相处', '情感萌生', '契约升温', '破除枷锁'], conflictScales: ['契约限制', '灵魂危机', '外界阻碍', '真心表达'] },
    },
  },
  '历史宫廷': {
    label: '历史宫廷',
    subgenres: {
      '帝王宠妃': { protagonistTypes: ['寒门女子', '九五之尊', '独宠专房', '贵妃娘娘'], tones: ['专宠霸道', '龙恩浩荡', '后宫独步', '帝心难测'], conflictScales: ['后宫争宠', '前朝牵制', '子嗣之争', '外戚干政'] },
      '宫廷秘史': { protagonistTypes: ['宫廷暗卫', '妃嫔密探', '皇室隐秘', '历史见证者'], tones: ['权谋算计', '忠义两难', '隐秘情愫', '乱世真情'], conflictScales: ['宫廷政变', '皇位继承', '异国入侵', '家族倾轧'] },
      '宅斗宫斗': { protagonistTypes: ['嫡女庶女', '正妻侍妾', '宫女妃嫔', '世家主母'], tones: ['心机算尽', '步步为营', '白莲花黑化', '大女主觉醒'], conflictScales: ['争夺家产', '嫁妆之战', '子嗣竞争', '家族延续'] },
      '后宫权谋': { protagonistTypes: ['权倾后宫者', '新晋宠妃', '太后皇后', '谋士幕僚'], tones: ['深宫寂寞', '权力欲望', '情与利的博弈', '最终胜者独孤'], conflictScales: ['废立皇后', '皇储之争', '派系对立', '外朝联动'] },
    },
  },
  '现代激情': {
    label: '现代激情',
    subgenres: {
      '一夜情缘': { protagonistTypes: ['陌生男女', '意外邂逅', '酒后乱性', '重逢旧恋'], tones: ['激情一夜', '难以忘怀', '意外怀孕', '命运纠缠'], conflictScales: ['身份悬殊', '情感纠缠', '意外结果', '关系定义'] },
      '前任回归': { protagonistTypes: ['前男友女友', '新欢旧爱', '情感疗愈者', '纠缠不清'], tones: ['旧情复燃', '爱恨交织', '放不下的执念', '破镜能否重圆'], conflictScales: ['感情空白期', '第三者介入', '过去伤害', '信任重建'] },
      '欲望都市': { protagonistTypes: ['都市白领', '成功商人', '独立女性', '富二代'], tones: ['纸醉金迷', '欲望横流', '都市孤独', '情欲游戏'], conflictScales: ['金钱利益', '情感空虚', '道德底线', '真爱追寻'] },
      '豪门宠婚': { protagonistTypes: ['灰姑娘女主', '豪门单身汉', '闪婚闪恋', '宠妻狂魔'], tones: ['宠溺日常', '霸道甜蜜', '嫉妒吃醋', '甜到发腻'], conflictScales: ['豪门规则', '家族考验', '外来破坏', '证明真爱'] },
    },
  },
}

function NSFWScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: () => void; onCreated: () => void }) {
  const [title, setTitle] = useState('')
  const [genre, setGenre] = useState('')
  const [subgenre, setSubgenre] = useState('')
  const [protagonistType, setProtagonistType] = useState('')
  const [tone, setTone] = useState('')
  const [conflictScale, setConflictScale] = useState('')
  const [numCharacters, setNumCharacters] = useState(5)
  const [numChapters, setNumChapters] = useState(8)
  const [violenceLevel, setViolenceLevel] = useState(0)
  const [eroticismLevel, setEroticismLevel] = useState(5)
  const [synopsis, setSynopsis] = useState('')
  const [generatingSynopsis, setGeneratingSynopsis] = useState(false)
  const [loading, setLoading] = useState(false)

  const genreKeys = Object.keys(NSFW_GENRE_CONFIGS)
  const subgenreKeys = genre ? Object.keys(NSFW_GENRE_CONFIGS[genre]?.subgenres ?? {}) : []
  const subgenreConfig = (genre && subgenre) ? NSFW_GENRE_CONFIGS[genre]?.subgenres[subgenre] : null

  const reset = () => {
    setTitle(''); setGenre(''); setSubgenre(''); setProtagonistType(''); setTone('')
    setConflictScale(''); setNumCharacters(5); setNumChapters(8); setViolenceLevel(0); setEroticismLevel(5); setSynopsis('')
  }

  const handleGenreSelect = (g: string) => {
    setGenre(g); setSubgenre(''); setProtagonistType(''); setTone(''); setConflictScale(''); setSynopsis('')
  }
  const handleSubgenreSelect = (s: string) => {
    setSubgenre(s); setProtagonistType(''); setTone(''); setConflictScale(''); setSynopsis('')
  }

  const handleGenerateSynopsis = async () => {
    if (!title) { toast.error('请输入作品标题'); return }
    if (!genre) { toast.error('请选择故事类型'); return }
    setGeneratingSynopsis(true)
    try {
      const result = await audiobookApi.generateNsfwSynopsis({
        title,
        genre: subgenre ? `${genre} - ${subgenre}` : genre,
        subgenre,
        protagonist_type: protagonistType,
        tone,
        conflict_scale: conflictScale,
        num_characters: numCharacters,
        num_chapters: numChapters,
        violence_level: violenceLevel,
        eroticism_level: eroticismLevel,
      })
      setSynopsis(result)
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setGeneratingSynopsis(false)
    }
  }

  const handleCreate = async () => {
    if (!title) { toast.error('请输入作品标题'); return }
    if (!synopsis) { toast.error('请先生成故事简介'); return }
    setLoading(true)
    try {
      await audiobookApi.createNsfwScript({
        title,
        genre: subgenre ? `${genre} - ${subgenre}` : genre,
        subgenre,
        premise: synopsis,
        style: tone,
        num_characters: numCharacters,
        num_chapters: numChapters,
        violence_level: violenceLevel,
        eroticism_level: eroticismLevel,
      } as NsfwScriptGenerationRequest)
      toast.success('NSFW剧本生成任务已创建')
      reset()
      onCreated()
      onClose()
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setLoading(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={v => { if (!v) { reset(); onClose() } }}>
      <DialogContent className="sm:max-w-2xl max-h-[90vh] flex flex-col">
        <DialogHeader className="shrink-0">
          <DialogTitle className="flex items-center gap-2">
            <NsfwIcon className="h-4 w-4 text-red-500" />
            NSFW 生成剧本
          </DialogTitle>
        </DialogHeader>
        <div className="flex-1 overflow-y-auto space-y-4 pr-1">
          <div className="space-y-1">
            <p className="text-xs text-muted-foreground">作品标题</p>
            <Input placeholder="输入作品标题" value={title} onChange={e => setTitle(e.target.value)} />
          </div>

          <div className="space-y-2">
            <p className="text-xs text-muted-foreground">故事类型</p>
            <div className="flex flex-wrap gap-1.5">
              {genreKeys.map(g => (
                <Chip key={g} label={NSFW_GENRE_CONFIGS[g].label} selected={genre === g} onClick={() => handleGenreSelect(g)} />
              ))}
            </div>
          </div>

          {genre && subgenreKeys.length > 0 && (
            <div className="space-y-2">
              <p className="text-xs text-muted-foreground">子类型</p>
              <div className="flex flex-wrap gap-1.5">
                {subgenreKeys.map(s => (
                  <Chip key={s} label={s} selected={subgenre === s} onClick={() => handleSubgenreSelect(s)} />
                ))}
              </div>
            </div>
          )}

          {subgenreConfig && (
            <>
              <div className="space-y-2">
                <p className="text-xs text-muted-foreground">主角类型</p>
                <div className="flex flex-wrap gap-1.5">
                  {subgenreConfig.protagonistTypes.map(p => (
                    <Chip key={p} label={p} selected={protagonistType === p} onClick={() => setProtagonistType(protagonistType === p ? '' : p)} />
                  ))}
                </div>
              </div>
              <div className="space-y-2">
                <p className="text-xs text-muted-foreground">故事基调</p>
                <div className="flex flex-wrap gap-1.5">
                  {subgenreConfig.tones.map(t => (
                    <Chip key={t} label={t} selected={tone === t} onClick={() => setTone(tone === t ? '' : t)} />
                  ))}
                </div>
              </div>
              <div className="space-y-2">
                <p className="text-xs text-muted-foreground">冲突规模</p>
                <div className="flex flex-wrap gap-1.5">
                  {subgenreConfig.conflictScales.map(c => (
                    <Chip key={c} label={c} selected={conflictScale === c} onClick={() => setConflictScale(conflictScale === c ? '' : c)} />
                  ))}
                </div>
              </div>
            </>
          )}

          <div className="flex gap-3">
            <label className="flex-1 flex flex-col gap-1 text-sm">
              <span className="text-muted-foreground text-xs">角色数量（2-10）</span>
              <Input type="number" min={2} max={10} value={numCharacters} onChange={e => setNumCharacters(Math.min(10, Math.max(2, Number(e.target.value))))} />
            </label>
            <label className="flex-1 flex flex-col gap-1 text-sm">
              <span className="text-muted-foreground text-xs">章节数量（2-30）</span>
              <Input type="number" min={2} max={30} value={numChapters} onChange={e => setNumChapters(Math.min(30, Math.max(2, Number(e.target.value))))} />
            </label>
          </div>

          <div className="flex gap-3">
            <label className="flex-1 flex flex-col gap-1 text-sm">
              <span className="text-muted-foreground text-xs">暴力程度：{violenceLevel}/10</span>
              <input type="range" min={0} max={10} step={1} value={violenceLevel} onChange={e => setViolenceLevel(Number(e.target.value))} className="w-full accent-red-500" />
            </label>
            <label className="flex-1 flex flex-col gap-1 text-sm">
              <span className="text-muted-foreground text-xs">色情程度：{eroticismLevel}/10</span>
              <input type="range" min={0} max={10} step={1} value={eroticismLevel} onChange={e => setEroticismLevel(Number(e.target.value))} className="w-full accent-pink-500" />
            </label>
          </div>

          <div className="flex justify-end">
            <Button size="sm" variant="outline" onClick={handleGenerateSynopsis} disabled={!genre || generatingSynopsis}>
              {generatingSynopsis ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
              {generatingSynopsis ? '生成中...' : '生成故事简介'}
            </Button>
          </div>

          {synopsis && (
            <div className="space-y-2">
              <p className="text-xs text-muted-foreground">故事简介（可编辑）</p>
              <Textarea rows={6} value={synopsis} onChange={e => setSynopsis(e.target.value)} className="text-sm" />
            </div>
          )}
        </div>

        <div className="flex justify-between gap-2 pt-3 shrink-0 border-t">
          <Button size="sm" variant="ghost" onClick={() => { reset(); onClose() }} disabled={loading}>取消</Button>
          <div className="flex gap-2">
            {synopsis && (
              <Button size="sm" variant="outline" onClick={handleGenerateSynopsis} disabled={generatingSynopsis}>
                <RotateCcw className="h-3 w-3 mr-1" />
                重新生成
              </Button>
            )}
            <Button size="sm" onClick={handleCreate} disabled={loading || !synopsis || !title}>
              {loading ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
              {loading ? '创建中...' : '生成剧本'}
            </Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  )
}

function ContinueScriptDialog({ open, onClose, onConfirm }: { open: boolean; onClose: () => void; onConfirm: (n: number) => Promise<void> }) {
  const { t } = useTranslation('audiobook')
  const [count, setCount] = useState(4)
  const [loading, setLoading] = useState(false)

  const handleSubmit = async () => {
    setLoading(true)
    try {
      await onConfirm(count)
    } finally {
      setLoading(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={v => { if (!v) onClose() }}>
      <DialogContent className="sm:max-w-sm">
        <DialogHeader>
          <DialogTitle>{t('projectCard.continueScriptDialog.title')}</DialogTitle>
        </DialogHeader>
        <div className="space-y-3 py-2">
          <label className="flex flex-col gap-1 text-sm">
            <span className="text-muted-foreground text-xs">{t('projectCard.continueScriptDialog.label')}</span>
            <Input
              type="number" min={1} max={20} value={count}
              onChange={e => setCount(Math.min(20, Math.max(1, Number(e.target.value))))}
            />
          </label>
        </div>
        <div className="flex justify-end gap-2 pt-2">
          <Button size="sm" variant="ghost" onClick={onClose} disabled={loading}>{t('projectCard.segments.cancel')}</Button>
          <Button size="sm" onClick={handleSubmit} disabled={loading}>
            {loading ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
            {loading ? t('projectCard.continueScriptDialog.starting') : t('projectCard.continueScriptDialog.start')}
          </Button>
        </div>
      </DialogContent>
    </Dialog>
  )
}

function ProjectListSidebar({
  projects,
  selectedId,
  onSelect,
  onNew,
  onAIScript,
  onNSFWScript,
  onLLM,
  loading,
  collapsed,
  onToggle,
  isSuperuser,
  hasNsfwAccess,
}: {
  projects: AudiobookProject[]
  selectedId: number | null
  onSelect: (id: number) => void
  onNew: () => void
  onAIScript: () => void
  onNSFWScript: () => void
  onLLM: () => void
  loading: boolean
  collapsed: boolean
  onToggle: () => void
  isSuperuser?: boolean
  hasNsfwAccess?: boolean
}) {
  const { t } = useTranslation('audiobook')
  return (
    <div className={`${collapsed ? 'w-10' : 'w-64'} shrink-0 flex flex-col bg-muted/30 overflow-hidden transition-all duration-200`}>
      <div className="h-16 flex items-center shrink-0 px-2 gap-1">
        {!collapsed && (
          <div className="flex items-center gap-2 flex-1 min-w-0 ml-1">
            <img src="/qwen.svg" alt="Qwen" className="h-5 w-5 shrink-0" />
            <span className="text-sm font-semibold truncate">{t('title')}</span>
          </div>
        )}
        <Button size="icon" variant="ghost" className="h-8 w-8 shrink-0" onClick={onToggle}>
          {collapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
        </Button>
      </div>
      {!collapsed && (
        <>
          <div className="flex items-center justify-end px-2 pb-1 gap-0.5 shrink-0">
            {isSuperuser && (
              <Button size="icon" variant="ghost" className="h-7 w-7" onClick={onLLM} title={t('llmConfig')}>
                <Settings2 className="h-4 w-4" />
              </Button>
            )}
            {hasNsfwAccess && (
              <Button size="icon" variant="ghost" className="h-7 w-7 text-red-500 hover:text-orange-600" onClick={onNSFWScript} title="NSFW 生成剧本">
                <NsfwIcon className="h-4 w-4" />
              </Button>
            )}
            <Button size="icon" variant="ghost" className="h-7 w-7" onClick={onAIScript} title="AI 生成剧本">
              <Zap className="h-4 w-4" />
            </Button>
            <Button size="icon" variant="ghost" className="h-7 w-7" onClick={onNew} title={t('newProject')}>
              <Plus className="h-4 w-4" />
            </Button>
          </div>
          <div className="flex-1 overflow-y-auto px-2 py-1 space-y-1.5">
            {loading ? (
              <div className="px-3 py-4 text-xs text-muted-foreground">{t('loading')}</div>
            ) : projects.length === 0 ? (
              <div className="px-3 py-4 text-xs text-muted-foreground">{t('noProjects')}</div>
            ) : (
              projects.map(p => {
                const isNsfw = p.source_type === 'ai_generated' && !!p.script_config?.nsfw_mode
                const ProjectIcon = p.source_type === 'epub'
                  ? BookOpen
                  : isNsfw
                    ? NsfwIcon
                    : p.source_type === 'ai_generated'
                      ? Zap
                      : Book
                const iconClass = isNsfw
                  ? 'h-3.5 w-3.5 shrink-0 text-red-500'
                  : 'h-3.5 w-3.5 shrink-0 text-muted-foreground'
                return (
                  <button
                    key={p.id}
                    onClick={() => onSelect(p.id)}
                    className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
                      selectedId === p.id
                        ? 'bg-muted/60 border-border shadow-sm'
                        : 'bg-muted/20 border-border/30 hover:bg-muted/40'
                    }`}
                  >
                    <div className="flex items-center gap-2">
                      {(() => { const sc = STATUS_CONFIG[p.status]; return sc ? <sc.Icon className={`${sc.iconCls} shrink-0`} /> : null })()}
                      <ProjectIcon className={iconClass} />
                      <span className="text-sm font-medium truncate flex-1" title={p.title}>{p.title}</span>
                    </div>
                  </button>
                )
              })
            )}
          </div>
        </>
      )}
    </div>
  )
}

function EmptyState() {
  const { t } = useTranslation('audiobook')
  return (
    <div className="flex-1 flex items-center justify-center text-muted-foreground">
      <div className="text-center">
        <Book className="h-12 w-12 mx-auto mb-3 opacity-30" />
        <p>{t('noProjects')}</p>
        <p className="text-sm mt-1">{t('noProjectsHint')}</p>
      </div>
    </div>
  )
}

function CharactersPanel({
  project,
  detail,
  loadingAction,
  onConfirm,
  onAnalyze,
  onFetchDetail,
  collapsed,
  onToggle,
  onScrollToChapter,
}: {
  project: AudiobookProject
  detail: AudiobookProjectDetail | null
  loadingAction: boolean
  onConfirm: () => void
  onAnalyze: () => void
  onFetchDetail: () => Promise<void>
  collapsed: boolean
  onToggle: () => void
  onScrollToChapter: (chapterId: number) => void
}) {
  const { t } = useTranslation('audiobook')
  const [editingCharId, setEditingCharId] = useState<number | null>(null)
  const [editFields, setEditFields] = useState({ name: '', gender: '', description: '', instruct: '' })
  const [regeneratingVoices, setRegeneratingVoices] = useState<Set<number>>(new Set())
  const [voiceKeys, setVoiceKeys] = useState<Record<number, number>>({})
  const status = project.status
  const isActive = ['analyzing', 'generating'].includes(status)

  useEffect(() => {
    if (status !== 'characters_ready') setEditingCharId(null)
  }, [status])

  const startEditChar = (char: AudiobookCharacter) => {
    setEditingCharId(char.id)
    setEditFields({ name: char.name, gender: char.gender || '', description: char.description || '', instruct: char.instruct || '' })
  }

  const saveEditChar = async (char: AudiobookCharacter) => {
    try {
      const updated = await audiobookApi.updateCharacter(project.id, char.id, {
        name: editFields.name || char.name,
        gender: editFields.gender || undefined,
        description: editFields.description,
        instruct: editFields.instruct,

      })
      setEditingCharId(null)
      await onFetchDetail()
      if (updated.voice_design_id) {
        handleRegeneratePreview(char.id)
      } else {
        toast.success(t('projectCard.characters.savedSuccess'))
      }
    } catch (e: any) {
      toast.error(formatApiError(e))
    }
  }

  const handleRegeneratePreview = async (charId: number) => {
    setRegeneratingVoices(prev => new Set(prev).add(charId))
    try {
      await audiobookApi.regenerateCharacterPreview(project.id, charId)
      toast.success(t('projectCard.characters.savedSuccess'))
      setVoiceKeys(prev => ({ ...prev, [charId]: (prev[charId] || 0) + 1 }))
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setRegeneratingVoices(prev => { const n = new Set(prev); n.delete(charId); return n })
    }
  }

  const handleRegenerateAllPreviews = async () => {
    const charsWithVoice = detail?.characters.filter(c => c.voice_design_id) ?? []
    if (charsWithVoice.length === 0) return
    const ids = charsWithVoice.map(c => c.id)
    setRegeneratingVoices(new Set(ids))
    for (const id of ids) {
      try {
        await audiobookApi.regenerateCharacterPreview(project.id, id)
        setVoiceKeys(prev => ({ ...prev, [id]: (prev[id] || 0) + 1 }))
      } catch (e: any) {
        toast.error(`${detail?.characters.find(c => c.id === id)?.name}: ${formatApiError(e)}`)
      } finally {
        setRegeneratingVoices(prev => { const n = new Set(prev); n.delete(id); return n })
      }
    }
    toast.success(t('projectCard.characters.regenerateAllDone'))
  }


  const charCount = detail?.characters.length ?? 0
  const hasChaptersOutline = (detail?.chapters.length ?? 0) > 0

  if (collapsed && hasChaptersOutline) {
    return (
      <div className="w-44 shrink-0 flex flex-col rounded-xl border border-border/40 bg-muted/20 overflow-hidden">
        <div className="flex items-center justify-between px-2 py-2 shrink-0 border-b border-border/30">
          <span className="text-xs font-medium text-muted-foreground truncate">
            {t('projectCard.chapters.title', { count: detail!.chapters.length })}
          </span>
          <Button size="icon" variant="ghost" className="h-6 w-6 shrink-0 ml-1" onClick={onToggle}>
            <PanelLeftOpen className="h-3.5 w-3.5" />
          </Button>
        </div>
        <div className="flex-1 overflow-y-auto px-2 py-2 space-y-1.5">
          {detail!.chapters.map(ch => {
            const chTitle = ch.title || t('projectCard.chapters.defaultTitle', { index: ch.chapter_index + 1 })
            const dotClass = ch.status === 'error' ? 'bg-destructive' : ch.status === 'parsing' || ch.status === 'generating' ? 'bg-muted-foreground/80' : ch.status === 'done' ? 'bg-muted-foreground/30' : ch.status === 'ready' ? 'bg-muted-foreground/50' : 'bg-muted-foreground/25'
            return (
              <button
                key={ch.id}
                className="w-full flex items-center gap-1.5 px-3 py-2 text-left rounded-lg border bg-muted/30 shadow-sm hover:bg-muted/60 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
                onClick={() => onScrollToChapter(ch.id)}
              >
                <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dotClass}`} />
                <span className="text-xs truncate">{chTitle}</span>
              </button>
            )
          })}
        </div>
      </div>
    )
  }

  return (
    <div className="w-72 shrink-0 flex flex-col rounded-xl border border-blue-500/20 bg-blue-500/5 overflow-hidden">
      <div className="flex items-center justify-between px-3 py-2 shrink-0">
        <span className="text-xs font-medium text-blue-400/80">
          {t('projectCard.characters.title', { count: charCount })}
        </span>
        <div className="flex items-center gap-1">
        {!isActive && status !== 'pending' && charCount > 0 && (
          <Button size="xs" variant="ghost" className="text-muted-foreground" onClick={onAnalyze} disabled={loadingAction}>
            {t('projectCard.reanalyze')}
          </Button>
        )}
        {!isActive && status === 'characters_ready' && (detail?.characters.some(c => c.voice_design_id) ?? false) && (
          <Button
            size="xs"
            variant="ghost"
            className="text-muted-foreground"
            onClick={handleRegenerateAllPreviews}
            disabled={regeneratingVoices.size > 0}
          >
            {regeneratingVoices.size > 0
              ? <><Loader2 className="h-3 w-3 mr-1 animate-spin" />全部</>
              : <><RefreshCw className="h-3 w-3 mr-1" />全部</>
            }
          </Button>
        )}
        {hasChaptersOutline && (
          <Button size="icon" variant="ghost" className="h-6 w-6" onClick={onToggle}>
            <PanelLeftClose className="h-3.5 w-3.5" />
          </Button>
        )}
        </div>
      </div>

      <Dialog open={editingCharId !== null} onOpenChange={open => { if (!open) setEditingCharId(null) }}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>{t('projectCard.characters.editTitle', { name: detail?.characters.find(c => c.id === editingCharId)?.name ?? '' })}</DialogTitle>
          </DialogHeader>
          <div className="space-y-3 py-2">
            <div className="space-y-1">
              <label className="text-sm font-medium">{t('projectCard.characters.namePlaceholder')}</label>
              <Input
                value={editFields.name}
                onChange={e => setEditFields(f => ({ ...f, name: e.target.value }))}
                placeholder={t('projectCard.characters.namePlaceholder')}
              />
            </div>
            <div className="space-y-1">
              <label className="text-sm font-medium">{t('projectCard.characters.genderPlaceholder')}</label>
              <select
                className="w-full h-9 rounded-md border border-input bg-background px-3 py-1 text-sm"
                value={editFields.gender}
                onChange={e => setEditFields(f => ({ ...f, gender: e.target.value }))}
              >
                <option value="">{t('projectCard.characters.genderPlaceholder')}</option>
                <option value="男">{t('projectCard.characters.genderMale')}</option>
                <option value="女">{t('projectCard.characters.genderFemale')}</option>
                <option value="未知">{t('projectCard.characters.genderUnknown')}</option>
              </select>
            </div>
            <div className="space-y-1">
              <label className="text-sm font-medium">{t('projectCard.characters.instructPlaceholder')}</label>
              <Textarea
                value={editFields.instruct}
                onChange={e => setEditFields(f => ({ ...f, instruct: e.target.value }))}
                placeholder={t('projectCard.characters.instructPlaceholder')}
                rows={4}
                className="resize-none"
              />
            </div>
            <div className="space-y-1">
              <label className="text-sm font-medium">{t('projectCard.characters.descPlaceholder')}</label>
              <Input
                value={editFields.description}
                onChange={e => setEditFields(f => ({ ...f, description: e.target.value }))}
                placeholder={t('projectCard.characters.descPlaceholder')}
              />
            </div>
          </div>
          <DialogFooter>
            <Button variant="ghost" onClick={() => setEditingCharId(null)}>{t('common:cancel')}</Button>
            {editingCharId !== null && detail && (() => {
              const editingChar = detail.characters.find(c => c.id === editingCharId)
              return editingChar ? (
                <Button onClick={() => saveEditChar(editingChar)}>
                  <Check className="h-3 w-3 mr-1" />{t('common:save')}
                </Button>
              ) : null
            })()}
          </DialogFooter>
        </DialogContent>
      </Dialog>
      {(!detail || charCount === 0) ? (
        <div className="flex-1 flex items-center justify-center text-xs text-muted-foreground px-3 text-center">
          {status === 'pending' ? t('stepHints.pending') : t('projectCard.characters.title', { count: 0 })}
        </div>
      ) : (
        <div className="flex-1 overflow-y-auto px-2 py-2 space-y-1.5">
          {detail.characters.map(char => (
            <div key={char.id} className="border rounded-lg px-3 py-2 bg-muted/30 shadow-sm">
              <div className="flex flex-col gap-1 text-sm">
                  <div className="flex items-center justify-between gap-1">
                    <div className="flex items-center gap-1.5 min-w-0">
                      <span className={`font-medium truncate ${char.gender === '男' ? 'text-blue-400' : char.gender === '女' ? 'text-pink-400' : ''}`}>{char.name}</span>
                    </div>
                    <div className="flex items-center gap-1 shrink-0">
{status === 'characters_ready' && (
                        <Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => startEditChar(char)}>
                          <Pencil className="h-3 w-3" />
                        </Button>
                      )}
                    </div>
                  </div>
                  {char.description && <span className="text-xs text-muted-foreground">{char.description}</span>}
                  {char.instruct && <span className="text-xs text-muted-foreground/70">{char.instruct}</span>}
                  {!char.voice_design_id && (
                    <div className="text-xs text-muted-foreground/60">{t('projectCard.characters.noVoice')}</div>
                  )}
                </div>
              {!editingCharId && char.voice_design_id && (
                <div className="mt-2 flex items-center gap-2">
                  <div className="flex-1 min-w-0">
                    {regeneratingVoices.has(char.id)
                      ? <div className="flex items-center gap-1.5 text-xs text-muted-foreground py-1"><Loader2 className="h-3 w-3 animate-spin" />{t('projectCard.characters.regeneratingPreview')}</div>
                      : <LazyAudioPlayer
                          key={`audio-${char.id}-${voiceKeys[char.id] || 0}`}
                          audioUrl={`${audiobookApi.getCharacterAudioUrl(project.id, char.id)}?t=${voiceKeys[char.id] || 0}`}
                          jobId={char.id}
                          compact
                        />
                    }
                  </div>
                  {status === 'characters_ready' && !regeneratingVoices.has(char.id) && (
                    <Button
                      size="sm"
                      variant="ghost"
                      className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground shrink-0"
                      onClick={() => handleRegeneratePreview(char.id)}
                    >
                      <RefreshCw className="h-3 w-3" />
                    </Button>
                  )}
                </div>
              )}
            </div>
          ))}
        </div>
      )}

      {status === 'characters_ready' && (
        <div className="px-2 py-2 border-t shrink-0">
          <Button
            className="w-full"
            onClick={onConfirm}
            disabled={loadingAction}
          >
            {loadingAction
              ? t('projectCard.confirm.loading')
              : project.source_type === 'ai_generated'
                ? t('projectCard.confirm.generateScript', '确认角色并生成剧本')
                : t('projectCard.confirm.button')}
          </Button>
        </div>
      )}
    </div>
  )
}

const EMOTION_OPTIONS = ['开心', '愤怒', '悲伤', '恐惧', '厌恶', '低沉', '惊讶']

const EMO_LEVEL_MAX: Record<string, number> = {
  '开心': 0.75, '愤怒': 0.08, '悲伤': 0.90,
  '恐惧': 0.10, '厌恶': 0.50, '低沉': 0.35, '惊讶': 0.35,
}


function ChaptersPanel({
  project,
  detail,
  segments,
  loadingAction,
  generatingChapterIndices,
  sequentialPlayingId,
  onParseChapter,
  onGenerate,
  onParseAll,
  onGenerateAll,
  onProcessAll,
  isBackgroundGenerating,
  onContinueScript,
  onDownload,
  onSequentialPlayingChange,
  onUpdateSegment,
  onRegenerateSegment,
  scrollToChapterId,
  onScrollToChapterDone,
}: {
  project: AudiobookProject
  detail: AudiobookProjectDetail | null
  segments: AudiobookSegment[]
  loadingAction: boolean
  generatingChapterIndices: Set<number>
  sequentialPlayingId: number | null
  onParseChapter: (chapterId: number, title?: string) => void
  onGenerate: (chapterIndex?: number, force?: boolean) => void
  onParseAll: () => void
  onGenerateAll: () => void
  onProcessAll: () => void
  isBackgroundGenerating: boolean
  onContinueScript?: () => void
  onDownload: (chapterIndex?: number) => void
  onSequentialPlayingChange: (id: number | null) => void
  onUpdateSegment: (segmentId: number, data: { text: string; emo_text?: string | null; emo_alpha?: number | null }) => Promise<void>
  onRegenerateSegment: (segmentId: number) => Promise<void>
  scrollToChapterId: number | null
  onScrollToChapterDone: () => void
}) {
  const { t } = useTranslation('audiobook')
  const [expandedChapters, setExpandedChapters] = useState<Set<number>>(new Set())
  const [editingSegId, setEditingSegId] = useState<number | null>(null)
  const [editText, setEditText] = useState('')
  const [editEmoSelections, setEditEmoSelections] = useState<string[]>([])
  const [editEmoWeights, setEditEmoWeights] = useState<Record<string, number>>({})
  const [savingSegId, setSavingSegId] = useState<number | null>(null)
  const [regeneratingSegs, setRegeneratingSegs] = useState<Set<number>>(new Set())
  const [audioVersions, setAudioVersions] = useState<Record<number, number>>({})
  const [chapterPlayerChIdx, setChapterPlayerChIdx] = useState<number | null>(null)
  const [confirmAction, setConfirmAction] = useState<{ label: string; onConfirm: () => void } | null>(null)
  const prevSegStatusRef = useRef<Record<number, string>>({})
  const initialExpandDoneRef = useRef(false)
  const segRefs = useRef<Record<number, HTMLDivElement | null>>({})
  const scrollContainerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!scrollToChapterId) return
    const el = document.getElementById(`ch-${scrollToChapterId}`)
    const container = scrollContainerRef.current
    if (el && container) {
      const top = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop
      container.scrollTo({ top, behavior: 'smooth' })
    }
    onScrollToChapterDone()
  }, [scrollToChapterId, onScrollToChapterDone])

  useEffect(() => {
    if (sequentialPlayingId === null || !detail) return
    const seg = segments.find(s => s.id === sequentialPlayingId)
    if (!seg) return
    const ch = detail.chapters.find(c => c.chapter_index === seg.chapter_index)
    if (ch) setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
    setTimeout(() => {
      segRefs.current[sequentialPlayingId]?.scrollIntoView({ behavior: 'smooth', block: 'center' })
    }, 50)
  }, [sequentialPlayingId, detail, segments])

  useEffect(() => {
    const bumps: Record<number, number> = {}
    segments.forEach(seg => {
      if (prevSegStatusRef.current[seg.id] === 'generating' && seg.status === 'done') {
        bumps[seg.id] = (audioVersions[seg.id] ?? 0) + 1
      }
    })
    if (Object.keys(bumps).length > 0) {
      setAudioVersions(prev => ({ ...prev, ...bumps }))
    }
    const next: Record<number, string> = {}
    segments.forEach(seg => { next[seg.id] = seg.status })
    prevSegStatusRef.current = next
  }, [segments])

  const startEdit = (seg: AudiobookSegment) => {
    setEditingSegId(seg.id)
    setEditText(seg.text)
    const rawEmo = seg.emo_text || ''
    const alpha = seg.emo_alpha ?? 5
    if (!rawEmo) {
      setEditEmoSelections([])
      setEditEmoWeights({})
      return
    }
    const tokens = rawEmo.split('+').filter(Boolean)
    const selections: string[] = []
    const weights: Record<string, number> = {}
    if (tokens.length === 1) {
      const [name] = tokens[0].split(':')
      const emoName = name.trim()
      selections.push(emoName)
      // Convert old float alpha to level if needed
      weights[emoName] = alpha > 1 ? Math.round(alpha) : Math.round(alpha / (EMO_LEVEL_MAX[emoName] || 0.35) * 10)
    } else {
      for (const tok of tokens) {
        const [name, w] = tok.split(':')
        const emo = name.trim()
        selections.push(emo)
        const rawW = w ? parseFloat(w) : (EMO_LEVEL_MAX[emo] || 0.35) * 0.5
        weights[emo] = Math.round(rawW / (EMO_LEVEL_MAX[emo] || 0.35) * 10)
      }
    }
    setEditEmoSelections(selections)
    setEditEmoWeights(weights)
  }

  const cancelEdit = () => setEditingSegId(null)

  const saveEdit = async (segId: number) => {
    setSavingSegId(segId)
    try {
      let emo_text: string | null = null
      let emo_alpha: number | null = null
      if (editEmoSelections.length === 1) {
        emo_text = editEmoSelections[0]
        emo_alpha = editEmoWeights[editEmoSelections[0]] ?? 5
      } else if (editEmoSelections.length > 1) {
        emo_text = editEmoSelections.map(e => {
          const level = editEmoWeights[e] ?? 5
          const weight = parseFloat((level / 10 * (EMO_LEVEL_MAX[e] || 0.35)).toFixed(4))
          return `${e}:${weight}`
        }).join('+')
        emo_alpha = 1.0
      }
      await onUpdateSegment(segId, { text: editText, emo_text, emo_alpha })
      setEditingSegId(null)
    } finally {
      setSavingSegId(null)
    }
  }

  const handleRegenerate = async (segId: number) => {
    setRegeneratingSegs(prev => new Set([...prev, segId]))
    try {
      await onRegenerateSegment(segId)
    } finally {
      setRegeneratingSegs(prev => { const n = new Set(prev); n.delete(segId); return n })
    }
  }

  const status = project.status
  const doneCount = segments.filter(s => s.status === 'done').length

  useEffect(() => {
    if (!detail || segments.length === 0) return
    const generatingChapterIds = detail.chapters
      .filter(ch => segments.some(s => s.chapter_index === ch.chapter_index && s.status === 'generating'))
      .map(ch => ch.id)
    if (generatingChapterIds.length > 0) {
      setExpandedChapters(prev => {
        const next = new Set(prev)
        generatingChapterIds.forEach(id => next.add(id))
        return next.size === prev.size ? prev : next
      })
      return
    }
    if (!initialExpandDoneRef.current) {
      initialExpandDoneRef.current = true
      const chapterIdsWithSegs = detail.chapters
        .filter(ch => segments.some(s => s.chapter_index === ch.chapter_index))
        .map(ch => ch.id)
      if (chapterIdsWithSegs.length > 0) {
        setExpandedChapters(new Set(chapterIdsWithSegs))
      }
    }
  }, [segments, detail])

  const isAIMode = project.source_type === 'ai_generated'
  const hasChapters = detail && detail.chapters.length > 0 && ['analyzing', 'ready', 'generating', 'done'].includes(status)

  return (
    <>
    <Dialog open={!!confirmAction} onOpenChange={open => { if (!open) setConfirmAction(null) }}>
      <DialogContent className="max-w-xs">
        <DialogHeader>
          <DialogTitle>确认操作</DialogTitle>
        </DialogHeader>
        <p className="text-sm text-muted-foreground">{confirmAction?.label}</p>
        <div className="flex justify-end gap-2 mt-2">
          <Button variant="ghost" size="sm" onClick={() => setConfirmAction(null)}>取消</Button>
          <Button variant="destructive" size="sm" onClick={() => { confirmAction?.onConfirm(); setConfirmAction(null) }}>确认</Button>
        </div>
      </DialogContent>
    </Dialog>
    <div className="flex-1 flex flex-col rounded-xl border border-border/40 bg-muted/20 overflow-hidden">
      <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between px-3 py-2 shrink-0">
        <span className="text-xs font-medium text-muted-foreground">
          {t('projectCard.chapters.title', { count: detail?.chapters.length ?? 0 })}
        </span>
        {hasChapters && (
          <div className="flex items-center gap-1 flex-wrap">
            {doneCount > 0 && <SequentialPlayer segments={segments} projectId={project.id} onPlayingChange={onSequentialPlayingChange} />}
            {detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && !isBackgroundGenerating && (
              <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onParseAll}>
                {isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}全部
              </Button>
            )}
            {detail!.chapters.some(c => c.status === 'ready') && (
              <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onGenerateAll}>
                <Volume2 className="h-3 w-3 mr-1" />全部
              </Button>
            )}
            {detail!.chapters.some(c => ['pending', 'error'].includes(c.status)) && detail!.chapters.some(c => c.status === 'ready') && (
              <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onProcessAll}>
                {loadingAction ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Zap className="h-3 w-3 mr-1" />}全部
              </Button>
            )}
            {isAIMode && onContinueScript && (
              <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={onContinueScript}>
                <Plus className="h-3 w-3 mr-1" />续写
              </Button>
            )}
          </div>
        )}
      </div>

      {!hasChapters ? (
        <div className="flex-1 flex items-center justify-center text-xs text-muted-foreground" />
      ) : (
        <div className="flex-1 overflow-y-auto px-3 py-2 space-y-2" ref={scrollContainerRef}>
          {detail!.chapters.map(ch => {
            const chSegs = segments.filter(s => s.chapter_index === ch.chapter_index)
            const chDone = chSegs.filter(s => s.status === 'done').length
            const chTotal = chSegs.length
            const chGenerating = chSegs.some(s => s.status === 'generating')
            const chAllDone = chTotal > 0 && chDone === chTotal
            const chTitle = ch.title || t('projectCard.chapters.defaultTitle', { index: ch.chapter_index + 1 })
            const chExpanded = expandedChapters.has(ch.id)
            const toggleChExpand = () => setExpandedChapters(prev => {
              const next = new Set(prev)
              if (next.has(ch.id)) next.delete(ch.id)
              else next.add(ch.id)
              return next
            })
            const chCardBorder = ch.status === 'error' ? 'border-destructive/40' : ch.status === 'parsing' || ch.status === 'generating' || chGenerating ? 'border-muted-foreground/50' : ch.status === 'done' || (ch.status === 'ready' && chAllDone) ? 'border-border/30' : ch.status === 'ready' ? 'border-muted-foreground/30' : 'border-border/25'
            return (
              <div key={ch.id} id={`ch-${ch.id}`} className={`rounded-xl border overflow-clip ${chCardBorder}`}>
                <button
                  className="sticky top-0 z-10 w-full flex items-center gap-2 px-3 py-2.5 bg-background/90 backdrop-blur-sm hover:bg-muted/60 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
                  onClick={toggleChExpand}
                >
                  <span className="shrink-0 text-muted-foreground">
                    {chExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
                  </span>
                  <span className="text-xs font-medium flex-1 truncate">{chTitle}</span>
                  <span className="shrink-0 flex items-center gap-1.5" onClick={e => e.stopPropagation()}>
                    {ch.status === 'pending' && (
                      isBackgroundGenerating ? (
                        <span className="flex items-center gap-1 text-[11px] text-muted-foreground">
                          <Loader2 className="h-3 w-3 animate-spin" />排队中
                        </span>
                      ) : (
                        <Button size="xs" variant="ghost" className="text-muted-foreground" onClick={() => onParseChapter(ch.id, ch.title)}>
                          {isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
                          {t(isAIMode ? 'projectCard.chapters.parseAI' : 'projectCard.chapters.parse')}
                        </Button>
                      )
                    )}
                    {ch.status === 'parsing' && (
                      <span className="flex items-center gap-1 text-[11px] text-muted-foreground">
                        <Loader2 className="h-3 w-3 animate-spin" />{t('projectCard.chapters.parsing')}
                      </span>
                    )}
                    {ch.status === 'ready' && !chGenerating && !chAllDone && !generatingChapterIndices.has(ch.chapter_index) && (
                      <>
                        <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => {
                          setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
                          onGenerate(ch.chapter_index)
                        }}>
                          <Volume2 className="h-3 w-3 mr-1" />
                          {t('projectCard.chapters.generate')}
                        </Button>
                        <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => setConfirmAction({ label: '重新解析将删除已有台词，确认继续？', onConfirm: () => onParseChapter(ch.id, ch.title) })}>
                          {isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
                          {t(isAIMode ? 'projectCard.chapters.reparseAI' : 'projectCard.chapters.reparse')}
                        </Button>
                      </>
                    )}
                    {ch.status === 'ready' && (chGenerating || generatingChapterIndices.has(ch.chapter_index)) && (
                      <span className="flex items-center gap-1 text-[11px] text-muted-foreground">
                        <Loader2 className="h-3 w-3 animate-spin" />{t('projectCard.chapters.segmentProgress', { done: chDone, total: chTotal })}
                      </span>
                    )}
                    {(ch.status === 'done' || (ch.status === 'ready' && chAllDone)) && (
                      <>
                        <span className="text-[11px] text-muted-foreground">{t('projectCard.chapters.doneBadge', { count: chDone })}</span>
                        <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => setConfirmAction({ label: '重新生成将覆盖已有音频，确认继续？', onConfirm: () => { setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n }); onGenerate(ch.chapter_index, true) } })}>
                          <RefreshCw className="h-3 w-3 mr-0.5" /><Volume2 className="h-3 w-3 mr-1" />{t('projectCard.chapters.generate')}
                        </Button>
                        <Button
                          size="icon"
                          variant={chapterPlayerChIdx === ch.chapter_index ? 'secondary' : 'ghost'}
                          className="h-6 w-6"
                          onClick={() => setChapterPlayerChIdx(prev => prev === ch.chapter_index ? null : ch.chapter_index)}
                          title="播放本章"
                        >
                          <Headphones className="h-3 w-3" />
                        </Button>
                        <Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => onDownload(ch.chapter_index)} title={t('projectCard.downloadAll')}>
                          <Download className="h-3 w-3" />
                        </Button>
                        <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => setConfirmAction({ label: '重新解析将删除已有台词和音频，确认继续？', onConfirm: () => onParseChapter(ch.id, ch.title) })}>
                          {isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
                          {t(isAIMode ? 'projectCard.chapters.reparseAI' : 'projectCard.chapters.reparse')}
                        </Button>
                      </>
                    )}
                    {ch.status === 'error' && (
                      <Button size="xs" variant="ghost" className="text-destructive" onClick={() => onParseChapter(ch.id, ch.title)}>
                        {isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
                        {t(isAIMode ? 'projectCard.chapters.reparseAI' : 'projectCard.chapters.reparse')}
                      </Button>
                    )}
                  </span>
                </button>

                {ch.status === 'parsing' && (
                  <div className="px-3 py-1 border-t">
                    <LogStream projectId={project.id} chapterId={ch.id} active={ch.status === 'parsing'} />
                  </div>
                )}

                {chExpanded && chSegs.length > 0 && (
                  <div className="px-3 py-2 space-y-2 border-t">
                    {chSegs.map(seg => {
                      const isEditing = editingSegId === seg.id
                      const isRegenerating = regeneratingSegs.has(seg.id) || seg.status === 'generating'
                      const isSaving = savingSegId === seg.id
                      const audioV = audioVersions[seg.id] ?? 0
                      return (
                        <div
                          key={seg.id}
                          ref={el => { segRefs.current[seg.id] = el }}
                          className={`rounded-lg border overflow-hidden ${sequentialPlayingId === seg.id ? 'border-red-500 ring-1 ring-red-500' : 'bg-card'}`}
                        >
                          {/* Card header */}
                          <div className="flex items-center gap-1.5 px-3 py-2 border-b border-border/50">
                            <span className="text-xs text-muted-foreground shrink-0">
                              <span className="text-sm text-foreground">{seg.character_name || t('projectCard.segments.unknownCharacter')}</span>
                              {!isEditing && seg.emo_text && (
                                <>
                                  {' | '}
                                  {seg.emo_text.split('+').map((tok, i) => {
                                    const [name, w] = tok.split(':')
                                    return <span key={tok}>{i > 0 ? ' ' : ''}{name.trim()}{w ? `:${parseFloat(w).toFixed(2)}` : ''}</span>
                                  })}
                                  {seg.emo_alpha != null && seg.emo_alpha !== 1 && ` Lv.${seg.emo_alpha > 1 ? Math.round(seg.emo_alpha) : seg.emo_alpha}`}
                                </>
                              )}
                            </span>
                            {isRegenerating && <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />}
                            {!isRegenerating && seg.status === 'error' && (
                              <Badge variant="destructive" className="text-xs">{t('projectCard.segments.errorBadge')}</Badge>
                            )}
                            <div className="ml-auto flex items-center gap-0.5 shrink-0">
                              {!isEditing ? (
                                <>
                                  <Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => startEdit(seg)} disabled={isRegenerating} title={t('projectCard.segments.edit')}>
                                    <Pencil className="h-3 w-3" />
                                  </Button>
                                  <Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => handleRegenerate(seg.id)} disabled={isRegenerating} title={t('projectCard.segments.regenerate')}>
                                    <RefreshCw className="h-3 w-3" />
                                  </Button>
                                </>
                              ) : (
                                <>
                                  <Button size="icon" variant="ghost" className="h-6 w-6 text-destructive" onClick={cancelEdit} title={t('projectCard.segments.cancel')}>
                                    <X className="h-3 w-3" />
                                  </Button>
                                  <Button size="icon" variant="ghost" className="h-6 w-6 text-primary" onClick={() => saveEdit(seg.id)} disabled={isSaving} title={t('projectCard.segments.save')}>
                                    {isSaving ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
                                  </Button>
                                </>
                              )}
                            </div>
                          </div>

                          {/* Card body */}
                          <div className="px-3 py-2.5">
                            {isEditing ? (
                              <div className="space-y-2">
                                <Textarea
                                  value={editText}
                                  onChange={e => setEditText(e.target.value)}
                                  className="text-sm min-h-[60px] resize-y"
                                  rows={3}
                                />
                                <div className="space-y-1.5">
                                  <div className="flex items-center gap-1 flex-wrap">
                                    <span className="text-xs text-muted-foreground shrink-0">{t('projectCard.segments.emotion')}:</span>
                                    {EMOTION_OPTIONS.map(emo => {
                                      const isSelected = editEmoSelections.includes(emo)
                                      return (
                                        <button
                                          key={emo}
                                          type="button"
                                          className={`px-2 py-0.5 rounded text-xs border transition-colors ${isSelected ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-transparent"}`}
                                          onClick={() => {
                                            if (isSelected) {
                                              setEditEmoSelections(prev => prev.filter(e => e !== emo))
                                            } else {
                                              setEditEmoSelections(prev => [...prev, emo])
                                              setEditEmoWeights(prev => ({ ...prev, [emo]: prev[emo] ?? 5 }))
                                            }
                                          }}
                                        >
                                          {emo}
                                        </button>
                                      )
                                    })}
                                  </div>
                                  {editEmoSelections.map(emo => (
                                    <div key={emo} className="flex items-center gap-1.5">
                                      <span className="text-xs text-muted-foreground w-8 shrink-0">{emo}:</span>
                                      <input
                                        type="range"
                                        min={1}
                                        max={10}
                                        step={1}
                                        value={editEmoWeights[emo] ?? 5}
                                        onChange={e => setEditEmoWeights(prev => ({ ...prev, [emo]: Number(e.target.value) }))}
                                        className="flex-1 h-1.5 accent-primary"
                                      />
                                      <span className="text-xs text-muted-foreground w-6 text-right">{editEmoWeights[emo] ?? 5}</span>
                                    </div>
                                  ))}
                                </div>
                              </div>
                            ) : (
                              <p className="text-sm text-foreground/80 break-words leading-relaxed">{seg.text}</p>
                            )}
                          </div>

                          {/* Audio — integrated at bottom of card */}
                          {!isEditing && seg.status === 'done' && (
                            <div className="px-3 pb-2">
                              <LazyAudioPlayer
                                key={`seg-audio-${seg.id}-${audioV}`}
                                audioUrl={`${audiobookApi.getSegmentAudioUrl(project.id, seg.id)}?v=${audioV}`}
                                jobId={seg.id}
                              />
                            </div>
                          )}
                        </div>
                      )
                    })}
                  </div>
                )}
              </div>
            )
          })}
        </div>
      )}

      {chapterPlayerChIdx !== null && (() => {
        const activeCh = detail?.chapters.find(c => c.chapter_index === chapterPlayerChIdx)
        const activeSegs = segments.filter(s => s.chapter_index === chapterPlayerChIdx)
        if (!activeCh) return null
        return (
          <div className="shrink-0">
            <ChapterPlayer
              projectId={project.id}
              chapterIndex={chapterPlayerChIdx}
              chapterTitle={activeCh.title || t('projectCard.chapters.defaultTitle', { index: chapterPlayerChIdx + 1 })}
              segments={activeSegs}
              onClose={() => setChapterPlayerChIdx(null)}
            />
          </div>
        )
      })()}

    </div>
    </>
  )
}

export default function Audiobook() {
  const { t } = useTranslation('audiobook')
  const { user } = useAuth()
  const [projects, setProjects] = useState<AudiobookProject[]>([])
  const [selectedProjectId, setSelectedProjectId] = useState<number | null>(null)
  const [detail, setDetail] = useState<AudiobookProjectDetail | null>(null)
  const [segments, setSegments] = useState<AudiobookSegment[]>([])
  const [loading, setLoading] = useState(true)
  const [detailLoading, setDetailLoading] = useState(false)
  const [loadingAction, setLoadingAction] = useState(false)
  const [isPolling, setIsPolling] = useState(false)
  const [generatingChapterIndices, setGeneratingChapterIndices] = useState<Set<number>>(new Set())
  const [sequentialPlayingId, setSequentialPlayingId] = useState<number | null>(null)
  const [showCreate, setShowCreate] = useState(false)
  const [showAIScript, setShowAIScript] = useState(false)
  const [showNSFWScript, setShowNSFWScript] = useState(false)
  const [showContinueScript, setShowContinueScript] = useState(false)
  const [showLLM, setShowLLM] = useState(false)
  const [hasNsfwAccess, setHasNsfwAccess] = useState(false)
  const [sidebarOpen, setSidebarOpen] = useState(true)
  const [charactersCollapsed, setCharactersCollapsed] = useState(false)
  const [scrollToChapterId, setScrollToChapterId] = useState<number | null>(null)
  const prevStatusRef = useRef<string>('')
  const autoExpandedRef = useRef(new Set<string>())

  const selectedProject = projects.find(p => p.id === selectedProjectId) ?? null

  const fetchProjects = useCallback(async () => {
    try {
      const list = await audiobookApi.listProjects()
      setProjects(list)
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setLoading(false)
    }
  }, [])

  const fetchDetail = useCallback(async () => {
    if (!selectedProjectId) return
    try { setDetail(await audiobookApi.getProject(selectedProjectId)) } catch {}
  }, [selectedProjectId])

  const fetchSegments = useCallback(async () => {
    if (!selectedProjectId) return
    try { setSegments(await audiobookApi.getSegments(selectedProjectId)) } catch {}
  }, [selectedProjectId])

  useEffect(() => {
    fetchProjects()
    authApi.getNsfwAccess().then(r => setHasNsfwAccess(r.has_access)).catch(() => {})
  }, [fetchProjects])

  useEffect(() => {
    if (!selectedProjectId) {
      setDetail(null)
      setSegments([])
      return
    }
    setDetail(null)
    setSegments([])
    setGeneratingChapterIndices(new Set())
    setIsPolling(false)
    autoExpandedRef.current.clear()
    prevStatusRef.current = ''
    setDetailLoading(true)
    Promise.all([fetchDetail(), fetchSegments()]).finally(() => setDetailLoading(false))
  }, [selectedProjectId, fetchDetail, fetchSegments])

  const status = selectedProject?.status ?? ''

  useEffect(() => {
    if (!selectedProject) return
    if (['done', 'error'].includes(status)) setIsPolling(false)
    if (['characters_ready', 'ready', 'generating'].includes(status) && !autoExpandedRef.current.has(status)) {
      autoExpandedRef.current.add(status)
      fetchDetail()
      fetchSegments()
    }
  }, [status, selectedProject, fetchDetail, fetchSegments])

  useEffect(() => {
    if (!selectedProject) return
    if (prevStatusRef.current === 'generating' && status === 'done') {
      toast.success(t('projectCard.allDoneToast', { title: selectedProject.title }))
    }
    prevStatusRef.current = status
  }, [status, selectedProject, t])

  const hasParsingChapter = detail?.chapters.some(c => c.status === 'parsing') ?? false
  const isAiProject = selectedProject?.source_type === 'ai_generated'
  const hasPendingChapters = detail?.chapters.some(c => c.status === 'pending') ?? false
  const isBackgroundGenerating = isAiProject && (status === 'analyzing' || (status === 'ready' && hasPendingChapters))

  useEffect(() => {
    if (!isPolling) return
    if (['analyzing', 'generating'].includes(status)) return
    if (hasParsingChapter) return
    if (isAiProject && hasPendingChapters) return
    if (!segments.some(s => s.status === 'generating')) setIsPolling(false)
  }, [isPolling, status, segments, hasParsingChapter, isAiProject, hasPendingChapters])

  useEffect(() => {
    if (generatingChapterIndices.size === 0) return
    const done: number[] = []
    generatingChapterIndices.forEach(chIdx => {
      const chSegs = segments.filter(s => s.chapter_index === chIdx)
      if (chSegs.length > 0 && chSegs.every(s => s.status === 'done' || s.status === 'error')) {
        done.push(chIdx)
      }
    })
    if (done.length > 0) {
      setGeneratingChapterIndices(prev => {
        const n = new Set(prev)
        done.forEach(i => n.delete(i))
        return n
      })
    }
  }, [segments, generatingChapterIndices])

  const shouldPoll = isPolling || ['analyzing', 'generating'].includes(status) || hasParsingChapter || (isAiProject && hasPendingChapters) || generatingChapterIndices.size > 0 || segments.some(s => s.status === 'generating')

  useEffect(() => {
    if (!shouldPoll || !selectedProjectId) return
    const id = setInterval(() => { fetchProjects(); fetchSegments(); fetchDetail() }, 1500)
    return () => clearInterval(id)
  }, [shouldPoll, selectedProjectId, fetchProjects, fetchSegments, fetchDetail])

  const handleAnalyze = async () => {
    if (!selectedProject) return
    if (['characters_ready', 'ready', 'done'].includes(status)) {
      if (!confirm(t('projectCard.reanalyzeConfirm'))) return
    }
    autoExpandedRef.current.clear()
    setLoadingAction(true)
    setIsPolling(true)
    try {
      if (selectedProject.source_type === 'ai_generated') {
        await audiobookApi.regenerateCharacters(selectedProject.id)
      } else {
        await audiobookApi.analyze(selectedProject.id, { turbo: true })
      }
      toast.success(t('projectCard.analyzeStarted'))
      fetchProjects()
    } catch (e: any) {
      setIsPolling(false)
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleConfirm = async () => {
    if (!selectedProject) return
    setLoadingAction(true)
    try {
      await audiobookApi.confirmCharacters(selectedProject.id)
      if (selectedProject.source_type === 'ai_generated') setIsPolling(true)
      toast.success(t('projectCard.confirm.chaptersRecognized'))
      fetchProjects()
      fetchDetail()
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleParseChapter = async (chapterId: number, title?: string) => {
    if (!selectedProject) return
    try {
      await audiobookApi.parseChapter(selectedProject.id, chapterId)
      toast.success(title
        ? t('projectCard.chapters.parseStarted', { title })
        : t('projectCard.chapters.parseStartedDefault'))
      fetchDetail()
    } catch (e: any) {
      toast.error(formatApiError(e))
    }
  }

  const handleGenerate = async (chapterIndex?: number, force?: boolean) => {
    if (!selectedProject) return
    setLoadingAction(true)
    if (chapterIndex !== undefined) {
      setGeneratingChapterIndices(prev => new Set([...prev, chapterIndex]))
    } else {
      setIsPolling(true)
    }
    try {
      await audiobookApi.generate(selectedProject.id, chapterIndex, force)
      toast.success(chapterIndex !== undefined
        ? t('projectCard.chapters.generateStarted', { index: chapterIndex + 1 })
        : t('projectCard.chapters.generateAllStarted'))
      fetchProjects()
      fetchSegments()
    } catch (e: any) {
      if (chapterIndex !== undefined) {
        setGeneratingChapterIndices(prev => { const n = new Set(prev); n.delete(chapterIndex); return n })
      } else {
        setIsPolling(false)
      }
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleParseAll = async () => {
    if (!selectedProject) return
    setLoadingAction(true)
    setIsPolling(true)
    try {
      const isAI = selectedProject.source_type === 'ai_generated'
      await audiobookApi.parseAllChapters(selectedProject.id, false, isAI)
      toast.success(t('projectCard.chapters.parseAllStarted'))
      fetchProjects()
      fetchDetail()
    } catch (e: any) {
      setIsPolling(false)
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleRetryFailed = async () => {
    if (!selectedProject) return
    setIsPolling(true)
    try {
      await audiobookApi.parseAllChapters(selectedProject.id, true)
      toast.success(t('projectCard.chapters.parseAllStarted'))
      fetchProjects()
      fetchDetail()
    } catch (e: any) {
      setIsPolling(false)
      toast.error(formatApiError(e))
    }
  }

  const handleGenerateAll = async () => {
    if (!selectedProject || !detail) return
    setLoadingAction(true)
    const ready = detail.chapters.filter(c => c.status === 'ready')
    if (ready.length > 0) {
      setGeneratingChapterIndices(prev => new Set([...prev, ...ready.map(c => c.chapter_index)]))
    }
    setIsPolling(true)
    try {
      await audiobookApi.generate(selectedProject.id)
      toast.success(t('projectCard.chapters.generateAllStarted'))
      fetchProjects()
      fetchSegments()
    } catch (e: any) {
      setIsPolling(false)
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleProcessAll = async () => {
    if (!selectedProject || !detail) return
    setLoadingAction(true)
    const ready = detail.chapters.filter(c => c.status === 'ready')
    if (ready.length > 0) {
      setGeneratingChapterIndices(prev => new Set([...prev, ...ready.map(c => c.chapter_index)]))
    }
    setIsPolling(true)
    try {
      await audiobookApi.processAll(selectedProject.id)
      toast.success(t('projectCard.chapters.processAllStarted'))
      fetchProjects()
      fetchDetail()
      fetchSegments()
    } catch (e: any) {
      setIsPolling(false)
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleContinueScript = async (additionalChapters: number) => {
    if (!selectedProject) return
    setLoadingAction(true)
    setIsPolling(true)
    try {
      await audiobookApi.continueScript(selectedProject.id, additionalChapters)
      toast.success(t('projectCard.chapters.continueScriptStarted'))
      setShowContinueScript(false)
      fetchProjects()
      fetchDetail()
    } catch (e: any) {
      setIsPolling(false)
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleCancelBatch = async () => {
    if (!selectedProject) return
    try {
      await audiobookApi.cancelBatch(selectedProject.id)
      toast.success(t('projectCard.cancelledToast'))
      setIsPolling(false)
      setGeneratingChapterIndices(new Set())
      setTimeout(() => { fetchProjects(); fetchDetail(); fetchSegments() }, 1000)
    } catch (e: any) {
      toast.error(formatApiError(e))
    }
  }

  const handleUpdateSegment = async (segmentId: number, data: { text: string; emo_text?: string | null; emo_alpha?: number | null }) => {
    if (!selectedProject) return
    try {
      await audiobookApi.updateSegment(selectedProject.id, segmentId, data)
      toast.success(t('projectCard.segments.savedSuccess'))
      fetchSegments()
    } catch (e: any) {
      toast.error(formatApiError(e))
      throw e
    }
  }

  const handleRegenerateSegment = async (segmentId: number) => {
    if (!selectedProject) return
    try {
      await audiobookApi.regenerateSegment(selectedProject.id, segmentId)
      fetchSegments()
    } catch (e: any) {
      toast.error(formatApiError(e))
      throw e
    }
  }

  const handleDownload = async (chapterIndex?: number) => {
    if (!selectedProject) return
    setLoadingAction(true)
    try {
      const response = await apiClient.get(`/audiobook/projects/${selectedProject.id}/download`, {
        responseType: 'blob',
        params: chapterIndex !== undefined ? { chapter: chapterIndex } : {},
      })
      const url = URL.createObjectURL(response.data)
      const a = document.createElement('a')
      a.href = url
      a.download = chapterIndex !== undefined
        ? `${selectedProject.title}_ch${chapterIndex + 1}.mp3`
        : `${selectedProject.title}.mp3`
      a.click()
      URL.revokeObjectURL(url)
    } catch (e: any) {
      toast.error(formatApiError(e))
    } finally {
      setLoadingAction(false)
    }
  }

  const handleDelete = async () => {
    if (!selectedProject) return
    if (!confirm(t('projectCard.deleteConfirm', { title: selectedProject.title }))) return
    try {
      await audiobookApi.deleteProject(selectedProject.id)
      toast.success(t('projectCard.deleteSuccess'))
      setSelectedProjectId(null)
      setDetail(null)
      setSegments([])
      fetchProjects()
    } catch (e: any) {
      toast.error(formatApiError(e))
    }
  }

  const doneCount = segments.filter(s => s.status === 'done').length
  const totalCount = segments.length
  const progress = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0
  const chaptersParsed = detail?.chapters.filter(c => ['ready', 'done'].includes(c.status) || (segments.some(s => s.chapter_index === c.chapter_index))).length ?? 0
  const chaptersParsing = detail?.chapters.filter(c => c.status === 'parsing').length ?? 0
  const chaptersError = detail?.chapters.filter(c => c.status === 'error').length ?? 0
  const chaptersTotal = detail?.chapters.length ?? 0
  const chapterProgress = chaptersTotal > 0 ? Math.round((chaptersParsed / chaptersTotal) * 100) : 0
  const hasGenerating = segments.some(s => s.status === 'generating')

  const displayStatus = (() => {
    if (!selectedProject) return ''
    const s = selectedProject.status
    if (['ready', 'generating', 'done'].includes(s)) {
      if (chaptersParsing > 0 && hasGenerating) return 'processing'
      if (chaptersParsing > 0) return 'parsing'
      if (hasGenerating) return 'generating'
      if (totalCount > 0 && doneCount === totalCount && chaptersTotal > 0 && chaptersParsing === 0 && chaptersError === 0) return 'done'
      if (s === 'done' && (chaptersError > 0 || (chaptersTotal > 0 && chaptersParsed < chaptersTotal))) return 'ready'
    }
    return s
  })()

  const isTurboMode = ['analyzing', 'parsing', 'processing'].includes(displayStatus)

  return (
    <div className="h-screen overflow-hidden flex bg-background">
      <ProjectListSidebar
        projects={projects}
        selectedId={selectedProjectId}
        onSelect={(id) => {
          if (id !== selectedProjectId) {
            setSelectedProjectId(id)
            setIsPolling(false)
            setGeneratingChapterIndices(new Set())
          }
        }}
        onNew={() => { setShowCreate(v => !v); setShowLLM(false); setShowAIScript(false); setShowNSFWScript(false) }}
        onAIScript={() => { setShowAIScript(v => !v); setShowCreate(false); setShowLLM(false); setShowNSFWScript(false) }}
        onNSFWScript={() => { setShowNSFWScript(v => !v); setShowCreate(false); setShowLLM(false); setShowAIScript(false) }}
        onLLM={() => { setShowLLM(v => !v); setShowCreate(false); setShowAIScript(false); setShowNSFWScript(false) }}
        loading={loading}
        collapsed={!sidebarOpen}
        onToggle={() => setSidebarOpen(v => !v)}
        isSuperuser={user?.is_superuser}
        hasNsfwAccess={hasNsfwAccess}
      />
      <div className="flex-1 flex flex-col overflow-hidden bg-muted/30">
        <Navbar />
        <div className="flex-1 flex flex-col overflow-hidden bg-background rounded-tl-2xl">
          <LLMConfigDialog open={showLLM} onClose={() => setShowLLM(false)} />
          <CreateProjectDialog open={showCreate} onClose={() => setShowCreate(false)} onCreated={fetchProjects} />
          <AIScriptDialog open={showAIScript} onClose={() => setShowAIScript(false)} onCreated={() => { fetchProjects(); setShowAIScript(false) }} />
          <NSFWScriptDialog open={showNSFWScript} onClose={() => setShowNSFWScript(false)} onCreated={() => { fetchProjects(); setShowNSFWScript(false) }} />
          <ContinueScriptDialog open={showContinueScript} onClose={() => setShowContinueScript(false)} onConfirm={handleContinueScript} />
          {!selectedProject ? (
            <EmptyState />
          ) : (
            <>
              <div className="shrink-0 border-b px-4 py-2 flex items-center justify-between gap-2">
                <div className="flex items-center gap-2 min-w-0 flex-1">
                  {(() => {
                    const isNsfw = selectedProject.source_type === 'ai_generated' && !!(selectedProject.script_config as any)?.nsfw_mode
                    const Icon = selectedProject.source_type === 'epub' ? BookOpen : isNsfw ? NsfwIcon : selectedProject.source_type === 'ai_generated' ? Zap : Book
                    const cls = isNsfw ? 'h-4 w-4 shrink-0 text-red-500' : 'h-4 w-4 shrink-0 text-muted-foreground'
                    return <Icon className={cls} />
                  })()}
                  <span className="font-medium break-words min-w-0">{selectedProject.title}</span>
                  {(() => {
                    const sc = STATUS_CONFIG[displayStatus]
                    return sc ? <sc.Icon className={`${sc.iconCls} shrink-0`} /> : null
                  })()}
                </div>
                <div className="flex items-center gap-1 shrink-0 flex-wrap justify-end">
                  {isTurboMode && (
                    <Badge variant="secondary" className="bg-amber-500/10 text-amber-500 hover:bg-amber-500/20 shadow-sm border-none">
                      {t('status.turboActive')}
                    </Badge>
                  )}
                  {status === 'pending' && (
                    <Button size="sm" variant="ghost" onClick={handleAnalyze} disabled={loadingAction}>
                      {t('projectCard.analyze')}
                    </Button>
                  )}
                  {status === 'ready' && (
                    <Button size="sm" variant="ghost" onClick={handleProcessAll} disabled={loadingAction}>
                      {loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : null}
                      {t('projectCard.processAll')}
                    </Button>
                  )}
                  {status === 'done' && (
                    <Button size="sm" variant="ghost" onClick={() => handleDownload()} disabled={loadingAction}>
                      <Download className="h-3 w-3" />{t('projectCard.downloadAll')}
                    </Button>
                  )}
                  <Button size="icon" variant="ghost" className="h-8 w-8 shrink-0" onClick={handleDelete}>
                    <Trash2 className="h-4 w-4 text-destructive" />
                  </Button>
                </div>
              </div>


              {(status === 'analyzing' || (status === 'generating' && selectedProject.source_type === 'ai_generated')) && (
                <div className="shrink-0 mx-4 mt-2">
                  <LogStream projectId={selectedProject.id} active={true} />
                </div>
              )}

              {selectedProject.error_message && (
                <div className="shrink-0 mx-4 mt-2 text-xs text-destructive bg-destructive/10 rounded p-2">
                  {selectedProject.error_message}
                </div>
              )}

              {chaptersTotal > 0 && (isBackgroundGenerating || ['ready', 'generating', 'done'].includes(status)) && (isBackgroundGenerating || chaptersParsing > 0 || chaptersError > 0 || (totalCount > 0 && doneCount > 0)) && (
                <div className="shrink-0 mx-4 mt-2 space-y-2">
                  {(isBackgroundGenerating || chaptersParsing > 0 || chaptersError > 0 || chaptersParsed < chaptersTotal) && (
                    <div className="space-y-1">
                      <div className="text-xs text-muted-foreground flex items-center justify-between">
                        <div className="flex items-center gap-1 flex-wrap">
                          {isBackgroundGenerating && chaptersParsing === 0 && !chaptersError
                            ? <Loader2 className="h-3 w-3 animate-spin" />
                            : <span>📝</span>}
                          <span>{t('projectCard.chaptersProgress', { parsed: chaptersParsed, total: chaptersTotal })}</span>
                          {chaptersParsing > 0 && (
                            <span className="text-primary">({t('projectCard.chaptersParsing', { count: chaptersParsing })})</span>
                          )}
                          {isBackgroundGenerating && hasPendingChapters && chaptersParsing > 0 && (
                            <span className="text-muted-foreground">({detail!.chapters.filter(c => c.status === 'pending').length} 排队中)</span>
                          )}
                          {chaptersError > 0 && (
                            <>
                              <span className="text-destructive">({t('projectCard.chaptersError', { count: chaptersError })})</span>
                              <Button size="xs" variant="outline" className="text-destructive border-destructive/40" onClick={handleRetryFailed}>
                                {t('projectCard.retryFailed')}
                              </Button>
                            </>
                          )}
                        </div>
                        {(isBackgroundGenerating || chaptersParsing > 0) && (
                          <Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
                            {t('projectCard.cancelParsing')}
                          </Button>
                        )}
                      </div>
                      <Progress value={chapterProgress} />
                    </div>
                  )}
                  {totalCount > 0 && doneCount > 0 && (
                    <div className="space-y-1">
                      <div className="text-xs text-muted-foreground flex items-center justify-between">
                        <div className="flex items-center gap-1">
                          <span>🎵</span>
                          <span>{t('projectCard.segmentsProgress', { done: doneCount, total: totalCount })}</span>
                        </div>
                        {!chaptersParsing && hasGenerating && (
                          <Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
                            {t('projectCard.cancelGenerating')}
                          </Button>
                        )}
                      </div>
                      <Progress value={progress} />
                    </div>
                  )}
                  {chaptersParsing > 0 && !totalCount && (
                    <div className="flex justify-end">
                      <Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
                        {t('projectCard.cancelParsing')}
                      </Button>
                    </div>
                  )}
                </div>
              )}

              {detailLoading ? (
                <div className="flex-1 flex overflow-hidden mt-2 px-2 pb-2 gap-2">
                  <div className="w-56 shrink-0 flex flex-col gap-2">
                    {[...Array(4)].map((_, i) => (
                      <div key={i} className="h-14 rounded-lg bg-muted/60 animate-pulse" style={{ animationDelay: `${i * 80}ms` }} />
                    ))}
                  </div>
                  <div className="flex-1 flex flex-col gap-3">
                    {[...Array(5)].map((_, i) => (
                      <div key={i} className="h-12 rounded-lg bg-muted/60 animate-pulse" style={{ animationDelay: `${i * 60}ms` }} />
                    ))}
                  </div>
                </div>
              ) : (
              <div className="flex-1 flex overflow-hidden mt-2 px-2 pb-2 gap-2">
                <CharactersPanel
                  project={selectedProject}
                  detail={detail}
                  loadingAction={loadingAction}
                  onConfirm={handleConfirm}
                  onAnalyze={handleAnalyze}
                  onFetchDetail={fetchDetail}
                  collapsed={charactersCollapsed}
                  onToggle={() => setCharactersCollapsed(v => !v)}
                  onScrollToChapter={(id) => setScrollToChapterId(id)}
                />
                <ChaptersPanel
                  key={selectedProject.id}
                  project={selectedProject}
                  detail={detail}
                  segments={segments}
                  loadingAction={loadingAction}
                  generatingChapterIndices={generatingChapterIndices}
                  sequentialPlayingId={sequentialPlayingId}
                  onParseChapter={handleParseChapter}
                  onGenerate={handleGenerate}
                  onParseAll={handleParseAll}
                  onGenerateAll={handleGenerateAll}
                  onProcessAll={handleProcessAll}
                  isBackgroundGenerating={isBackgroundGenerating}
                  onContinueScript={selectedProject.source_type === 'ai_generated' ? () => setShowContinueScript(true) : undefined}
                  onDownload={handleDownload}
                  onSequentialPlayingChange={setSequentialPlayingId}
                  onUpdateSegment={handleUpdateSegment}
                  onRegenerateSegment={handleRegenerateSegment}
                  scrollToChapterId={scrollToChapterId}
                  onScrollToChapterDone={() => setScrollToChapterId(null)}
                />
              </div>
              )}
            </>
          )}
        </div>
      </div>
    </div>
  )
}
