/**
 * DialogTaskList Component
 *
 * Displays tasks that were created from the current dialog session.
 * Used within AI generation dialogs to show only tasks created in that dialog.
 * Supports pagination with scroll-to-load-more and type filtering via RPC.
 */

import React, { useMemo, useState, useCallback } from 'react';
import type { CSSProperties } from 'react';
import { useFilteredTaskQueue } from '../../hooks/useFilteredTaskQueue';
import {
  Task,
  TaskType,
  TaskStatus,
  isRetryableStatus,
} from '../../types/task.types';
import { useDrawnix, DialogType } from '../../hooks/use-drawnix';
import { MessagePlugin } from 'tdesign-react';
import {
  Download,
  Edit3,
  Eye,
  Image as ImageIcon,
  ImagePlus,
  MoreHorizontal,
  Music2,
  RefreshCcw,
  Scissors,
  Send,
  Trash2,
  Video as VideoIcon,
} from 'lucide-react';
import { normalizeImageDataUrl } from '@aitu/utils';
import { taskQueueService } from '../../services/task-queue';
import { taskStorageReader } from '../../services/task-storage-reader';
import { hasAIImageDraftContent } from '../../utils/ai-image-draft-state';
import {
  buildImageTaskPrefillInitialData,
  getImageTaskReferenceImages,
  type ImageGenerationReferenceImage,
} from '../../utils/image-task-prefill';
import {
  buildTaskDownloadItems,
  smartDownload,
} from '../../utils/download-utils';
import { CharacterCreateDialog } from '../character/CharacterCreateDialog';
import { useConfirmDialog } from '../dialog/ConfirmDialog';
import {
  UnifiedMediaViewer,
  type MediaItem as UnifiedMediaItem,
} from '../shared/media-preview';
import { VideoPosterPreview } from '../shared/VideoPosterPreview';
import { HoverTip } from '../shared';
import { getModelConfig } from '../../constants/model-config';
import {
  executeCanvasInsertion,
  type InsertionItem,
} from '../../services/canvas-operations';
import './dialog-task-list.scss';
import './dialog-task-loading-final.scss';

const TRUEGROWTH_PUBLISH_HANDOFF_EVENT = 'truegrowth:publish-asset';
const TRUEGROWTH_PUBLISH_HANDOFF_STORAGE_KEY = 'truegrowth-publish-handoff-v1';
const TRUEGROWTH_TIMELINE_HANDOFF_EVENT = 'truegrowth:timeline-asset';
const TRUEGROWTH_TIMELINE_HANDOFF_STORAGE_KEY =
  'truegrowth-timeline-handoff-v1';
