import React, { useState, useEffect, useRef } from 'react';
import { UploadCloud, Film, Settings2, Plus, Loader2, Activity, Images, Trash2, Pencil, Check, X, Copy } from 'lucide-react';
import { cn } from '../lib/utils';
import { useStore } from '../store/useStore';
import { getProjects, createProject, updateProject, copyProject, uploadMedia, parseMedia, uploadDicomBatch, deleteProject, getTask } from '../lib/api';
import type { UploadProgress } from '../lib/api';
import type { Project } from '../store/useStore';
import { TransientNotice, type NoticeState, type NoticeTone } from './TransientNotice';

const naturalFilenameCompare = (left: File, right: File) => left.name.localeCompare(
  right.name,
  undefined,
  { numeric: true, sensitivity: 'base' },
);

interface ProjectLibraryProps {
  onProjectSelect: () => void;
}

interface ImportProgressState {
  kind: 'video' | 'dicom';
  phase: 'preparing' | 'uploading' | 'queueing' | 'parsing' | 'done' | 'error';
  title: string;
  detail: string;
  percent?: number;
  fileCount?: number;
}

export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
  const projects = useStore((state) => state.projects);
  const setProjects = useStore((state) => state.setProjects);
  const currentProject = useStore((state) => state.currentProject);
  const setCurrentProject = useStore((state) => state.setCurrentProject);
  const addProject = useStore((state) => state.addProject);
  const setFrames = useStore((state) => state.setFrames);
  const setMasks = useStore((state) => state.setMasks);
  const setSelectedMaskIds = useStore((state) => state.setSelectedMaskIds);
  const [isLoading, setIsLoading] = useState(false);
  const [isCreating, setIsCreating] = useState(false);
  const [showModal, setShowModal] = useState(false);
  const [newName, setNewName] = useState('');
  const [newDesc, setNewDesc] = useState('');
  const [showImportMenu, setShowImportMenu] = useState(false);
  const [showVideoConfig, setShowVideoConfig] = useState(false);
  const [pendingFile, setPendingFile] = useState<File | null>(null);
  const [frameProject, setFrameProject] = useState<Project | null>(null);
  const [showFrameConfig, setShowFrameConfig] = useState(false);
  const [frameParseFps, setFrameParseFps] = useState(30);
  const [isGeneratingFrames, setIsGeneratingFrames] = useState(false);
  const [deletingProjectId, setDeletingProjectId] = useState<string | null>(null);
  const [deleteProjectTarget, setDeleteProjectTarget] = useState<Project | null>(null);
  const [copyingProjectId, setCopyingProjectId] = useState<string | null>(null);
  const [copyProjectTarget, setCopyProjectTarget] = useState<Project | null>(null);
  const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
  const [editingProjectName, setEditingProjectName] = useState('');
  const [renamingProjectId, setRenamingProjectId] = useState<string | null>(null);
  const [notice, setNotice] = useState<NoticeState | null>(null);
  const [importProgress, setImportProgress] = useState<ImportProgressState | null>(null);
  const videoInputRef = useRef<HTMLInputElement>(null);
  const dicomInputRef = useRef<HTMLInputElement>(null);

  const showNotice = (message: string, tone: NoticeTone = 'info') => {
    setNotice({ id: Date.now(), message, tone });
  };

  const formatUploadBytes = (value: number) => {
    if (!Number.isFinite(value) || value <= 0) return '0 B';
    const units = ['B', 'KB', 'MB', 'GB'];
    const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
    const amount = value / (1024 ** index);
    return `${amount >= 10 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`;
  };

  const scheduleProgressDismiss = () => {
    window.setTimeout(() => setImportProgress((current) => (
      current?.phase === 'done' ? null : current
    )), 1400);
  };

  const uploadProgressDetail = (progress: UploadProgress, fallback: string) => {
    if (progress.total) {
      return `${formatUploadBytes(progress.loaded)} / ${formatUploadBytes(progress.total)}`;
    }
    return `${fallback}，已上传 ${formatUploadBytes(progress.loaded)}`;
  };

  const waitForTaskDone = async (
    taskId: string | number,
    onProgress: (progress: { progress?: number; message?: string | null; status?: string }) => void,
  ) => {
    for (;;) {
      await new Promise((resolve) => window.setTimeout(resolve, 1200));
      const task = await getTask(taskId);
      onProgress(task);
      if (['success', 'failed', 'cancelled'].includes(task.status)) return task;
    }
  };

  const refreshProjects = async () => {
    const data = await getProjects();
    setProjects(data);
    if (currentProject?.id) {
      const refreshedCurrentProject = data.find((project) => project.id === currentProject.id);
      if (refreshedCurrentProject) {
        setCurrentProject(refreshedCurrentProject);
      }
    }
    return data;
  };

  const ImportProgressPanel = () => {
    if (!importProgress) return null;
    const percent = typeof importProgress.percent === 'number'
      ? Math.min(100, Math.max(0, importProgress.percent))
      : undefined;
    const toneClass = importProgress.phase === 'error'
      ? 'border-red-500/25 bg-red-950/20'
      : importProgress.kind === 'dicom'
        ? 'border-emerald-500/25 bg-emerald-950/15'
        : 'border-cyan-500/25 bg-cyan-950/15';
    const barClass = importProgress.phase === 'error'
      ? 'bg-red-400'
      : importProgress.kind === 'dicom'
        ? 'bg-emerald-400'
        : 'bg-cyan-400';

    return (
      <div
        aria-live="polite"
        aria-label="导入进度"
        className={cn('mb-6 rounded-lg border px-4 py-3 shadow-lg shadow-black/20', toneClass)}
      >
        <div className="flex items-start justify-between gap-4">
          <div className="min-w-0">
            <div className="flex items-center gap-2 text-sm font-medium text-gray-100">
              {importProgress.phase === 'done' ? (
                <Check size={16} className="text-emerald-300" />
              ) : importProgress.phase === 'error' ? (
                <X size={16} className="text-red-300" />
              ) : (
                <Loader2 size={16} className="animate-spin text-cyan-300" />
              )}
              <span>{importProgress.title}</span>
              {importProgress.fileCount && (
                <span className="rounded border border-white/10 bg-black/20 px-2 py-0.5 text-[10px] font-mono text-gray-400">
                  {importProgress.fileCount} 文件
                </span>
              )}
            </div>
            <div className="mt-1 truncate text-xs text-gray-400">{importProgress.detail}</div>
          </div>
          {percent !== undefined && (
            <div className="shrink-0 font-mono text-sm text-gray-200">{percent}%</div>
          )}
        </div>
        <div
          role="progressbar"
          aria-label="导入进度"
          aria-valuemin={0}
          aria-valuemax={100}
          aria-valuenow={percent}
          className="mt-3 h-2 overflow-hidden rounded-full bg-black/35"
        >
          <div
            className={cn(
              'h-full rounded-full transition-all duration-200',
              barClass,
              percent === undefined && 'w-1/3 animate-pulse',
            )}
            style={percent !== undefined ? { width: `${percent}%` } : undefined}
          />
        </div>
      </div>
    );
  };

  const frameSequenceLabel = (project: Project) => {
    if (project.source_type === 'dicom') return 'DICOM';
    if (project.video_path && (project.frames ?? 0) === 0 && project.status !== 'parsing') return '待生成帧';
    if (project.parse_fps && project.parse_fps > 0) {
      const rounded = Math.round(project.parse_fps * 10) / 10;
      return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1)}FPS`;
    }
    return project.fps || '30FPS';
  };

  const canGenerateFrames = (project: Project) => (
    project.source_type !== 'dicom'
    && Boolean(project.video_path)
    && (project.frames ?? 0) === 0
    && project.status !== 'parsing'
    && editingProjectId !== project.id
  );

  useEffect(() => {
    setIsLoading(true);
    getProjects()
      .then((data) => setProjects(data))
      .catch(console.error)
      .finally(() => setIsLoading(false));
  }, [setProjects]);

  const handleCreate = async () => {
    if (!newName.trim()) return;
    setIsCreating(true);
    try {
      const project = await createProject({ name: newName.trim(), description: newDesc.trim() || undefined });
      addProject(project);
      setShowModal(false);
      setNewName('');
      setNewDesc('');
    } catch (err) {
      console.error('Failed to create project:', err);
    } finally {
      setIsCreating(false);
    }
  };

  const handleSelect = (project: Project) => {
    setCurrentProject(project);
    onProjectSelect();
  };

  const openDeleteProject = (project: Project, event: React.MouseEvent) => {
    event.stopPropagation();
    if (deletingProjectId) return;
    setDeleteProjectTarget(project);
  };

  const handleDeleteProject = async () => {
    const project = deleteProjectTarget;
    if (!project || deletingProjectId) return;
    setDeletingProjectId(project.id);
    try {
      await deleteProject(project.id);
      setProjects(projects.filter((item) => item.id !== project.id));
      if (currentProject?.id === project.id) {
        setCurrentProject(null);
        setFrames([]);
        setMasks([]);
        setSelectedMaskIds([]);
      }
      setDeleteProjectTarget(null);
    } catch (err) {
      console.error('Delete project failed:', err);
      showNotice('删除项目失败，请检查后端服务', 'error');
    } finally {
      setDeletingProjectId(null);
    }
  };

  const openCopyProject = (project: Project, event: React.MouseEvent) => {
    event.stopPropagation();
    setCopyProjectTarget(project);
  };

  const handleCopyProject = async (mode: 'reset' | 'full') => {
    if (!copyProjectTarget || copyingProjectId) return;
    setCopyingProjectId(copyProjectTarget.id);
    try {
      const copied = await copyProject(copyProjectTarget.id, { mode });
      const data = await getProjects();
      setProjects(data);
      setCopyProjectTarget(null);
      showNotice(mode === 'full'
        ? `已全内容复制项目：${copied.name}`
        : `已复制为重置项目：${copied.name}`, 'success');
    } catch (err) {
      console.error('Copy project failed:', err);
      showNotice('复制项目失败，请检查后端服务', 'error');
    } finally {
      setCopyingProjectId(null);
    }
  };

  const beginRenameProject = (project: Project, event: React.MouseEvent) => {
    event.stopPropagation();
    setEditingProjectId(project.id);
    setEditingProjectName(project.name);
  };

  const cancelRenameProject = (event: React.MouseEvent) => {
    event.stopPropagation();
    setEditingProjectId(null);
    setEditingProjectName('');
  };

  const commitRenameProject = async (project: Project, event?: React.SyntheticEvent) => {
    event?.preventDefault();
    event?.stopPropagation();
    const nextName = editingProjectName.trim();
    if (!nextName) {
      showNotice('项目名称不能为空', 'error');
      return;
    }
    if (nextName === project.name) {
      setEditingProjectId(null);
      setEditingProjectName('');
      return;
    }
    setRenamingProjectId(project.id);
    try {
      const updated = await updateProject(project.id, { name: nextName });
      setProjects(projects.map((item) => (item.id === updated.id ? updated : item)));
      if (currentProject?.id === updated.id) {
        setCurrentProject(updated);
      }
      setEditingProjectId(null);
      setEditingProjectName('');
      showNotice('项目名称已更新', 'success');
    } catch (err) {
      console.error('Rename project failed:', err);
      showNotice('项目名称修改失败，请检查后端服务', 'error');
    } finally {
      setRenamingProjectId(null);
    }
  };

  const handleVideoSelect = (file: File) => {
    setPendingFile(file);
    setShowVideoConfig(true);
  };

  const handleVideoUpload = async () => {
    if (!pendingFile) return;
    setShowVideoConfig(false);
    setIsLoading(true);
    setImportProgress({
      kind: 'video',
      phase: 'preparing',
      title: '正在准备视频导入',
      detail: `创建项目：${pendingFile.name}`,
      percent: 2,
    });
    try {
      const newProject = await createProject({
        name: pendingFile.name,
        description: `导入于 ${new Date().toLocaleString()}`,
      });
      setImportProgress({
        kind: 'video',
        phase: 'uploading',
        title: '正在上传视频文件',
        detail: pendingFile.name,
        percent: 5,
      });
      const result = await uploadMedia(pendingFile, String(newProject.id), {
        onProgress: (progress) => setImportProgress({
          kind: 'video',
          phase: 'uploading',
          title: '正在上传视频文件',
          detail: uploadProgressDetail(progress, pendingFile.name),
          percent: progress.percent,
        }),
      });
      setImportProgress({
        kind: 'video',
        phase: 'done',
        title: '视频导入完成',
        detail: pendingFile.name,
        percent: 100,
      });
      showNotice(`视频导入成功: ${pendingFile.name}\n已保存至: ${result.url}\n需要生成帧时，请在项目卡片点击“生成帧”。`, 'success');
      const data = await getProjects();
      setProjects(data);
      scheduleProgressDismiss();
    } catch (err) {
      console.error('Upload failed:', err);
      setImportProgress({
        kind: 'video',
        phase: 'error',
        title: '视频导入失败',
        detail: pendingFile.name,
        percent: 100,
      });
      showNotice('上传失败，请检查后端服务', 'error');
    } finally {
      setIsLoading(false);
      setPendingFile(null);
      if (videoInputRef.current) videoInputRef.current.value = '';
    }
  };

  const openFrameConfig = (project: Project, event: React.MouseEvent) => {
    event.stopPropagation();
    setFrameProject(project);
    setFrameParseFps(Math.round(project.parse_fps || 30));
    setShowFrameConfig(true);
  };

  const handleGenerateFrames = async () => {
    if (!frameProject?.id) return;
    const targetProject = frameProject;
    setIsGeneratingFrames(true);
    try {
      const task = await parseMedia(targetProject.id, { parseFps: frameParseFps });
      showNotice(`生成帧任务已入队 #${task.id}\n帧率: ${frameParseFps} FPS\n可在 Dashboard 查看进度。`, 'success');
      await refreshProjects();
      setShowFrameConfig(false);
      setFrameProject(null);
      if (task.id) {
        setImportProgress({
          kind: 'video',
          phase: 'parsing',
          title: '正在生成视频帧',
          detail: task.message || `项目：${targetProject.name}`,
          percent: task.progress ?? 0,
        });
        void waitForTaskDone(task.id, (progress) => {
          setImportProgress({
            kind: 'video',
            phase: 'parsing',
            title: '正在生成视频帧',
            detail: progress.message || `任务状态: ${progress.status || 'running'}`,
            percent: progress.progress,
          });
        }).then(async (completed) => {
          if (completed.status === 'failed') {
            setImportProgress({
              kind: 'video',
              phase: 'error',
              title: '视频帧生成失败',
              detail: completed.error || completed.message || targetProject.name,
              percent: 100,
            });
            showNotice('视频帧生成失败，请检查后端服务或任务日志', 'error');
            return;
          }
          if (completed.status === 'cancelled') {
            setImportProgress({
              kind: 'video',
              phase: 'error',
              title: '视频帧生成已取消',
              detail: targetProject.name,
              percent: 100,
            });
            showNotice('视频帧生成任务已取消', 'error');
            return;
          }
          await refreshProjects();
          setImportProgress({
            kind: 'video',
            phase: 'done',
            title: '视频帧生成完成',
            detail: '项目封面已自动更新',
            percent: 100,
          });
          showNotice('视频帧生成完成，项目封面已自动更新', 'success');
          scheduleProgressDismiss();
        }).catch((err) => {
          console.error('Frame generation polling failed:', err);
          setImportProgress({
            kind: 'video',
            phase: 'error',
            title: '视频帧生成进度同步失败',
            detail: targetProject.name,
            percent: 100,
          });
          showNotice('视频帧生成进度同步失败，请刷新项目库重试', 'error');
        });
      }
    } catch (err) {
      console.error('Frame generation failed:', err);
      showNotice('生成帧失败，请检查后端服务或项目源文件', 'error');
    } finally {
      setIsGeneratingFrames(false);
    }
  };

  const handleDicomUpload = async (files: FileList | null) => {
    if (!files || files.length === 0) return;
    const dcmFiles = Array.from(files)
      .filter((f) => f.name.toLowerCase().endsWith('.dcm'))
      .sort(naturalFilenameCompare);
    if (dcmFiles.length === 0) {
      showNotice('未选择有效的 .dcm 文件', 'error');
      return;
    }
    setIsLoading(true);
    setImportProgress({
      kind: 'dicom',
      phase: 'uploading',
      title: '正在上传 DICOM 序列',
      detail: `${dcmFiles.length} 个文件，按文件名自然顺序上传`,
      percent: 0,
      fileCount: dcmFiles.length,
    });
    try {
      const result = await uploadDicomBatch(dcmFiles, undefined, {
        onProgress: (progress) => setImportProgress({
          kind: 'dicom',
          phase: 'uploading',
          title: '正在上传 DICOM 序列',
          detail: uploadProgressDetail(progress, `${dcmFiles.length} 个文件`),
          percent: progress.percent,
          fileCount: dcmFiles.length,
        }),
      });
      setImportProgress({
        kind: 'dicom',
        phase: 'queueing',
        title: 'DICOM 上传完成，正在创建解析任务',
        detail: `${result.uploaded_count} 个文件已上传`,
        percent: 92,
        fileCount: result.uploaded_count,
      });
      const task = await parseMedia(String(result.project_id));
      setImportProgress({
        kind: 'dicom',
        phase: 'parsing',
        title: '正在解析 DICOM 序列',
        detail: task.message || '解析任务已入队',
        percent: task.progress ?? 0,
        fileCount: result.uploaded_count,
      });
      if (task.id) {
        const completed = await waitForTaskDone(task.id, (progress) => {
          setImportProgress({
            kind: 'dicom',
            phase: 'parsing',
            title: '正在解析 DICOM 序列',
            detail: progress.message || `任务状态: ${progress.status || 'running'}`,
            percent: progress.progress,
            fileCount: result.uploaded_count,
          });
        });
        if (completed.status === 'failed') {
          throw new Error(completed.error || completed.message || 'DICOM 解析失败');
        }
        if (completed.status === 'cancelled') {
          throw new Error('DICOM 解析任务已取消');
        }
      }
      setImportProgress({
        kind: 'dicom',
        phase: 'done',
        title: 'DICOM 导入完成',
        detail: `${result.uploaded_count} 个文件已上传并完成解析`,
        percent: 100,
        fileCount: result.uploaded_count,
      });
      showNotice(`DICOM 导入完成: ${result.uploaded_count} 个文件`, 'success');
      const data = await getProjects();
      setProjects(data);
      scheduleProgressDismiss();
    } catch (err) {
      console.error('DICOM upload failed:', err);
      setImportProgress({
        kind: 'dicom',
        phase: 'error',
        title: 'DICOM 导入失败',
        detail: `${dcmFiles.length} 个文件`,
        percent: 100,
        fileCount: dcmFiles.length,
      });
      showNotice('DICOM 上传失败，请检查后端服务', 'error');
    } finally {
      setIsLoading(false);
      if (dicomInputRef.current) dicomInputRef.current.value = '';
    }
  };

  const SkeletonCard = () => (
    <div className="group flex flex-col bg-[#111] border border-white/5 rounded-xl overflow-hidden animate-pulse">
      <div className="w-full aspect-[16/9] bg-[#1a1a1a]" />
      <div className="p-4 flex flex-col gap-2">
        <div className="h-4 bg-[#1a1a1a] rounded w-3/4" />
        <div className="h-3 bg-[#1a1a1a] rounded w-1/2 mt-2" />
      </div>
    </div>
  );

  return (
    <div className="p-8 w-full h-full overflow-y-auto bg-[#0a0a0a]">
      <TransientNotice notice={notice} onDismiss={() => setNotice(null)} />
      <div className="flex justify-between items-end mb-8 border-b border-white/5 pb-6">
        <div>
          <h1 className="text-3xl font-medium tracking-tight text-white mb-2">视频与连续帧项目库</h1>
          <p className="text-gray-400 text-sm">支持导入视频文件、DICOM序列文件</p>
        </div>
        <div className="flex items-center gap-3">
          <button
            onClick={() => setShowModal(true)}
            className="flex items-center gap-2 bg-white/5 hover:bg-white/10 border border-white/10 text-gray-200 px-5 py-2.5 rounded-lg font-medium text-sm transition-colors"
          >
            <Plus size={18} />
            <span>新建项目</span>
          </button>
          <div className="relative">
            <button
              onClick={() => setShowImportMenu(!showImportMenu)}
              className="flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 text-white px-5 py-2.5 rounded-lg font-medium text-sm transition-colors border border-cyan-500 shadow-lg shadow-cyan-900/20"
            >
              <UploadCloud size={18} />
              <span>导入多媒体资源</span>
            </button>
            {showImportMenu && (
              <div className="absolute right-0 top-full mt-2 w-56 bg-[#111] border border-white/10 rounded-lg shadow-2xl z-50 overflow-hidden">
                <button
                  className="w-full text-left px-4 py-3 text-sm text-gray-200 hover:bg-white/5 flex items-center gap-3 transition-colors"
                  onClick={() => { setShowImportMenu(false); videoInputRef.current?.click(); }}
                >
                  <Film size={16} className="text-cyan-400" />
                  导入视频文件
                </button>
                <button
                  className="w-full text-left px-4 py-3 text-sm text-gray-200 hover:bg-white/5 flex items-center gap-3 transition-colors border-t border-white/5"
                  onClick={() => { setShowImportMenu(false); dicomInputRef.current?.click(); }}
                >
                  <Activity size={16} className="text-emerald-400" />
                  导入 DICOM 序列
                </button>
              </div>
            )}
          </div>
          <input
            type="file"
            ref={videoInputRef}
            className="hidden"
            accept="video/*"
            onChange={(e) => {
              const file = e.target.files?.[0];
              if (file) handleVideoSelect(file);
            }}
          />
          <input
            type="file"
            ref={dicomInputRef}
            className="hidden"
            accept=".dcm"
            multiple
            onChange={(e) => handleDicomUpload(e.target.files)}
          />
        </div>
      </div>

      <ImportProgressPanel />

      {isLoading && projects.length === 0 ? (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
          {Array.from({ length: 8 }).map((_, i) => (
            <SkeletonCard key={i} />
          ))}
        </div>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
          {projects.map((proj) => (
            <div
              key={proj.id}
              className="group flex flex-col bg-[#111] border border-white/5 rounded-xl overflow-hidden cursor-pointer hover:border-cyan-500/50 transition-all hover:shadow-[0_0_20px_rgba(6,182,212,0.15)]"
              onClick={() => handleSelect(proj)}
            >
              <div className={cn("w-full aspect-[16/9] relative flex items-center justify-center overflow-hidden bg-[#0d0d0d]")}>
                {proj.thumbnail_url ? (
                  <img
                    src={proj.thumbnail_url}
                    alt={proj.name}
                    className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
                    loading="lazy"
                  />
                ) : (
                  <Film className="w-12 h-12 text-[#2a2a2a] group-hover:text-[#333] transition-colors" />
                )}
                <div className="absolute top-2 right-2 flex gap-2">
                  <span className="backdrop-blur-md bg-black/40 text-gray-200 text-[10px] font-mono px-2 py-1 rounded border border-white/10 uppercase tracking-widest">
                    {frameSequenceLabel(proj)}
                  </span>
                  <span className="backdrop-blur-md bg-black/40 text-gray-200 text-[10px] px-2 py-1 rounded border border-white/10 flex items-center gap-1 uppercase tracking-widest">
                    {proj.status === 'ready' ? (
                      <><div className="w-1.5 h-1.5 bg-emerald-500 rounded-full" /> 已就绪</>
                    ) : proj.status === 'parsing' ? (
                      <><div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" /> 解析拆帧中</>
                    ) : proj.status === 'error' ? (
                      <><div className="w-1.5 h-1.5 bg-red-500 rounded-full" /> 异常</>
                    ) : (
                      <><div className="w-1.5 h-1.5 bg-blue-500 rounded-full" /> 待处理</>
                    )}
                  </span>
                </div>
              </div>
              <div className="p-4 flex flex-col gap-1">
                <div className="flex justify-between items-start">
                  {editingProjectId === proj.id ? (
                    <form
                      className="flex min-w-0 flex-1 items-center gap-1 pr-2"
                      onClick={(event) => event.stopPropagation()}
                      onSubmit={(event) => void commitRenameProject(proj, event)}
                    >
                      <input
                        value={editingProjectName}
                        onChange={(event) => setEditingProjectName(event.target.value)}
                        autoFocus
                        className="min-w-0 flex-1 rounded border border-cyan-400/40 bg-black/30 px-2 py-1 text-sm text-gray-100 outline-none focus:border-cyan-300"
                      />
                      <button
                        type="button"
                        aria-label={`保存项目名称 ${proj.name}`}
                        title="保存名称"
                        disabled={renamingProjectId === proj.id}
                        onClick={(event) => void commitRenameProject(proj, event)}
                        className="text-cyan-300 hover:text-cyan-100 disabled:cursor-wait disabled:opacity-50"
                      >
                        {renamingProjectId === proj.id ? <Loader2 size={15} className="animate-spin" /> : <Check size={15} />}
                      </button>
                      <button
                        type="button"
                        aria-label={`取消修改项目名称 ${proj.name}`}
                        title="取消"
                        onClick={cancelRenameProject}
                        disabled={renamingProjectId === proj.id}
                        className="text-gray-500 hover:text-gray-200 disabled:opacity-50"
                      >
                        <X size={15} />
                      </button>
                    </form>
                  ) : (
                    <div className="flex min-w-0 flex-1 items-center gap-2 pr-2">
                      <h3 className="truncate text-sm font-medium text-gray-200" title={proj.name}>{proj.name}</h3>
                      <button
                        type="button"
                        aria-label={`修改项目名称 ${proj.name}`}
                        title="修改项目名称"
                        onClick={(event) => beginRenameProject(proj, event)}
                        className="shrink-0 text-gray-500 opacity-0 transition-colors hover:text-cyan-300 group-hover:opacity-100 focus:opacity-100"
                      >
                        <Pencil size={14} />
                      </button>
                    </div>
                  )}
                  <div className="flex shrink-0 items-center gap-2">
                    <button
                      type="button"
                      aria-label={`复制项目 ${proj.name}`}
                      title="复制项目"
                      disabled={copyingProjectId === proj.id || deletingProjectId === proj.id || renamingProjectId === proj.id}
                      onClick={(event) => openCopyProject(proj, event)}
                      className="text-gray-500 hover:text-emerald-400 disabled:opacity-50 disabled:cursor-wait transition-colors"
                    >
                      {copyingProjectId === proj.id ? <Loader2 size={16} className="animate-spin" /> : <Copy size={16} />}
                    </button>
                    <button
                      type="button"
                      aria-label={`删除项目 ${proj.name}`}
                      title="删除项目"
                      disabled={deletingProjectId === proj.id || renamingProjectId === proj.id}
                      onClick={(event) => openDeleteProject(proj, event)}
                      className="text-gray-500 hover:text-red-400 disabled:opacity-50 disabled:cursor-wait transition-colors"
                    >
                      {deletingProjectId === proj.id ? <Loader2 size={16} className="animate-spin" /> : <Trash2 size={16} />}
                    </button>
                  </div>
                </div>
                <div className="flex items-center gap-4 text-xs text-gray-500 font-mono mt-2">
                   <span className="flex items-center gap-1.5"><Settings2 size={12} /> {proj.frames ?? 0} 帧节点</span>
                   {proj.original_fps && (
                     <span className="flex items-center gap-1.5 text-cyan-400/80"><Activity size={12} /> 原 {proj.original_fps.toFixed(1)}fps</span>
                   )}
                </div>
                {canGenerateFrames(proj) && (
                  <button
                    onClick={(event) => openFrameConfig(proj, event)}
                    className="mt-3 inline-flex items-center justify-center gap-2 rounded-md border border-cyan-500/30 bg-cyan-500/10 px-3 py-2 text-xs font-medium text-cyan-200 hover:bg-cyan-500/20 transition-colors"
                  >
                    <Images size={14} />
                    生成帧
                  </button>
                )}
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Delete project confirmation */}
      {deleteProjectTarget && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
          <div
            className="w-full max-w-md rounded-2xl border border-red-500/20 bg-[#111] p-6 shadow-2xl"
            onClick={(event) => event.stopPropagation()}
          >
            <h2 className="text-lg font-semibold text-white">删除项目</h2>
            <p className="mt-3 text-sm leading-6 text-gray-400">
              确认删除项目“<span className="text-gray-100">{deleteProjectTarget.name}</span>”？
              该操作会删除项目帧、标注、任务记录和相关 mask 元数据，无法撤销。
            </p>
            <div className="mt-6 flex justify-end gap-3">
              <button
                type="button"
                onClick={() => setDeleteProjectTarget(null)}
                disabled={deletingProjectId === deleteProjectTarget.id}
                className="rounded-lg px-4 py-2 text-sm text-gray-400 transition-colors hover:text-white disabled:opacity-50"
              >
                取消
              </button>
              <button
                type="button"
                onClick={() => void handleDeleteProject()}
                disabled={deletingProjectId === deleteProjectTarget.id}
                className="inline-flex items-center gap-2 rounded-lg bg-red-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-400 disabled:cursor-wait disabled:opacity-60"
              >
                {deletingProjectId === deleteProjectTarget.id && <Loader2 size={14} className="animate-spin" />}
                确认删除
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Copy project modal */}
      {copyProjectTarget && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
          <div
            className="bg-[#111] border border-white/10 rounded-2xl p-6 w-full max-w-md shadow-2xl"
            onClick={(event) => event.stopPropagation()}
          >
            <h2 className="text-lg font-semibold text-white mb-2">复制项目</h2>
            <p className="text-sm text-gray-400 mb-5">
              {copyProjectTarget.name}
            </p>
            <div className="space-y-3">
              <button
                type="button"
                onClick={() => void handleCopyProject('reset')}
                disabled={copyingProjectId === copyProjectTarget.id}
                className="w-full rounded-lg border border-cyan-500/25 bg-cyan-500/10 px-4 py-3 text-left transition-colors hover:bg-cyan-500/20 disabled:cursor-wait disabled:opacity-60"
              >
                <div className="flex items-center justify-between gap-3">
                  <span className="text-sm font-medium text-cyan-100">新项目重置</span>
                  {copyingProjectId === copyProjectTarget.id && <Loader2 size={16} className="animate-spin text-cyan-200" />}
                </div>
                <p className="mt-1 text-xs leading-5 text-gray-500">复制项目媒体和已生成帧序列，清空标注与 mask 内容。</p>
              </button>
              <button
                type="button"
                onClick={() => void handleCopyProject('full')}
                disabled={copyingProjectId === copyProjectTarget.id}
                className="w-full rounded-lg border border-emerald-500/25 bg-emerald-500/10 px-4 py-3 text-left transition-colors hover:bg-emerald-500/20 disabled:cursor-wait disabled:opacity-60"
              >
                <div className="flex items-center justify-between gap-3">
                  <span className="text-sm font-medium text-emerald-100">全内容复制</span>
                  {copyingProjectId === copyProjectTarget.id && <Loader2 size={16} className="animate-spin text-emerald-200" />}
                </div>
                <p className="mt-1 text-xs leading-5 text-gray-500">复制项目、帧序列、标注和已关联 mask 元数据。</p>
              </button>
            </div>
            <div className="flex justify-end mt-5">
              <button
                type="button"
                onClick={() => setCopyProjectTarget(null)}
                disabled={copyingProjectId === copyProjectTarget.id}
                className="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white transition-colors disabled:opacity-50"
              >
                取消
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Video parse FPS config modal */}
      {showVideoConfig && pendingFile && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
          <div className="bg-[#111] border border-white/10 rounded-2xl p-6 w-full max-w-md shadow-2xl">
            <h2 className="text-lg font-semibold text-white mb-4">导入视频文件</h2>
            <div className="space-y-4">
              <div className="text-sm text-gray-400">文件: <span className="text-gray-200">{pendingFile.name}</span></div>
              <p className="text-xs leading-5 text-gray-500">此步骤只上传源视频并创建项目，不会立即拆帧。拆帧时再选择目标 FPS。</p>
            </div>
            <div className="flex justify-end gap-3 mt-6">
              <button
                onClick={() => { setShowVideoConfig(false); setPendingFile(null); }}
                className="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white transition-colors"
              >
                取消
              </button>
              <button
                onClick={handleVideoUpload}
                className="px-4 py-2 rounded-lg text-sm font-medium bg-cyan-500 hover:bg-cyan-400 text-black transition-all"
              >
                开始导入
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Frame generation FPS config modal */}
      {showFrameConfig && frameProject && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
          <div className="bg-[#111] border border-white/10 rounded-2xl p-6 w-full max-w-md shadow-2xl">
            <h2 className="text-lg font-semibold text-white mb-4">生成帧</h2>
            <div className="space-y-4">
              <div className="text-sm text-gray-400">项目: <span className="text-gray-200">{frameProject.name}</span></div>
              <div>
                <label className="block text-xs font-medium text-gray-400 uppercase tracking-widest mb-2">生成帧率 (FPS)</label>
                <div className="flex items-center gap-3">
                  <input
                    type="range"
                    min="1"
                    max="60"
                    value={frameParseFps}
                    onChange={(e) => setFrameParseFps(parseInt(e.target.value))}
                    className="flex-1 accent-cyan-500"
                  />
                  <span className="text-sm font-mono text-cyan-400 w-12 text-right">{frameParseFps}</span>
                </div>
                <p className="text-[10px] text-gray-600 mt-1">帧率越低，提取的帧越少，处理速度越快</p>
              </div>
            </div>
            <div className="flex justify-end gap-3 mt-6">
              <button
                onClick={() => { setShowFrameConfig(false); setFrameProject(null); }}
                disabled={isGeneratingFrames}
                className="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white transition-colors disabled:opacity-50"
              >
                取消
              </button>
              <button
                onClick={handleGenerateFrames}
                disabled={isGeneratingFrames}
                className="px-4 py-2 rounded-lg text-sm font-medium bg-cyan-500 hover:bg-cyan-400 text-black transition-all disabled:opacity-60"
              >
                {isGeneratingFrames ? '入队中...' : '开始生成帧'}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* New project modal */}
      {showModal && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
          <div className="bg-[#111] border border-white/10 rounded-2xl p-6 w-full max-w-md shadow-2xl">
            <h2 className="text-lg font-semibold text-white mb-4">新建项目</h2>
            <div className="space-y-4">
              <div>
                <label className="block text-xs font-medium text-gray-400 uppercase tracking-widest mb-2">项目名称</label>
                <input
                  type="text"
                  value={newName}
                  onChange={(e) => setNewName(e.target.value)}
                  className="w-full bg-[#1a1a1a] border border-white/10 rounded-lg px-4 py-3 text-sm focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/50 transition-all"
                  placeholder="输入项目名称"
                />
              </div>
              <div>
                <label className="block text-xs font-medium text-gray-400 uppercase tracking-widest mb-2">描述（可选）</label>
                <input
                  type="text"
                  value={newDesc}
                  onChange={(e) => setNewDesc(e.target.value)}
                  className="w-full bg-[#1a1a1a] border border-white/10 rounded-lg px-4 py-3 text-sm focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/50 transition-all"
                  placeholder="输入项目描述"
                />
              </div>
            </div>
            <div className="flex justify-end gap-3 mt-6">
              <button
                onClick={() => { setShowModal(false); setNewName(''); setNewDesc(''); }}
                className="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white transition-colors"
              >
                取消
              </button>
              <button
                onClick={handleCreate}
                disabled={isCreating || !newName.trim()}
                className={cn(
                  "px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-all",
                  isCreating || !newName.trim()
                    ? "bg-cyan-500/50 text-black/70 cursor-not-allowed"
                    : "bg-cyan-500 hover:bg-cyan-400 text-black"
                )}
              >
                {isCreating && <Loader2 size={14} className="animate-spin" />}
                创建
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