const FALLBACK_TASK_FAILURE_REASON = '生成失败，请检查模型服务或参数配置。';
const TECHNICAL_FAILURE_PATTERNS = [
  /\b(seedance|volcengine|comfyui|api|endpoint|fetch|http|https|json|request|response|submit|xhr|network|timeout|exception|stack|trace)\b/i,
  /\b(4\d{2}|5\d{2})\b/,
  /\bfailed\b/i,
  /\berror\b/i,
  /\bnot\s+found\b/i,
  /\bunauthorized\b/i,
  /\bforbidden\b/i,
  /\bquota\b/i,
  /\brate\s*limit\b/i,
  /[{}[\]"']/,
];

export interface DialogTaskListProps {
  /** Task IDs to display. If not provided, shows all tasks (subject to taskType filter) */
  taskIds?: string[];
  /** Type of tasks to show (optional filter) - used for RPC filtering */
  taskType?: TaskType;
  /** Multiple task types to show in one creation feed. Uses RPC filtering only for single-type lists. */
  taskTypes?: TaskType[];
  /** Callback when edit button is clicked - if provided, will update parent form instead of opening dialog */
  onEditTask?: (task: any) => void;
}

type CreationBatch = {
  id: string;
  tasks: Task[];
  representative: Task;
  createdAt: number;
};

type CreationBatchItem = CreationBatch & {
  dateHeading: string | null;
};

type CreationBatchMedia = {
  id: string;
  task: Task;
  taskId: string;
  url?: string;
  thumbnailUrl?: string;
  duration?: number | null;
  title: string;
  index: number;
};

const sortBatchTasks = (tasks: Task[]): Task[] => {
  return [...tasks].sort((a, b) => {
    const aIndex =
      typeof a.params.batchIndex === 'number'
        ? a.params.batchIndex
        : Number.MAX_SAFE_INTEGER;
    const bIndex =
      typeof b.params.batchIndex === 'number'
        ? b.params.batchIndex
        : Number.MAX_SAFE_INTEGER;
    if (aIndex !== bIndex) {
      return aIndex - bIndex;
    }
    return a.createdAt - b.createdAt;
  });
};

const buildCreationBatches = (tasks: Task[]): CreationBatch[] => {
  const batchMap = new Map<string, Task[]>();

  tasks.forEach((task) => {
    const batchId =
      typeof task.params.batchId === 'string' && task.params.batchId.trim()
        ? task.params.batchId
        : task.id;
    const existing = batchMap.get(batchId);
    if (existing) {
      existing.push(task);
    } else {
      batchMap.set(batchId, [task]);
    }
  });

  return Array.from(batchMap.entries())
    .map(([id, batchTasks]) => {
      const sortedTasks = sortBatchTasks(batchTasks);
      const representative = sortedTasks[0] || batchTasks[0];
      return {
        id,
        tasks: sortedTasks,
        representative,
        createdAt: Math.max(...sortedTasks.map((task) => task.createdAt)),
      };
    })
    .sort((a, b) => b.createdAt - a.createdAt);
};

/**
 * DialogTaskList component - displays filtered tasks for a specific dialog
 * Now uses useFilteredTaskQueue for pagination and type filtering via RPC.
 */
export const DialogTaskList: React.FC<DialogTaskListProps> = ({
  taskIds,
  taskType,
  taskTypes,
  onEditTask,
}) => {
  const effectiveTaskTypes = useMemo(
    () =>
      Array.from(
        new Set(taskTypes?.length ? taskTypes : taskType ? [taskType] : [])
      ),
    [taskType, taskTypes]
  );
  const rpcTaskType =
    effectiveTaskTypes.length === 1 ? effectiveTaskTypes[0] : undefined;

  // 使用按类型过滤的分页 hook
  const {
    tasks,
    isLoading,
    isLoadingMore,
    hasMore,
    totalCount,
    loadedCount,
    loadMore,
    retryTask,
    deleteTask,
  } = useFilteredTaskQueue({ taskType: rpcTaskType });

  const { board, openDialog } = useDrawnix();
  const { confirm, confirmDialog } = useConfirmDialog();
  const [previewVisible, setPreviewVisible] = useState(false);
  const [previewInitialIndex, setPreviewInitialIndex] = useState<
    number | number[]
  >(0);
  const [previewInitialMode, setPreviewInitialMode] = useState<
    'single' | 'compare'
  >('single');
  // Character extraction dialog state
  const [characterDialogTask, setCharacterDialogTask] = useState<Task | null>(
    null
  );

  // Filter tasks by IDs and search text (type filtering is now done via RPC)
  const filteredTasks = useMemo(() => {
    let filtered = tasks;

    if (effectiveTaskTypes.length > 0) {
      filtered = filtered.filter((task) =>
        effectiveTaskTypes.includes(task.type)
      );
    }

    // 如果指定了 taskIds，进行过滤
    if (taskIds && taskIds.length > 0) {
      filtered = filtered.filter((task) => taskIds.includes(task.id));
    }

    // Sort by creation time - newest first
    return [...filtered].sort((a, b) => b.createdAt - a.createdAt);
  }, [tasks, taskIds, effectiveTaskTypes]);

  const resolveTask = useCallback(
    async (taskId: string) =>
      (await taskStorageReader.getTask(taskId)) ||
      taskQueueService.getTask(taskId) ||
      tasks.find((item) => item.id === taskId),
    [tasks]
  );

  // Task action handlers
  const handleRetry = (taskIds: string | string[]) => {
    const uniqueTaskIds = Array.from(
      new Set(Array.isArray(taskIds) ? taskIds : [taskIds])
    ).filter(Boolean);
    if (uniqueTaskIds.length === 0) {
      return;
    }

    uniqueTaskIds.forEach((taskId) => retryTask(taskId));
    MessagePlugin.success(
      uniqueTaskIds.length > 1
        ? `已重新提交 ${uniqueTaskIds.length} 个失败任务`
        : '已重新提交失败任务'
    );
  };

  const handleDelete = async (taskId: string) => {
    const accepted = await confirm({
      title: '删除这条记录？',
      description: '删除后会从作品流移除，已插入画布或已下载的素材不受影响。',
      confirmText: '删除',
      cancelText: '取消',
      confirmTheme: 'danger',
    });

    if (!accepted) {
      return;
    }

    deleteTask(taskId);
    MessagePlugin.success('任务记录已删除');
  };

  const handleDeleteBatch = async (taskIds: string[]) => {
    const uniqueTaskIds = Array.from(new Set(taskIds));
    if (uniqueTaskIds.length === 0) {
      return;
    }

    const accepted = await confirm({
      title: uniqueTaskIds.length > 1 ? '删除这一批记录？' : '删除这条记录？',
      description:
        uniqueTaskIds.length > 1
          ? `将从作品流移除 ${uniqueTaskIds.length} 条记录，已插入画布或已下载的素材不受影响。`
          : '删除后会从作品流移除，已插入画布或已下载的素材不受影响。',
      confirmText: '删除',
      cancelText: '取消',
      confirmTheme: 'danger',
    });

    if (!accepted) {
      return;
    }

    uniqueTaskIds.forEach((id) => deleteTask(id));
    MessagePlugin.success(
      uniqueTaskIds.length > 1
        ? `已删除 ${uniqueTaskIds.length} 条任务记录`
        : '任务记录已删除'
    );
  };

  const handleDownload = async (taskId: string) => {
    const task = await resolveTask(taskId);
    if (!task) return;

    const downloadItems = buildTaskDownloadItems(task);
    if (downloadItems.length === 0) return;

    try {
      const result = await smartDownload(downloadItems);
      if (result.openedCount > 0 && result.downloadedCount === 0) {
        MessagePlugin.success(
          result.openedCount > 1
            ? `已打开 ${result.openedCount} 个链接，请在新标签页下载`
            : '资源不支持直接下载，已打开链接'
        );
      } else {
        MessagePlugin.success(
          task.type === TaskType.AUDIO
            ? downloadItems.length > 1
              ? '多条音频已开始下载'
              : '音频下载成功'
            : downloadItems.length > 1
            ? '多图已开始下载'
            : '下载成功'
        );
      }
    } catch (error) {
      console.error('Download failed:', error);
      MessagePlugin.error('下载失败，请稍后重试');
    }
  };

  const handleDownloadBatch = async (taskIds: string[]) => {
    const resolvedTasks = (
      await Promise.all(taskIds.map((id) => resolveTask(id)))
    ).filter((task): task is Task => Boolean(task));
    const downloadItems = resolvedTasks.flatMap((task) =>
      buildTaskDownloadItems(task)
    );
    if (downloadItems.length === 0) {
      MessagePlugin.warning('这一批暂无可下载作品');
      return;
    }

    try {
      const result = await smartDownload(
        downloadItems,
        `truegrowth_batch_${Date.now()}.zip`
      );
      if (result.openedCount > 0 && result.downloadedCount === 0) {
        MessagePlugin.success(
          result.openedCount > 1
            ? `已打开 ${result.openedCount} 个链接，请在新标签页下载`
            : '资源不支持直接下载，已打开链接'
        );
      } else {
        MessagePlugin.success(
          downloadItems.length > 1 ? '这一批作品已开始下载' : '下载成功'
        );
      }
    } catch (error) {
      console.error('Batch download failed:', error);
      MessagePlugin.error('批量下载失败，请稍后重试');
    }
  };

  const handleInsert = async (taskId: string) => {
    const task = await resolveTask(taskId);
    if (!task || !hasTaskResultMedia(task)) {
      console.warn('Cannot insert: task result not available');
      MessagePlugin.warning('无法插入：任务结果为空');
      return;
    }

    const { result } = task;

    try {
      if (task.type === TaskType.IMAGE) {
        const urls = result.urls?.length ? result.urls : [result.url];
        await executeCanvasInsertion({
          items: urls.map((url) => ({
            type: 'image',
            content: normalizeImageDataUrl(url),
            groupId: `dialog-task-image-${task.id}`,
          })),
        });
        MessagePlugin.success(
          urls.length > 1 ? '多图已插入到白板' : '图片已插入到白板'
        );
      } else if (task.type === TaskType.VIDEO) {
        await executeCanvasInsertion({
          items: [{ type: 'video', content: result.url }],
        });
        MessagePlugin.success('视频已插入到白板');
      } else if (task.type === TaskType.AUDIO) {
        const urls = resolveAudioResultUrls(task);
        await executeCanvasInsertion({
          items: urls.map((url, index) => {
            const clip = task.result?.clips?.[index];
            return {
              type: 'audio',
              content: url,
              groupId: `dialog-task-audio-${task.id}`,
              metadata: {
                title:
                  clip?.title ||
                  task.result?.title ||
                  task.params.title ||
                  task.params.prompt,
                duration:
                  typeof clip?.duration === 'number'
                    ? clip.duration || undefined
                    : task.result?.duration,
                previewImageUrl:
                  clip?.imageLargeUrl ||
                  clip?.imageUrl ||
                  task.result?.previewImageUrl,
                tags:
                  typeof task.params.tags === 'string'
                    ? task.params.tags
                    : undefined,
                mv:
                  typeof task.params.mv === 'string'
                    ? task.params.mv
                    : undefined,
                prompt: task.params.prompt,
                providerTaskId: task.result?.providerTaskId || task.remoteId,
                clipId:
                  clip?.clipId ||
                  clip?.id ||
                  task.result?.clipIds?.[index] ||
                  task.result?.primaryClipId,
                clipIds: task.result?.clipIds,
              },
            };
          }),
        });
        MessagePlugin.success(
          urls.length > 1 ? '多条音频卡片已插入到白板' : '音频卡片已插入到白板'
        );
      }
    } catch (error) {
      console.error('Failed to insert to board:', error);
      MessagePlugin.error(
        `插入失败: ${error instanceof Error ? error.message : '未知错误'}`
      );
    }
  };

  const handleInsertBatch = async (taskIds: string[]) => {
    const resolvedTasks = (
      await Promise.all(taskIds.map((id) => resolveTask(id)))
    ).filter((task): task is Task => Boolean(task));
    const insertableTasks = resolvedTasks.filter(
      (task) => task.status === TaskStatus.COMPLETED && hasTaskResultMedia(task)
    );

    if (insertableTasks.length === 0) {
      MessagePlugin.warning('这一批暂无可插入作品');
      return;
    }

    try {
      const items: InsertionItem[] = insertableTasks.flatMap<InsertionItem>(
        (task) => {
          if (task.type === TaskType.IMAGE) {
            const urls = task.result!.urls?.length
              ? task.result!.urls
              : [task.result!.url];
            return urls.map((url) => ({
              type: 'image' as const,
              content: normalizeImageDataUrl(url),
              groupId: `dialog-batch-image-${task.id}`,
            }));
          }
          if (task.type === TaskType.VIDEO && task.result?.url) {
            return [
              {
                type: 'video' as const,
                content: task.result.url,
                groupId: `dialog-batch-video-${task.id}`,
              },
            ];
          }
          if (task.type === TaskType.AUDIO) {
            const urls = resolveAudioResultUrls(task);
            return urls.map((url, index) => {
              const clip = task.result?.clips?.[index];
              return {
                type: 'audio' as const,
                content: url,
                groupId: `dialog-batch-audio-${task.id}`,
                metadata: {
                  title:
                    clip?.title ||
                    task.result?.title ||
                    task.params.title ||
                    task.params.prompt,
                  duration:
                    typeof clip?.duration === 'number'
                      ? clip.duration || undefined
                      : task.result?.duration,
                  previewImageUrl:
                    clip?.imageLargeUrl ||
                    clip?.imageUrl ||
                    task.result?.previewImageUrl,
                  tags:
                    typeof task.params.tags === 'string'
                      ? task.params.tags
                      : undefined,
                  mv:
                    typeof task.params.mv === 'string'
                      ? task.params.mv
                      : undefined,
                  prompt: task.params.prompt,
                  providerTaskId: task.result?.providerTaskId || task.remoteId,
                  clipId:
                    clip?.clipId ||
                    clip?.id ||
                    task.result?.clipIds?.[index] ||
                    task.result?.primaryClipId,
                  clipIds: task.result?.clipIds,
                },
              };
            });
          }
          return [];
        }
      );
      await executeCanvasInsertion({ items });
      MessagePlugin.success(`已插入 ${items.length} 个作品到画布`);
    } catch (error) {
      console.error('Failed to insert batch to board:', error);
      MessagePlugin.error(
        `插入失败: ${error instanceof Error ? error.message : '未知错误'}`
      );
    }
  };

  const handlePublish = async (taskId: string) => {
    const task = await resolveTask(taskId);
    const mediaUrl = task ? getFeedMediaUrl(task) : undefined;
    if (
      !task ||
      task.status !== TaskStatus.COMPLETED ||
      !mediaUrl ||
      (task.type !== TaskType.IMAGE && task.type !== TaskType.VIDEO)
    ) {
      MessagePlugin.warning('当前作品还不能发布');
      return;
    }

    const asset = buildPublishAsset(task, mediaUrl);
    const detail = {
      asset,
      assets: [asset],
      route: '/publish',
      createdAt: Date.now(),
    };

    if (typeof window !== 'undefined') {
      try {
        window.localStorage.setItem(
          TRUEGROWTH_PUBLISH_HANDOFF_STORAGE_KEY,
          JSON.stringify(detail)
        );
      } catch (error) {
        console.warn('[TrueGrowth] Failed to store publish handoff:', error);
      }
      window.dispatchEvent(
        new CustomEvent(TRUEGROWTH_PUBLISH_HANDOFF_EVENT, { detail })
      );
      if (window.location.pathname !== '/publish') {
        window.history.pushState(null, '', '/publish');
        window.dispatchEvent(new PopStateEvent('popstate'));
      }
    }
    MessagePlugin.success('已带入发布中心草稿');
  };

  const handlePublishBatch = async (taskIds: string[]) => {
    const resolvedTasks = (
      await Promise.all(taskIds.map((id) => resolveTask(id)))
    ).filter((task): task is Task => Boolean(task));
    const assets = resolvedTasks
      .filter(
        (task) =>
          task.status === TaskStatus.COMPLETED &&
          (task.type === TaskType.IMAGE || task.type === TaskType.VIDEO)
      )
      .flatMap((task) => buildPublishAssets(task));

    if (assets.length === 0) {
      MessagePlugin.warning('这一批暂无可发布作品');
      return;
    }

    const detail = {
      asset: assets[0],
      assets,
      route: '/publish',
      createdAt: Date.now(),
    };

    if (typeof window !== 'undefined') {
      try {
        window.localStorage.setItem(
          TRUEGROWTH_PUBLISH_HANDOFF_STORAGE_KEY,
          JSON.stringify(detail)
        );
      } catch (error) {
        console.warn('[TrueGrowth] Failed to store publish handoff:', error);
      }
      window.dispatchEvent(
        new CustomEvent(TRUEGROWTH_PUBLISH_HANDOFF_EVENT, { detail })
      );
      if (window.location.pathname !== '/publish') {
        window.history.pushState(null, '', '/publish');
        window.dispatchEvent(new PopStateEvent('popstate'));
      }
    }
    MessagePlugin.success(`已带入 ${assets.length} 个作品到发布中心`);
  };

  const handleClip = async (taskId: string) => {
    const task = await resolveTask(taskId);
    if (
      !task ||
      task.type !== TaskType.VIDEO ||
      task.status !== TaskStatus.COMPLETED ||
      !task.result
    ) {
      MessagePlugin.warning('当前视频还不能剪辑');
      return;
    }

    const url = task.result.urls?.[0] || task.result.url;
    if (!url) {
      MessagePlugin.warning('当前视频没有可剪辑的视频源');
      return;
    }

    const title = getFeedTitle(task);
    const detail = {
      asset: {
        id: `task-${task.id}`,
        assetId: `task-${task.id}`,
        type: 'video',
        name: title,
        source: 'ai-video',
        provider: task.params.model || 'TrueGrowth',
        url,
        thumbnailUrl:
          task.result.thumbnailUrls?.[0] ||
          task.result.previewImageUrl ||
          undefined,
        durationMs:
          typeof task.result.duration === 'number'
            ? task.result.duration * 1000
            : undefined,
        width: task.result.width,
        height: task.result.height,
        createdAt: task.completedAt || task.createdAt || Date.now(),
        publishState: 'unpublished',
        sourceRefs: [
          {
            type: 'task',
            id: task.id,
            label: title,
          },
        ],
      },
      route: '/timeline',
      intent: 'clip',
      createdAt: Date.now(),
    };

    if (typeof window !== 'undefined') {
      try {
        window.localStorage.setItem(
          TRUEGROWTH_TIMELINE_HANDOFF_STORAGE_KEY,
          JSON.stringify(detail)
        );
      } catch (error) {
        console.warn('[TrueGrowth] Failed to store timeline handoff:', error);
      }
      window.dispatchEvent(
        new CustomEvent(TRUEGROWTH_TIMELINE_HANDOFF_EVENT, { detail })
      );
      if (window.location.pathname !== '/timeline') {
        window.history.pushState(null, '', '/timeline');
        window.dispatchEvent(new PopStateEvent('popstate'));
      }
    }
    MessagePlugin.success('已带入剪辑工作台，请选择剪辑能力');
  };

  const confirmOverwriteDraftIfNeeded = useCallback(async () => {
    if (!hasAIImageDraftContent()) {
      return true;
    }

    return confirm({
      title: '覆盖当前输入？',
      description:
        '当前 AI 图片生成窗口已有提示词或参考图，继续会覆盖当前输入。',
      confirmText: '覆盖当前输入',
      cancelText: '取消',
      confirmTheme: 'warning',
    });
  }, [confirm]);

  const handleRegenerate = async (taskId: string) => {
    const task =
      (await taskStorageReader.getTask(taskId)) ||
      taskQueueService.getTask(taskId) ||
      tasks.find((item) => item.id === taskId);
    if (!task || task.type !== TaskType.IMAGE) {
      MessagePlugin.warning('未找到可回填的图片任务');
      return;
    }

    if (!(await confirmOverwriteDraftIfNeeded())) {
      return;
    }

    if (onEditTask) {
      onEditTask(task);
    } else {
      openDialog(
        DialogType.aiImageGeneration,
        buildImageTaskPrefillInitialData(task)
      );
    }
    MessagePlugin.success('已回填历史提示词和参考图，请手动发送');
  };

  const handleEdit = async (taskId: string) => {
    const task =
      (await taskStorageReader.getTask(taskId)) ||
      taskQueueService.getTask(taskId) ||
      tasks.find((item) => item.id === taskId);
    if (!task) {
      console.warn('Cannot edit: task not found');
      return;
    }

    if (task.type === TaskType.IMAGE && task.status !== TaskStatus.COMPLETED) {
      MessagePlugin.warning('只有已完成且有结果图的图片任务可以编辑');
      return;
    }

    if (
      task.type === TaskType.IMAGE &&
      !task.result?.url &&
      !task.result?.urls?.length
    ) {
      MessagePlugin.warning('当前任务没有可编辑的结果图');
      return;
    }

    // 如果有 onEditTask 回调（从弹窗内部调用），直接更新父组件表单
    if (onEditTask) {
      onEditTask(task);
      return;
    }

    // 否则打开新的对话框（从任务队列面板调用）
    if (task.type === TaskType.IMAGE) {
      // 准备图片生成初始数据
      openDialog(
        DialogType.aiImageGeneration,
        buildImageTaskPrefillInitialData(task)
      );
    } else if (task.type === TaskType.VIDEO) {
      // 准备视频生成初始数据
      const initialData = {
        initialPrompt: task.params.prompt,
        initialDuration:
          typeof task.params.seconds === 'string'
            ? parseInt(task.params.seconds, 10)
            : task.params.seconds, // 确保转换为数字
        initialModel: task.params.model, // 传递模型
        initialSize: task.params.size, // 传递尺寸
        initialImages: task.params.uploadedImages, // 传递上传的图片（多图片格式）
        initialResultUrl: task.result?.url, // 传递结果URL用于预览
        initialResultUrls: task.result?.urls, // 多图/多视频结果
      };
      // console.log('DialogTaskList - handleEdit VIDEO task:', {
      //   taskId,
      //   taskParams: task.params,
      //   initialData
      // });
      openDialog(DialogType.aiVideoGeneration, initialData);
    }
  };

  // Get completed tasks with results for navigation (deduplicated by ID)
  const completedTasksWithResults = useMemo(() => {
    const seen = new Set<string>();
    return filteredTasks.filter((t) => {
      if (t.status !== TaskStatus.COMPLETED) return false;
      if (!hasTaskResultMedia(t)) return false;
      if (
        t.type !== TaskType.IMAGE &&
        t.type !== TaskType.VIDEO &&
        !(t.type === TaskType.AUDIO && t.result?.resultKind !== 'lyrics')
      ) {
        return false;
      }
      if (seen.has(t.id)) return false;
      seen.add(t.id);
      return true;
    });
  }, [filteredTasks]);

  // 展开多图任务为多个 MediaItem，同时建立 taskId -> 首个 previewIndex 的映射
  const { previewMediaItems, taskIdToPreviewConfig } = useMemo(() => {
    const items: UnifiedMediaItem[] = [];
    const configMap = new Map<
      string,
      { mode: 'single' | 'compare'; index: number | number[] }
    >();

    for (const task of completedTasksWithResults) {
      const startIndex = items.length;
      const title =
        task.result?.title ||
        task.params.title ||
        task.params.prompt?.substring(0, 50) ||
        '媒体预览';

      if (task.type === TaskType.AUDIO) {
        const audioItems =
          task.result?.clips
            ?.filter((clip) => Boolean(clip.audioUrl))
            .map((clip, i) => ({
              id: clip.clipId || clip.id || `${task.id}-${i}`,
              url: clip.audioUrl,
              type: 'audio' as const,
              title:
                clip.title ||
                (task.result?.clips && task.result.clips.length > 1
                  ? `${title} (${i + 1}/${task.result.clips.length})`
                  : title),
              posterUrl:
                clip.imageLargeUrl ||
                clip.imageUrl ||
                task.result?.previewImageUrl,
              duration:
                typeof clip.duration === 'number'
                  ? clip.duration
                  : task.result?.duration,
              prompt: task.params.prompt,
              tags:
                typeof task.params.tags === 'string'
                  ? task.params.tags
                  : undefined,
              artist: task.params.model || task.params.mv || 'TrueGrowth',
              album: 'TrueGrowth Generated',
            })) ?? [];
        const fallbackUrls = task.result!.urls?.length
          ? task.result!.urls
          : task.result!.url
          ? [task.result!.url]
          : [];
        const mediaItems =
          audioItems.length > 0
            ? audioItems
            : fallbackUrls.map((url, i) => ({
                id: fallbackUrls.length > 1 ? `${task.id}-${i}` : task.id,
                url,
                type: 'audio' as const,
                title:
                  fallbackUrls.length > 1
                    ? `${title} (${i + 1}/${fallbackUrls.length})`
                    : title,
                posterUrl: task.result?.previewImageUrl,
                duration: task.result?.duration,
                prompt: task.params.prompt,
                tags:
                  typeof task.params.tags === 'string'
                    ? task.params.tags
                    : undefined,
                artist: task.params.model || task.params.mv || 'TrueGrowth',
                album: 'TrueGrowth Generated',
              }));
        items.push(...mediaItems);
        mediaItems.forEach((item, offset) => {
          configMap.set(item.id, {
            mode: 'single',
            index: startIndex + offset,
          });
        });
      } else {
        const urls = task.result!.urls?.length
          ? task.result!.urls
          : [task.result!.url];
        const mediaType =
          task.type === TaskType.VIDEO
            ? ('video' as const)
            : ('image' as const);

        for (let i = 0; i < urls.length; i++) {
          const previewKey = `${task.id}:${i}`;
          items.push({
            id: previewKey,
            url:
              mediaType === 'image' ? normalizeImageDataUrl(urls[i]) : urls[i],
            type: mediaType,
            title:
              urls.length > 1 ? `${title} (${i + 1}/${urls.length})` : title,
          });
          configMap.set(previewKey, {
            mode: 'single',
            index: startIndex + i,
          });
        }
      }

      const taskItemCount = items.length - startIndex;
      configMap.set(task.id, {
        mode:
          task.type === TaskType.AUDIO && taskItemCount > 1
            ? 'compare'
            : 'single',
        index:
          task.type === TaskType.AUDIO && taskItemCount > 1
            ? Array.from(
                { length: Math.min(taskItemCount, 4) },
                (_, offset) => startIndex + offset
              )
            : startIndex,
      });
    }

    return { previewMediaItems: items, taskIdToPreviewConfig: configMap };
  }, [completedTasksWithResults]);

  // Preview handlers - 使用 Map 精确查找索引
  const handlePreviewOpen = useCallback(
    (taskId: string) => {
      const config = taskIdToPreviewConfig.get(taskId);
      if (config !== undefined) {
        setPreviewInitialMode(config.mode);
        setPreviewInitialIndex(config.index);
        setPreviewVisible(true);
      }
    },
    [taskIdToPreviewConfig]
  );

  const handlePreviewClose = useCallback(() => {
    setPreviewVisible(false);
  }, []);

  const groupedTaskItems = useMemo(() => {
    const batches = buildCreationBatches(filteredTasks);
    return batches.map((batch, index): CreationBatchItem => {
      const currentDate = new Date(batch.createdAt);
      const previousBatch = batches[index - 1];
      const previousDate = previousBatch
        ? new Date(previousBatch.createdAt)
        : null;
      const currentKey = currentDate.toDateString();
      const previousKey = previousDate?.toDateString();
      return {
        ...batch,
        dateHeading:
          currentKey !== previousKey
            ? currentDate.toLocaleDateString('zh-CN', {
                month: 'long',
                day: 'numeric',
              })
            : null,
      };
    });
  }, [filteredTasks]);

  // 显示的总数（优先使用 RPC 返回的总数）
  const displayTotalCount = totalCount > 0 ? totalCount : tasks.length;
  const activeFilter =
    effectiveTaskTypes.length > 1
      ? 'mixed'
      : effectiveTaskTypes[0] === TaskType.VIDEO
      ? 'video'
      : effectiveTaskTypes[0] === TaskType.IMAGE
      ? 'image'
      : effectiveTaskTypes[0] === TaskType.AUDIO
      ? 'audio'
      : 'all';
  const feedHeading = formatFeedDateHeading(filteredTasks[0]?.createdAt);
  const feedLabel =
    activeFilter === 'video'
      ? '视频生成'
      : activeFilter === 'image'
      ? '图片生成'
      : activeFilter === 'audio'
      ? '音乐生成'
      : activeFilter === 'mixed'
      ? '创作记录'
      : '全部生成';
  const feedSummary = getFeedSummary(filteredTasks, rpcTaskType);
  const feedEmptyText =
    activeFilter === 'video'
      ? '生成后的视频作品会按日期显示在这里。'
      : activeFilter === 'audio'
      ? '生成后的音乐作品会按日期显示在这里。'
      : '生成后的作品会按日期显示在这里。';

  return (
    <>
      {confirmDialog}

      <div className="dialog-task-list tg-creation-feed">
        <div className="tg-creation-feed__header">
          <h2>{feedHeading}</h2>
        </div>
        <div className="dialog-task-list__content dialog-task-list__content--grouped tg-creation-feed__content">
          {groupedTaskItems.length ? (
            <>
              {groupedTaskItems.map((batchItem) => (
                <React.Fragment key={batchItem.id}>
                  {batchItem.dateHeading ? (
                    <h3 className="dialog-task-list__date-heading tg-creation-feed__date">
                      {batchItem.dateHeading}
                    </h3>
                  ) : null}
                  <CreationBatchCard
                    batch={batchItem}
                    onRetry={handleRetry}
                    onDownloadBatch={handleDownloadBatch}
                    onInsertBatch={handleInsertBatch}
                    onEdit={handleEdit}
                    onRegenerate={handleRegenerate}
                    onDelete={handleDelete}
                    onDeleteBatch={handleDeleteBatch}
                    onPublishBatch={handlePublishBatch}
                    onClip={handleClip}
                    onPreviewOpen={handlePreviewOpen}
                  />
                </React.Fragment>
              ))}
              {hasMore ? (
                <button
                  className="dialog-task-list__load-more"
                  type="button"
                  disabled={isLoadingMore}
                  onClick={loadMore}
                >
                  {isLoadingMore
                    ? '加载中...'
                    : `加载更多 ${loadedCount}/${displayTotalCount}`}
                </button>
              ) : null}
            </>
          ) : (
            <div className="dialog-task-list__empty">
              {isLoading ? <p>加载中...</p> : <p>{feedEmptyText}</p>}
            </div>
          )}
        </div>
      </div>

      {/* Unified Preview */}
      <UnifiedMediaViewer
        visible={previewVisible}
        items={previewMediaItems}
        initialMode={previewInitialMode}
        initialIndex={previewInitialIndex}
        onClose={handlePreviewClose}
        showThumbnails={true}
      />

      {/* Character Create Dialog */}
      <CharacterCreateDialog
        visible={!!characterDialogTask}
        task={characterDialogTask}
        onClose={() => setCharacterDialogTask(null)}
        onCreateComplete={(characterId) => {
          // console.log('Character created:', characterId);
          setCharacterDialogTask(null);
        }}
      />
    </>
  );
};

function getStatusLabel(status: TaskStatus): string {
  if (status === TaskStatus.COMPLETED) return '已完成';
  if (status === TaskStatus.FAILED) return '生成失败';
  if (status === TaskStatus.CANCELLED) return '已取消';
  return '生成中';
}

function truncateFailureReason(text: string, maxLength = 120): string {
  const value = text.replace(/\s+/g, ' ').trim();
  if (value.length <= maxLength) {
    return value;
  }

  return `${value.slice(0, maxLength - 1)}…`;
}

function getFriendlyTaskFailureReason(task: Task, rawReason?: string): string {
  const fallback =
    task.type === TaskType.VIDEO
      ? '视频生成失败，请检查视频模型配置、额度或网络后重试。'
      : task.type === TaskType.IMAGE
      ? '图片生成失败，请检查模型配置、额度或网络后重试。'
      : task.type === TaskType.AUDIO
      ? '音乐生成失败，请检查音乐模型配置、额度或网络后重试。'
      : FALLBACK_TASK_FAILURE_REASON;

  if (!rawReason) {
    return fallback;
  }

  const normalized = rawReason.replace(/^失败原因[:：]\s*/i, '').trim();
  const isTechnical = TECHNICAL_FAILURE_PATTERNS.some((pattern) =>
    pattern.test(normalized)
  );

  if (isTechnical || normalized.length > 90) {
    return fallback;
  }

  return truncateFailureReason(normalized, 90);
}

function normalizeFailureReason(value: unknown): string | undefined {
  if (value === undefined || value === null) {
    return undefined;
  }

  if (typeof value === 'string') {
    const trimmed = value.trim();
    if (!trimmed) {
      return undefined;
    }

    const looksLikeJson =
      (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
      (trimmed.startsWith('[') && trimmed.endsWith(']'));
    if (looksLikeJson) {
      try {
        const parsed = JSON.parse(trimmed);
        const parsedText = normalizeFailureReason(parsed);
        if (parsedText) {
          return parsedText;
        }
      } catch {
        // Fall back to the raw API text below.
      }
    }

    return truncateFailureReason(trimmed.replace(/^error:\s*/i, ''));
  }

  if (value instanceof Error) {
    return normalizeFailureReason(value.message);
  }

  if (Array.isArray(value)) {
    for (const item of value) {
      const text = normalizeFailureReason(item);
      if (text) {
        return text;
      }
    }
    return undefined;
  }

  if (typeof value === 'object') {
    const record = value as Record<string, unknown>;
    const preferredKeys = [
      'message',
      'msg',
      'originalError',
      'errorMessage',
      'failReason',
      'failureReason',
      'reason',
      'task_status_msg',
      'detail',
      'details',
      'error',
      'apiResponse',
      'data',
    ];

    for (const key of preferredKeys) {
      const text = normalizeFailureReason(record[key]);
      if (text) {
        return text;
      }
    }
  }

  return undefined;
}

function getTaskFailureReason(task: Task): string {
  const taskWithLooseFields = task as Task & {
    error?: unknown;
    errorMessage?: unknown;
    failReason?: unknown;
  };

  const candidates: unknown[] = [
    task.error?.message,
    task.error?.details?.originalError,
    task.error?.details?.apiResponse,
    taskWithLooseFields.errorMessage,
    taskWithLooseFields.failReason,
    task.params.errorMessage,
    task.params.failReason,
    task.params.failureReason,
    (task.result as { errorMessage?: unknown } | undefined)?.errorMessage,
    (task.result as { failReason?: unknown } | undefined)?.failReason,
    taskWithLooseFields.error,
  ];

  for (const candidate of candidates) {
    const reason = normalizeFailureReason(candidate);
    if (reason) {
      return getFriendlyTaskFailureReason(task, reason);
    }
  }

  return getFriendlyTaskFailureReason(task);
}

function getBatchFailureReason(batch: CreationBatch): string | undefined {
  const reasons = Array.from(
    new Set(
      batch.tasks
        .filter((task) => task.status === TaskStatus.FAILED)
        .map((task) => getTaskFailureReason(task))
        .filter(Boolean)
    )
  );

  if (reasons.length === 0) {
    return undefined;
  }

  if (reasons.length === 1) {
    return reasons[0];
  }

  return `${reasons[0]}（另有 ${reasons.length - 1} 条失败原因）`;
}

function formatFeedTime(value: number): string {
  return new Date(value).toLocaleTimeString('zh-CN', {
    hour: '2-digit',
    minute: '2-digit',
  });
}

function formatFeedDateHeading(value?: number): string {
  if (!value) return '今天';
  const date = new Date(value);
  const today = new Date();

  if (
    date.getFullYear() === today.getFullYear() &&
    date.getMonth() === today.getMonth() &&
    date.getDate() === today.getDate()
  ) {
    return '今天';
  }

  return date.toLocaleDateString('zh-CN', {
    month: 'long',
    day: 'numeric',
  });
}

function getFeedTitle(task: Task): string {
  return (
    task.result?.title ||
    task.params.title ||
    task.params.prompt ||
    (task.type === TaskType.VIDEO
      ? '视频作品'
      : task.type === TaskType.AUDIO
      ? '音频作品'
      : '图片作品')
  );
}

function getFeedMediaUrl(task: Task): string | undefined {
  const url =
    task.type === TaskType.AUDIO
      ? resolveAudioResultUrls(task)[0]
      : task.result?.urls?.[0] || task.result?.url;
  return task.type === TaskType.IMAGE && url ? normalizeImageDataUrl(url) : url;
}

function resolveAudioResultUrls(task: Task): string[] {
  if (task.type !== TaskType.AUDIO) {
    return [];
  }

  const clipUrls =
    task.result?.clips
      ?.map((clip) => clip.audioUrl)
      .filter((url): url is string => Boolean(url)) ?? [];
  if (clipUrls.length) {
    return clipUrls;
  }

  const fallbackUrls = task.result?.urls?.length
    ? task.result.urls
    : task.result?.url
    ? [task.result.url]
    : [];
  return fallbackUrls.filter((url): url is string => Boolean(url));
}

function hasTaskResultMedia(
  task: Task
): task is Task & { result: NonNullable<Task['result']> } {
  if (!task.result) {
    return false;
  }

  if (task.type === TaskType.AUDIO) {
    return (
      task.result.resultKind !== 'lyrics' &&
      resolveAudioResultUrls(task).length > 0
    );
  }

  return Boolean(task.result.url || task.result.urls?.length);
}

function getTaskMediaItems(task: Task): CreationBatchMedia[] {
  const title = getFeedTitle(task);
  const audioClipItems =
    task.type === TaskType.AUDIO
      ? task.result?.clips
          ?.filter((clip) => Boolean(clip.audioUrl))
          .map((clip, index) => ({
            id: clip.clipId || clip.id || `${task.id}:${index}`,
            task,
            taskId: task.id,
            url: clip.audioUrl,
            thumbnailUrl:
              clip.imageLargeUrl ||
              clip.imageUrl ||
              task.result?.previewImageUrl,
            duration: clip.duration,
            title:
              clip.title ||
              (task.result?.clips && task.result.clips.length > 1
                ? `${title} ${index + 1}`
                : title),
            index:
              typeof task.params.batchIndex === 'number'
                ? task.params.batchIndex
                : index + 1,
          })) ?? []
      : [];
  const urls =
    task.type === TaskType.AUDIO
      ? resolveAudioResultUrls(task)
      : task.result?.urls?.length
      ? task.result.urls
      : task.result?.url
      ? [task.result.url]
      : [];

  if (audioClipItems.length) {
    return audioClipItems;
  }

  if (urls.length === 0) {
    return [
      {
        id: `${task.id}:pending`,
        task,
        taskId: task.id,
        title,
        index:
          typeof task.params.batchIndex === 'number'
            ? task.params.batchIndex
            : 1,
      },
    ];
  }

  return urls.map((url, index) => ({
    id: `${task.id}:${index}`,
    task,
    taskId: task.id,
    url: task.type === TaskType.IMAGE ? normalizeImageDataUrl(url) : url,
    thumbnailUrl:
      task.type === TaskType.VIDEO
        ? task.result?.thumbnailUrls?.[index] || task.result?.previewImageUrl
        : task.result?.thumbnailUrls?.[index] ||
          task.result?.previewImageUrl ||
          normalizeImageDataUrl(url),
    duration:
      task.type === TaskType.AUDIO && typeof task.result?.duration === 'number'
        ? task.result.duration
        : undefined,
    title: urls.length > 1 ? `${title} ${index + 1}` : title,
    index:
      typeof task.params.batchIndex === 'number'
        ? task.params.batchIndex
        : index + 1,
  }));
}

function getBatchMediaItems(batch: CreationBatch): CreationBatchMedia[] {
  return batch.tasks.flatMap((task) => getTaskMediaItems(task));
}

function getBatchTitle(batch: CreationBatch): string {
  return getFeedTitle(batch.representative);
}

function getBatchReferenceImages(
  batch: CreationBatch
): ImageGenerationReferenceImage[] {
  const seen = new Set<string>();
  const references: ImageGenerationReferenceImage[] = [];

  batch.tasks.forEach((task) => {
    getImageTaskReferenceImages(task).forEach((image) => {
      if (!image.url || seen.has(image.url)) {
        return;
      }
      seen.add(image.url);
      references.push(image);
    });
  });

  return references;
}

function readTaskExtraParams(task: Task): Record<string, string> {
  const params = task.params.params;
  if (!params || typeof params !== 'object' || Array.isArray(params)) {
    return {};
  }

  return Object.entries(params as Record<string, unknown>).reduce<
    Record<string, string>
  >((acc, [key, value]) => {
    if (value !== undefined && value !== null && String(value).trim()) {
      acc[key] = String(value);
    }
    return acc;
  }, {});
}

function isVisibleMetaValue(
  value?: string | number | null
): value is string | number {
  if (value === undefined || value === null) return false;
  const text = String(value).trim();
  return Boolean(text) && text.toLowerCase() !== 'auto';
}

function formatBatchModel(task: Task): string | undefined {
  const modelId =
    task.params.modelRef?.modelId || task.params.model || task.params.mv;
  if (!isVisibleMetaValue(modelId)) {
    return undefined;
  }

  const config = getModelConfig(String(modelId));
  return (
    config?.shortLabel || config?.label || config?.shortCode || String(modelId)
  );
}

function formatBatchSize(batch: CreationBatch): string | undefined {
  const task = batch.representative;
  const params = task.params;
  const extraParams = readTaskExtraParams(task);
  const size =
    params.size ||
    params.resolution ||
    extraParams.size ||
    extraParams.resolution;
  if (isVisibleMetaValue(size)) {
    return String(size);
  }

  const resultWithDimensions = batch.tasks.find(
    (item) => item.result?.width && item.result?.height
  );
  if (
    resultWithDimensions?.result?.width &&
    resultWithDimensions.result.height
  ) {
    return `${resultWithDimensions.result.width}x${resultWithDimensions.result.height}`;
  }

  if (params.width && params.height) {
    return `${params.width}x${params.height}`;
  }

  return undefined;
}

function readLooseRecord(value: unknown): Record<string, unknown> | undefined {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return undefined;
  }

  return value as Record<string, unknown>;
}

function readLooseValue(
  task: Task,
  keys: string[]
): string | number | undefined {
  const paramRecord = task.params as unknown as Record<string, unknown>;
  const resultRecord = task.result as unknown as Record<string, unknown>;
  const records = [
    paramRecord,
    readLooseRecord(paramRecord.params),
    readLooseRecord(paramRecord.metadata),
    readLooseRecord(paramRecord.options),
    readLooseRecord(paramRecord.extra),
    resultRecord,
    readLooseRecord(resultRecord?.metadata),
    readLooseRecord(resultRecord?.params),
  ].filter(Boolean) as Record<string, unknown>[];

  for (const record of records) {
    for (const key of keys) {
      const value = record[key];
      if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
        return value;
      }
      if (typeof value === 'string' && value.trim()) {
        return value.trim();
      }
    }
  }

  return undefined;
}

function parsePositiveDimension(value: unknown): number | undefined {
  if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
    return value;
  }

  if (typeof value === 'string') {
    const parsed = Number.parseFloat(value.replace(/[^\d.]/g, ''));
    if (Number.isFinite(parsed) && parsed > 0) {
      return parsed;
    }
  }

  return undefined;
}

function parseRatioText(value: unknown): number | undefined {
  if (!isVisibleMetaValue(value as string | number | undefined)) {
    return undefined;
  }

  const text = String(value)
    .trim()
    .toLowerCase()
    .replace(/[：]/g, ':')
    .replace(/[×*]/g, 'x');
  const pairMatch = text.match(/(\d+(?:\.\d+)?)\s*[:x/]\s*(\d+(?:\.\d+)?)/);

  if (pairMatch) {
    const width = Number.parseFloat(pairMatch[1]);
    const height = Number.parseFloat(pairMatch[2]);
    if (width > 0 && height > 0) {
      return width / height;
    }
  }

  if (/(portrait|vertical|竖屏|竖版|手机|小红书|抖音)/i.test(text)) {
    return 9 / 16;
  }

  if (/(landscape|horizontal|横屏|横版|宽屏|bilibili|youtube)/i.test(text)) {
    return 16 / 9;
  }

  if (/(square|方形|正方形)/i.test(text)) {
    return 1;
  }

  const numeric = Number.parseFloat(text);
  if (Number.isFinite(numeric) && numeric > 0 && numeric < 10) {
    return numeric;
  }

  return undefined;
}

function getTaskPlaceholderRatio(task: Task): number {
  const width = parsePositiveDimension(
    readLooseValue(task, ['width', 'w', 'imageWidth', 'videoWidth'])
  );
  const height = parsePositiveDimension(
    readLooseValue(task, ['height', 'h', 'imageHeight', 'videoHeight'])
  );

  if (width && height) {
    return width / height;
  }

  const ratio = parseRatioText(
    readLooseValue(task, [
      'aspectRatio',
      'aspect_ratio',
      'ratio',
      'imageRatio',
      'videoRatio',
      'format',
      'videoFormat',
      'screenRatio',
      'screen_ratio',
      'outputRatio',
      'output_ratio',
      'dimensions',
      'dimension',
      'size',
      'imageSize',
      'image_size',
      'videoSize',
      'video_size',
      'outputSize',
      'output_size',
      'resolution',
    ])
  );

  if (ratio) {
    return ratio;
  }

  return task.type === TaskType.VIDEO ? 16 / 9 : 1;
}

function getTaskPlaceholderStyle(task: Task): CSSProperties {
  const ratio = Math.min(3, Math.max(1 / 3, getTaskPlaceholderRatio(task)));
  const width =
    ratio >= 1
      ? Math.min(360, Math.round(240 * ratio))
      : Math.round(320 * ratio);
  const height = ratio >= 1 ? Math.max(168, Math.round(width / ratio)) : 320;
  const processingRingSize = Math.min(
    54,
    Math.max(34, Math.round(height * 0.24))
  );
  const processingCopyHeight = Math.min(
    20,
    Math.max(16, Math.round(height * 0.095))
  );
  const processingGap = Math.min(10, Math.max(4, Math.round(height * 0.035)));
  const processingPadding = Math.min(
    18,
    Math.max(8, Math.round(height * 0.06))
  );

  return {
    '--tg-creation-placeholder-ratio': String(ratio),
    '--tg-creation-placeholder-width': `${width}px`,
    '--tg-creation-placeholder-height': `${height}px`,
    '--tg-processing-ring-size': `${processingRingSize}px`,
    '--tg-processing-copy-height': `${processingCopyHeight}px`,
    '--tg-processing-gap': `${processingGap}px`,
    '--tg-processing-padding': `${processingPadding}px`,
    '--tg-loading-ring-size': `${processingRingSize}px`,
    '--tg-loading-copy-height': `${processingCopyHeight}px`,
    '--tg-loading-gap': `${processingGap}px`,
    '--tg-loading-padding': `${processingPadding}px`,
  } as CSSProperties;
}

function getBatchMeta(batch: CreationBatch): string {
  const task = batch.representative;
  const params = task.params;
  const count =
    typeof params.batchTotal === 'number' &&
    params.batchTotal > batch.tasks.length
      ? params.batchTotal
      : batch.tasks.length;
  const unit =
    task.type === TaskType.VIDEO
      ? '条'
      : task.type === TaskType.AUDIO
      ? '首'
      : '张';
  const pieces = [
    `${count} ${unit}`,
    formatBatchModel(task),
    formatBatchSize(batch),
  ].filter(isVisibleMetaValue);

  return pieces.join('  |  ') || '历史参数待补全';
}

function getBatchStatusMeta(batch: CreationBatch): string {
  const completedCount = batch.tasks.filter(
    (task) => task.status === TaskStatus.COMPLETED
  ).length;
  const failedCount = batch.tasks.filter(
    (task) => task.status === TaskStatus.FAILED
  ).length;
  const processingCount = batch.tasks.filter(
    (task) =>
      task.status === TaskStatus.PROCESSING ||
      task.status === TaskStatus.PENDING
  ).length;
  const pieces = [
    `${formatFeedTime(batch.createdAt)}`,
    completedCount ? `完成 ${completedCount}` : undefined,
    processingCount ? `生成中 ${processingCount}` : undefined,
    failedCount ? `失败 ${failedCount}` : undefined,
  ].filter(Boolean);

  return pieces.join('  |  ');
}

function getFeedSummary(tasks: Task[], taskType?: TaskType): string {
  const latestBatch = buildCreationBatches(tasks)[0];
  if (!latestBatch) {
    return taskType === TaskType.VIDEO
      ? '选择模型、比例与时长后，生成结果会按时间展示。'
      : taskType === TaskType.AUDIO
      ? '选择音乐风格、歌词与模型后，生成结果会按批次展示。'
      : '选择模型、尺寸与数量后，生成结果会按批次展示。';
  }

  return getBatchMeta(latestBatch);
}

function buildPublishAsset(task: Task, url: string, index = 0) {
  const normalizedUrl =
    task.type === TaskType.IMAGE ? normalizeImageDataUrl(url) : url;
  const isMulti = Boolean(task.result?.urls && task.result.urls.length > 1);
  const title = getFeedTitle(task);

  return {
    id: `task-${task.id}${isMulti ? `-${index + 1}` : ''}`,
    assetId: `task-${task.id}${isMulti ? `-${index + 1}` : ''}`,
    type: task.type === TaskType.VIDEO ? 'video' : 'image',
    name: isMulti ? `${title} ${index + 1}` : title,
    source: task.type === TaskType.VIDEO ? 'ai-video' : 'ai-image',
    provider: task.params.model || 'TrueGrowth',
    url: normalizedUrl,
    thumbnailUrl:
      task.result?.thumbnailUrls?.[index] ||
      task.result?.previewImageUrl ||
      normalizedUrl,
    durationMs:
      typeof task.result?.duration === 'number'
        ? task.result.duration * 1000
        : undefined,
    width: task.result?.width,
    height: task.result?.height,
    createdAt: task.completedAt || task.createdAt || Date.now(),
    publishState: 'unpublished',
    sourceRefs: [
      {
        type: 'task',
        id: task.id,
        label: title,
      },
    ],
  };
}

function buildPublishAssets(task: Task) {
  if (task.type !== TaskType.IMAGE && task.type !== TaskType.VIDEO) {
    return [];
  }

  const urls = task.result?.urls?.length
    ? task.result.urls
    : task.result?.url
    ? [task.result.url]
    : [];
  return urls.map((url, index) => buildPublishAsset(task, url, index));
}

function CreationBatchCard({
  batch,
  onRetry,
  onDownloadBatch,
  onInsertBatch,
  onEdit,
  onRegenerate,
  onDelete,
  onDeleteBatch,
  onPublishBatch,
  onClip,
  onPreviewOpen,
}: {
  batch: CreationBatch;
  onRetry?: (taskIds: string[]) => void;
  onDownloadBatch?: (taskIds: string[]) => void;
  onInsertBatch?: (taskIds: string[]) => void;
  onEdit?: (taskId: string) => void;
  onRegenerate?: (taskId: string) => void;
  onDelete?: (taskId: string) => void;
  onDeleteBatch?: (taskIds: string[]) => void;
  onPublishBatch?: (taskIds: string[]) => void;
  onClip?: (taskId: string) => void;
  onPreviewOpen?: (previewKey: string) => void;
}) {
  const mediaItems = getBatchMediaItems(batch);
  const references = getBatchReferenceImages(batch);
  const title = getBatchTitle(batch);
  const taskIds = batch.tasks.map((task) => task.id);
  const primaryTask =
    batch.tasks.find(
      (task) => task.status === TaskStatus.COMPLETED && hasTaskResultMedia(task)
    ) ||
    batch.tasks.find((task) => task.type === batch.representative.type) ||
    batch.representative;
  const hasCompletedMedia = batch.tasks.some(
    (task) => task.status === TaskStatus.COMPLETED && hasTaskResultMedia(task)
  );
  const hasPublishableMedia = batch.tasks.some(
    (task) =>
      task.status === TaskStatus.COMPLETED &&
      (task.type === TaskType.IMAGE || task.type === TaskType.VIDEO) &&
      hasTaskResultMedia(task)
  );
  const firstCompletedVideoTask = batch.tasks.find(
    (task) =>
      task.type === TaskType.VIDEO &&
      task.status === TaskStatus.COMPLETED &&
      hasTaskResultMedia(task)
  );
  const hasFailedTask = batch.tasks.some(
    (task) => task.status === TaskStatus.FAILED
  );
  const retryableTaskIds = batch.tasks
    .filter((task) => isRetryableStatus(task.status))
    .map((task) => task.id);
  const canEdit =
    primaryTask.type === TaskType.IMAGE &&
    primaryTask.status === TaskStatus.COMPLETED &&
    Boolean(primaryTask.result?.url || primaryTask.result?.urls?.length);
  const statusMeta = getBatchStatusMeta(batch);
  const batchMeta = getBatchMeta(batch);
  const failureReason = getBatchFailureReason(batch);

  return (
    <article className="tg-creation-batch">
      <div className="tg-creation-batch__head">
        <div className="tg-creation-batch__copy">
          <h3>{title}</h3>
          <div className="tg-creation-batch__meta">
            <span>{batchMeta}</span>
            {statusMeta ? <span>{statusMeta}</span> : null}
          </div>
          {failureReason ? (
            <div className="tg-creation-batch__failure" title={failureReason}>
              失败原因：{failureReason}
            </div>
          ) : null}
        </div>

        {references.length ? (
          <div className="tg-creation-batch__refs" aria-label="历史参考图">
            {references.slice(0, 4).map((image, index) => (
              <img
                key={`${image.url}-${index}`}
                src={image.url}
                alt={image.name || `参考图 ${index + 1}`}
              />
            ))}
            {references.length > 4 ? (
              <span>+{references.length - 4}</span>
            ) : null}
          </div>
        ) : null}
      </div>

      <div className="tg-creation-batch__rail" aria-label="本批生成结果">
        {mediaItems.map((media) => {
          const isCompleted =
            media.task.status === TaskStatus.COMPLETED && Boolean(media.url);
          const isVideo = media.task.type === TaskType.VIDEO;
          const isAudio = media.task.type === TaskType.AUDIO;
          const isPublishable =
            media.task.type === TaskType.IMAGE ||
            media.task.type === TaskType.VIDEO;
          const isProcessing =
            media.task.status === TaskStatus.PROCESSING ||
            media.task.status === TaskStatus.PENDING;
          const mediaFailureReason =
            media.task.status === TaskStatus.FAILED
              ? getTaskFailureReason(media.task)
              : undefined;
          const mediaProgress = Math.max(
            0,
            Math.min(100, media.task.progress || 0)
          );
          const processingTypeLabel =
            media.task.type === TaskType.VIDEO
              ? '生成视频'
              : media.task.type === TaskType.AUDIO
              ? '生成音乐'
              : '生成图片';
          const placeholderStyle = !isCompleted
            ? getTaskPlaceholderStyle(media.task)
            : undefined;
          const mediaStyle =
            isCompleted && isVideo
              ? getTaskPlaceholderStyle(media.task)
              : placeholderStyle;
          const mediaTaskIds = [media.taskId];
          return (
            <div className="tg-creation-batch__item" key={media.id}>
              <div
                className={`tg-creation-batch__media${
                  isVideo ? ' tg-creation-batch__media--video' : ''
                }${
                  mediaFailureReason ? ' tg-creation-batch__media--failed' : ''
                }${
                  isProcessing ? ' tg-creation-batch__media--processing' : ''
                }`}
                style={mediaStyle}
              >
                <button
                  className="tg-creation-batch__media-preview"
                  type="button"
                  disabled={!isCompleted}
                  aria-label={
                    mediaFailureReason
                      ? `${getStatusLabel(
                          media.task.status
                        )}：${mediaFailureReason}`
                      : `预览 ${media.title}`
                  }
                  onClick={() => {
                    if (isCompleted) {
                      onPreviewOpen?.(media.id);
                    }
                  }}
                >
                  {isCompleted && isVideo ? (
                    <VideoPosterPreview
                      src={media.url!}
                      poster={media.thumbnailUrl}
                      alt={media.title}
                      fit="contain"
                      thumbnailSize="small"
                      playOnHover={true}
                      videoProps={{
                        muted: true,
                        loop: true,
                        playsInline: true,
                        preload: 'metadata',
                        controls: false,
                      }}
                    />
                  ) : isCompleted && isAudio ? (
                    <span className="tg-creation-batch__audio-card">
                      <span className="tg-creation-batch__audio-art">
                        {media.thumbnailUrl ? (
                          <img src={media.thumbnailUrl} alt="" />
                        ) : (
                          <Music2 size={24} />
                        )}
                      </span>
                      <span className="tg-creation-batch__audio-copy">
                        <strong>{media.title}</strong>
                        <small>
                          {typeof media.duration === 'number' &&
                          media.duration > 0
                            ? `${Math.round(media.duration)} 秒`
                            : 'AI 音乐作品'}
                        </small>
                      </span>
                      <audio
                        controls
                        preload="metadata"
                        src={media.url}
                        onClick={(event) => event.stopPropagation()}
                      />
                    </span>
                  ) : isCompleted ? (
                    <img src={media.url} alt={media.title} />
                  ) : (
                    <span
                      className={`tg-creation-batch__placeholder${
                        isProcessing
                          ? ' tg-creation-batch__placeholder--processing'
                          : ''
                      }`}
                    >
                      {isProcessing ? (
                        <span className="tg-creation-batch__processing-state tg-creation-batch__loading-stack">
                          <span className="tg-creation-batch__loading-title">
                            {processingTypeLabel}
                          </span>
                          <span
                            className="tg-creation-batch__progress tg-creation-batch__loading-progress"
                            aria-label={`生成进度 ${mediaProgress}%`}
                          >
                            <span className="tg-creation-batch__progress-ring" />
                            <span className="tg-creation-batch__progress-value">
                              {mediaProgress}%
                            </span>
                          </span>
                          <span className="tg-creation-batch__loading-status">
                            {getStatusLabel(media.task.status)}
                          </span>
                        </span>
                      ) : (
                        <span className="tg-creation-batch__placeholder-icon">
                          {isVideo ? (
                            <VideoIcon size={20} />
                          ) : isAudio ? (
                            <Music2 size={20} />
                          ) : (
                            <ImageIcon size={20} />
                          )}
                        </span>
                      )}
                      {!isProcessing ? (
                        <span className="tg-creation-batch__placeholder-label">
                          {getStatusLabel(media.task.status)}
                        </span>
                      ) : null}
                    </span>
                  )}
                </button>

                {isAudio && isCompleted ? (
                  <span className="tg-creation-batch__play" aria-hidden="true">
                    <Music2 size={22} />
                  </span>
                ) : null}
                {isCompleted ? (
                  <span className="tg-creation-batch__hover-actions">
                    <HoverTip content="预览">
                      <button
                        className="tg-creation-batch__icon"
                        type="button"
                        aria-label="预览"
                        onClick={(event) => {
                          event.stopPropagation();
                          onPreviewOpen?.(media.id);
                        }}
                      >
                        <Eye size={15} />
                      </button>
                    </HoverTip>
                    {isVideo ? (
                      <HoverTip content="去剪辑">
                        <button
                          className="tg-creation-batch__icon"
                          type="button"
                          aria-label="去剪辑"
                          onClick={(event) => {
                            event.stopPropagation();
                            onClip?.(media.taskId);
                          }}
                        >
                          <Scissors size={15} />
                        </button>
                      </HoverTip>
                    ) : null}
                    <HoverTip content="插入画布">
                      <button
                        className="tg-creation-batch__icon"
                        type="button"
                        aria-label="插入画布"
                        onClick={(event) => {
                          event.stopPropagation();
                          onInsertBatch?.(mediaTaskIds);
                        }}
                      >
                        <ImagePlus size={15} />
                      </button>
                    </HoverTip>
                    {isPublishable ? (
                      <HoverTip content="去发布">
                        <button
                          className="tg-creation-batch__icon"
                          type="button"
                          aria-label="去发布"
                          onClick={(event) => {
                            event.stopPropagation();
                            onPublishBatch?.(mediaTaskIds);
                          }}
                        >
                          <Send size={15} />
                        </button>
                      </HoverTip>
                    ) : null}
                    <HoverTip content="下载">
                      <button
                        className="tg-creation-batch__icon"
                        type="button"
                        aria-label="下载"
                        onClick={(event) => {
                          event.stopPropagation();
                          onDownloadBatch?.(mediaTaskIds);
                        }}
                      >
                        <Download size={15} />
                      </button>
                    </HoverTip>
                  </span>
                ) : null}
              </div>
            </div>
          );
        })}
      </div>

      <div className="tg-creation-batch__actions">
        {canEdit ? (
          <button type="button" onClick={() => onEdit?.(primaryTask.id)}>
            <Edit3 size={15} />
            <span>编辑</span>
          </button>
        ) : null}
        {hasFailedTask ? (
          <button
            type="button"
            onClick={() => {
              onRetry?.(
                retryableTaskIds.length ? retryableTaskIds : [primaryTask.id]
              );
            }}
          >
            <RefreshCcw size={15} />
            <span>重试</span>
          </button>
        ) : primaryTask.type === TaskType.IMAGE ? (
          <button type="button" onClick={() => onRegenerate?.(primaryTask.id)}>
            <RefreshCcw size={15} />
            <span>重新生成</span>
          </button>
        ) : null}
        {hasCompletedMedia ? (
          <button type="button" onClick={() => onDownloadBatch?.(taskIds)}>
            <Download size={15} />
            <span>下载</span>
          </button>
        ) : null}
        {hasCompletedMedia ? (
          <button type="button" onClick={() => onInsertBatch?.(taskIds)}>
            <ImagePlus size={15} />
            <span>插入画布</span>
          </button>
        ) : null}
        {firstCompletedVideoTask ? (
          <button
            type="button"
            onClick={() => onClip?.(firstCompletedVideoTask.id)}
          >
            <Scissors size={15} />
            <span>去剪辑</span>
          </button>
        ) : null}
        {hasPublishableMedia ? (
          <details className="tg-creation-batch__more">
            <summary>
              <MoreHorizontal size={15} />
              <span>更多</span>
            </summary>
            <div className="tg-creation-batch__more-menu">
              <button type="button" onClick={() => onPublishBatch?.(taskIds)}>
                <Send size={15} />
                <span>去发布</span>
              </button>
              <button
                type="button"
                className="tg-creation-batch__danger"
                onClick={() =>
                  batch.tasks.length > 1
                    ? onDeleteBatch?.(taskIds)
                    : onDelete?.(primaryTask.id)
                }
              >
                <Trash2 size={15} />
                <span>删除</span>
              </button>
            </div>
          </details>
        ) : (
          <button
            type="button"
            className="tg-creation-batch__danger"
            onClick={() =>
              batch.tasks.length > 1
                ? onDeleteBatch?.(taskIds)
                : onDelete?.(primaryTask.id)
            }
          >
            <Trash2 size={15} />
            <span>删除</span>
          </button>
        )}
      </div>
    </article>
  );
}
