/**
 * 配音素材管理页面 — V21 设计系统
 *
 * 功能：配音素材的增删改查（对接真实 API）
 * - 卡片 / 列表双布局
 * - 上传配音（音频文件 + 元信息）
 * - 试听播放（使用 file_url 真实播放）
 * - 编辑元信息（名称、描述、性别、风格标签）
 * - 删除素材
 */
import React, {
  useState,
  useRef,
  useCallback,
  useEffect,
  useMemo,
} from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
  AudioOutlined,
  PlayCircleOutlined,
  PauseCircleOutlined,
  SearchOutlined,
  PlusOutlined,
  EditOutlined,
  DeleteOutlined,
  UploadOutlined,
  UnorderedListOutlined,
  AppstoreOutlined,
  CloseOutlined,
  SoundOutlined,
  UserOutlined,
  ManOutlined,
  WomanOutlined,
  CheckOutlined,
  TagsOutlined,
  MutedOutlined,
  RobotOutlined,
  LoadingOutlined,
} from "@ant-design/icons";
import { Button, Input, Select, Modal, Tag } from "@/components/ui";
import { message, Popover, Popconfirm, Tooltip } from "antd";
import PageHead from "@/components/layout/PageHead";
import {
  getAssetsByKind,
  createAsset,
  updateAsset,
  deleteAsset,
  uploadAssetDirect,
  getAssetLibraries,
  createAssetLibrary,
  type AssetItem,
} from "@/api/assets";
import {
  type TagItem,
  getTags,
  createTag,
  tagAsset,
  untagAsset,
} from "@/api/tags";
import { synthesizeSpeech, getTTSJobStatus, saveTtsToLibrary } from "@/api/tts";
import { fetchPresetVoices, type PresetVoiceItem } from "@/api/voices";
import "./voice-materials.css";

/* ============================================================
 * 类型定义
 * ============================================================ */

type VoiceGender = "male" | "female" | "child" | "neutral";
type ViewMode = "card" | "list";

/** 标签溢出限制 */
const MAX_CARD_TAGS = 3;
const MAX_ROW_TAGS = 2;
const TAG_VARIANTS = [
  "info",
  "primary",
  "success",
  "warning",
  "error",
] as const;

/** 性别选项 */
const GENDER_OPTIONS: {
  value: VoiceGender;
  label: string;
  icon: React.ReactNode;
}[] = [
  { value: "male", label: "男声", icon: <ManOutlined /> },
  { value: "female", label: "女声", icon: <WomanOutlined /> },
  { value: "child", label: "童声", icon: <UserOutlined /> },
  { value: "neutral", label: "中性", icon: <SoundOutlined /> },
];

/** 前端配音素材数据模型（从 AssetItem 映射） */
interface VoiceMaterial {
  id: string;
  name: string;
  description: string;
  gender: VoiceGender;
  tagIds: string[];
  fileName: string;
  fileSize: number;
  duration: number;
  mimeType: string;
  createdAt: string;
  fileUrl?: string;
}

/* ============================================================
 * 数据映射：AssetItem ↔ VoiceMaterial
 * ============================================================ */

/** 后端 AssetItem → 前端 VoiceMaterial */
const mapAssetToMaterial = (asset: AssetItem): VoiceMaterial => {
  const meta = asset.metadata || {};

  return {
    id: asset.id,
    name: asset.name,
    description: (meta.description as string) || "",
    gender: (meta.gender as VoiceGender) || "neutral",
    tagIds: Array.isArray(asset.tag_ids) ? asset.tag_ids : [],
    fileName: asset.storage_key?.split("/").pop() || asset.name,
    fileSize: asset.file_size || 0,
    duration: (meta.duration as number) || 0,
    mimeType: asset.mime_type || "audio/mpeg",
    createdAt: asset.created_at || new Date().toISOString(),
    fileUrl: asset.file_url,
  };
};

/** 前端表单数据 → 后端 metadata（标签走独立 API，不再写 metadata.style） */
const buildMetadata = (data: {
  gender: VoiceGender;
  description: string;
  duration?: number;
}): Record<string, unknown> => ({
  gender: data.gender,
  description: data.description,
  duration: data.duration || 0,
});

/* ============================================================
 * 工具函数
 * ============================================================ */

const genderLabel = (g: VoiceGender) =>
  GENDER_OPTIONS.find((o) => o.value === g)?.label ?? g;

const genderIcon = (g: VoiceGender) =>
  GENDER_OPTIONS.find((o) => o.value === g)?.icon ?? null;

const genderClass = (g: VoiceGender) => `vmat-gender--${g}`;

const formatDuration = (seconds: number): string => {
  const m = Math.floor(seconds / 60);
  const s = Math.floor(seconds % 60);
  return `${m}:${s.toString().padStart(2, "0")}`;
};

const formatFileSize = (bytes: number): string => {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};

const formatDate = (iso: string): string =>
  new Date(iso).toLocaleDateString("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  });

/** 获取音频文件时长（秒） */
const getAudioDuration = (file: File): Promise<number> => {
  return new Promise((resolve) => {
    const audio = new Audio();
    const url = URL.createObjectURL(file);
    audio.addEventListener("loadedmetadata", () => {
      resolve(audio.duration);
      URL.revokeObjectURL(url);
    });
    audio.addEventListener("error", () => {
      resolve(0);
      URL.revokeObjectURL(url);
    });
    audio.src = url;
  });
};

/* ============================================================
 * 标签选择器组件（支持自定义新增 + 移除）
 * ============================================================ */

interface TagSelectorProps {
  /** 已选标签 ID 列表 */
  value: string[];
  onChange: (tagIds: string[]) => void;
  /** 所有可用标签（来自 API） */
  tags: TagItem[];
  /** 标签 ID → TagItem 映射 */
  tagMap: Map<string, TagItem>;
  /** 创建新标签，返回带 ID 的 TagItem */
  onCreateTag: (name: string) => Promise<TagItem>;
  placeholder?: string;
}

const TagSelector: React.FC<TagSelectorProps> = ({
  value,
  onChange,
  tags,
  tagMap,
  onCreateTag,
  placeholder = "输入标签后回车添加",
}) => {
  const [inputVal, setInputVal] = useState("");
  const [showSuggestions, setShowSuggestions] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  /** 按名称查找已有标签（大小写不敏感） */
  const findTagByName = useCallback(
    (name: string) =>
      tags.find((t) => t.name.toLowerCase() === name.toLowerCase()),
    [tags],
  );

  /** 去重添加标签（按 ID） */
  const addTagId = useCallback(
    (tagId: string) => {
      if (value.includes(tagId)) return;
      onChange([...value, tagId]);
      setInputVal("");
      setShowSuggestions(false);
    },
    [value, onChange],
  );

  /** 输入自定义标签名：若已存在则直接选，否则创建新标签 */
  const addTagByName = useCallback(
    async (name: string) => {
      const trimmed = name.trim();
      if (!trimmed) return;
      const existing = findTagByName(trimmed);
      if (existing) {
        addTagId(existing.id);
      } else {
        try {
          const created = await onCreateTag(trimmed);
          addTagId(created.id);
        } catch {
          /* 创建失败静默忽略 */
        }
      }
    },
    [findTagByName, addTagId, onCreateTag],
  );

  const removeTagId = useCallback(
    (tagId: string) => {
      onChange(value.filter((t) => t !== tagId));
    },
    [value, onChange],
  );

  /** 输入补全建议（排除已选） */
  const suggestions = useMemo(() => {
    if (!inputVal.trim()) return [];
    const lower = inputVal.toLowerCase();
    return tags.filter(
      (t) => t.name.toLowerCase().includes(lower) && !value.includes(t.id),
    );
  }, [inputVal, tags, value]);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") {
      e.preventDefault();
      if (suggestions.length > 0) {
        addTagId(suggestions[0].id);
      } else {
        addTagByName(inputVal);
      }
    } else if (e.key === "Backspace" && !inputVal && value.length > 0) {
      removeTagId(value[value.length - 1]);
    }
  };

  return (
    <div className="vmat-tag-selector-wrapper">
      <div
        className="vmat-tag-selector"
        onClick={() => inputRef.current?.focus()}
      >
        {value.map((tagId) => (
          <Tag
            key={tagId}
            variant="info"
            closable
            onClose={() => removeTagId(tagId)}
          >
            {tagMap.get(tagId)?.name ?? tagId}
          </Tag>
        ))}
        <input
          ref={inputRef}
          className="vmat-tag-selector-input"
          value={inputVal}
          onChange={(e) => {
            setInputVal(e.target.value);
            setShowSuggestions(true);
          }}
          onFocus={() => setShowSuggestions(true)}
          onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
          onKeyDown={handleKeyDown}
          placeholder={value.length === 0 ? placeholder : ""}
        />
      </div>

      {/* 自动补全下拉 */}
      {showSuggestions && suggestions.length > 0 && (
        <div className="vmat-tag-suggestions">
          {suggestions.slice(0, 6).map((tag) => (
            <button
              key={tag.id}
              type="button"
              className="vmat-tag-suggestion-item"
              onMouseDown={(e) => {
                e.preventDefault();
                addTagId(tag.id);
              }}
            >
              {tag.name}
            </button>
          ))}
        </div>
      )}

      {/* 已有标签快捷选择 */}
      {tags.length > 0 && (
        <div className="vmat-tag-selector-presets">
          {tags.map((tag) => {
            const isSelected = value.includes(tag.id);
            return (
              <button
                key={tag.id}
                type="button"
                className={`vmat-tag-selector-preset${isSelected ? " selected" : ""}`}
                onClick={() => {
                  if (isSelected) removeTagId(tag.id);
                  else addTagId(tag.id);
                }}
              >
                {isSelected && (
                  <CheckOutlined style={{ fontSize: 10, marginRight: 2 }} />
                )}
                {tag.name}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
};

/* ============================================================
 * 上传 / 编辑 表单
 * ============================================================ */

interface MaterialFormProps {
  initial?: VoiceMaterial;
  onSubmit: (
    data: Omit<VoiceMaterial, "id" | "createdAt"> & { file?: File },
  ) => void;
  onCancel: () => void;
  loading?: boolean;
  uploadProgress?: number | null;
  tags?: TagItem[];
  tagMap?: Map<string, TagItem>;
  onCreateTag?: (name: string) => Promise<TagItem>;
}

const MaterialForm: React.FC<MaterialFormProps> = ({
  initial,
  onSubmit,
  onCancel,
  loading,
  uploadProgress,
  tags = [],
  tagMap = new Map(),
  onCreateTag,
}) => {
  const [name, setName] = useState(initial?.name ?? "");
  const [description, setDescription] = useState(initial?.description ?? "");
  const [gender, setGender] = useState<VoiceGender>(
    initial?.gender ?? "female",
  );
  const [selectedTagIds, setSelectedTagIds] = useState<string[]>(
    initial?.tagIds ?? [],
  );
  const [file, setFile] = useState<File | undefined>(undefined);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = () => {
    if (!name.trim()) return;
    if (!initial && !file) return;
    onSubmit({
      name: name.trim(),
      description: description.trim(),
      gender,
      tagIds: selectedTagIds,
      fileName: file?.name ?? initial?.fileName ?? "",
      fileSize: file?.size ?? initial?.fileSize ?? 0,
      duration: initial?.duration ?? 0,
      mimeType: file?.type ?? initial?.mimeType ?? "audio/mpeg",
      file,
    });
  };

  return (
    <div className="vmat-form">
      {/* 音频文件上传（编辑模式不显示） */}
      {!initial && (
        <div className="vmat-form-field">
          <label className="vmat-form-label">音频文件 *</label>
          <div
            className="vmat-upload-zone"
            onClick={() => fileInputRef.current?.click()}
            onDragOver={(e) => e.preventDefault()}
            onDrop={(e) => {
              e.preventDefault();
              const f = e.dataTransfer.files[0];
              if (f?.type.startsWith("audio/")) setFile(f);
            }}
          >
            <input
              ref={fileInputRef}
              type="file"
              accept="audio/*"
              style={{ display: "none" }}
              onChange={(e) => {
                const f = e.target.files?.[0];
                if (f) setFile(f);
              }}
            />
            {file ? (
              <div className="vmat-upload-selected">
                <SoundOutlined className="vmat-upload-icon" />
                <span className="vmat-upload-filename">{file.name}</span>
                <span className="vmat-upload-filesize">
                  {formatFileSize(file.size)}
                </span>
                <button
                  type="button"
                  className="vmat-upload-clear"
                  onClick={(e) => {
                    e.stopPropagation();
                    setFile(undefined);
                  }}
                >
                  <CloseOutlined />
                </button>
              </div>
            ) : (
              <div className="vmat-upload-placeholder">
                <UploadOutlined className="vmat-upload-icon" />
                <p>点击或拖拽音频文件到此处</p>
                <span>支持 MP3、WAV、AAC、FLAC 等格式</span>
              </div>
            )}
          </div>
          {/* 上传进度条 */}
          {uploadProgress !== null && uploadProgress !== undefined && (
            <div className="vmat-upload-progress">
              <div
                className="vmat-upload-progress-bar"
                style={{ width: `${uploadProgress}%` }}
              />
              <span className="vmat-upload-progress-text">
                {uploadProgress}%
              </span>
            </div>
          )}
        </div>
      )}

      {/* 名称 */}
      <div className="vmat-form-field">
        <label className="vmat-form-label">名称 *</label>
        <Input
          placeholder="输入配音素材名称"
          value={name}
          onChange={(e) => setName(e.target.value)}
          maxLength={50}
        />
      </div>

      {/* 音色描述 */}
      <div className="vmat-form-field">
        <label className="vmat-form-label">音色描述</label>
        <Input.TextArea
          placeholder="描述音色特点，如：适合产品宣传的男声配音..."
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          rows={3}
          maxLength={200}
        />
      </div>

      {/* 性别 */}
      <div className="vmat-form-field">
        <label className="vmat-form-label">性别</label>
        <div className="vmat-gender-group">
          {GENDER_OPTIONS.map((opt) => (
            <button
              key={opt.value}
              type="button"
              className={`vmat-gender-btn${gender === opt.value ? " active" : ""} ${genderClass(opt.value)}`}
              onClick={() => setGender(opt.value)}
            >
              {opt.icon}
              {opt.label}
            </button>
          ))}
        </div>
      </div>

      {/* 风格标签 */}
      <div className="vmat-form-field">
        <label className="vmat-form-label">风格标签</label>
        <TagSelector
          value={selectedTagIds}
          onChange={setSelectedTagIds}
          tags={tags}
          tagMap={tagMap}
          onCreateTag={onCreateTag ?? (async () => ({ id: "", name: "" }))}
        />
      </div>

      {/* 操作按钮 */}
      <div className="vmat-form-actions">
        <Button buttonType="ghost" buttonSize="md" onClick={onCancel}>
          取消
        </Button>
        <Button
          buttonType="primary"
          buttonSize="md"
          onClick={handleSubmit}
          loading={loading}
          disabled={!name.trim() || (!initial && !file)}
        >
          {initial ? "保存修改" : "上传"}
        </Button>
      </div>
    </div>
  );
};

/* ============================================================
 * 卡片组件
 * ============================================================ */

interface VoiceCardProps {
  material: VoiceMaterial;
  isPlaying: boolean;
  currentTime: number;
  isSelected: boolean;
  batchMode: boolean;
  volume: number;
  tagMap: Map<string, TagItem>;
  onPlay: () => void;
  onPause: () => void;
  onSeek: (time: number) => void;
  onEdit: () => void;
  onDelete: () => void;
  onToggleSelect: (id: string) => void;
  onVolumeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onToggleMute: () => void;
}

const VoiceMaterialCard: React.FC<VoiceCardProps> = ({
  material,
  isPlaying,
  currentTime,
  isSelected,
  batchMode,
  volume,
  tagMap,
  onPlay,
  onPause,
  onSeek,
  onEdit,
  onDelete,
  onToggleSelect,
  onVolumeChange,
  onToggleMute,
}) => {
  const progressRef = useRef<HTMLDivElement>(null);

  const handleProgressMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!progressRef.current) return;
    e.preventDefault();
    const doSeek = (ev: MouseEvent) => {
      if (!progressRef.current) return;
      const rect = progressRef.current.getBoundingClientRect();
      const percent = Math.max(
        0,
        Math.min(1, (ev.clientX - rect.left) / rect.width),
      );
      onSeek(percent * material.duration);
    };
    doSeek(e.nativeEvent);
    const handleMove = (ev: MouseEvent) => doSeek(ev);
    const handleUp = () => {
      document.removeEventListener("mousemove", handleMove);
      document.removeEventListener("mouseup", handleUp);
    };
    document.addEventListener("mousemove", handleMove);
    document.addEventListener("mouseup", handleUp);
  };

  const progress =
    material.duration > 0 ? (currentTime / material.duration) * 100 : 0;

  const handleCardClick = () => {
    if (batchMode) {
      onToggleSelect(material.id);
    }
  };

  return (
    <div
      className={`vmat-card ${genderClass(material.gender)}${isPlaying ? " playing" : ""}${isSelected ? " selected" : ""}${batchMode ? " batch-mode" : ""}`}
      onClick={handleCardClick}
    >
      {/* 批量选择 checkbox */}
      {(batchMode || isSelected) && (
        <div
          className={`vmat-card-checkbox vmat-checkbox${isSelected ? " checked" : ""}`}
          onClick={(e) => {
            e.stopPropagation();
            onToggleSelect(material.id);
          }}
        >
          {isSelected && <CheckOutlined />}
        </div>
      )}

      {/* 操作按钮 */}
      <div className="vmat-card-actions">
        <button
          type="button"
          className="vmat-card-action-btn"
          onClick={(e) => {
            e.stopPropagation();
            onEdit();
          }}
          title="编辑"
        >
          <EditOutlined />
        </button>
        <button
          type="button"
          className="vmat-card-action-btn vmat-card-action-btn--danger"
          onClick={(e) => {
            e.stopPropagation();
            onDelete();
          }}
          title="删除"
        >
          <DeleteOutlined />
        </button>
      </div>

      {/* 头部：图标 + 名称 + 性别 */}
      <div className="vmat-card-header">
        <div className="vmat-card-avatar">
          <AudioOutlined />
        </div>
        <div className="vmat-card-title-area">
          <h4 className="vmat-card-name" title={material.name}>
            {material.name}
          </h4>
          <span className={`vmat-card-gender ${genderClass(material.gender)}`}>
            {genderIcon(material.gender)}
            {genderLabel(material.gender)}
          </span>
        </div>
      </div>

      {/* 描述 */}
      {material.description && (
        <p className="vmat-card-desc">{material.description}</p>
      )}

      {/* 标签 */}
      <div className="vmat-card-tags">
        {material.tagIds.length === 0 ? (
          <span
            className="vmat-tag-empty"
            onClick={(e) => {
              e.stopPropagation();
              onEdit();
            }}
          >
            添加标签
          </span>
        ) : (
          <>
            {material.tagIds.slice(0, MAX_CARD_TAGS).map((tagId, i) => (
              <Tag key={tagId} variant={TAG_VARIANTS[i % TAG_VARIANTS.length]}>
                {tagMap.get(tagId)?.name ?? tagId}
              </Tag>
            ))}
            {material.tagIds.length > MAX_CARD_TAGS && (
              <Tooltip
                title={material.tagIds
                  .slice(MAX_CARD_TAGS)
                  .map((id) => tagMap.get(id)?.name ?? id)
                  .join("、")}
              >
                <Tag className="vmat-tag-overflow">
                  +{material.tagIds.length - MAX_CARD_TAGS}
                </Tag>
              </Tooltip>
            )}
          </>
        )}
      </div>

      {/* 元信息 */}
      <div className="vmat-card-meta">
        <span>{formatDuration(material.duration)}</span>
        <span>{formatFileSize(material.fileSize)}</span>
        <span>{formatDate(material.createdAt)}</span>
      </div>

      {/* 播放控制 */}
      <div className="vmat-card-player">
        <button
          type="button"
          className="vmat-play-btn"
          onClick={(e) => {
            e.stopPropagation();
            isPlaying ? onPause() : onPlay();
          }}
          disabled={!material.fileUrl}
        >
          {isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
        </button>
        <div
          ref={progressRef}
          className="vmat-progress"
          onMouseDown={handleProgressMouseDown}
        >
          <div
            className="vmat-progress-bar"
            style={{ width: `${progress}%` }}
          />
          {isPlaying && (
            <div
              className="vmat-progress-thumb"
              style={{ left: `${progress}%` }}
            />
          )}
        </div>
        <span className="vmat-time">
          {isPlaying
            ? formatDuration(currentTime)
            : formatDuration(material.duration)}
        </span>
        {/* 音量控制 */}
        <div className="vmat-volume">
          <button
            type="button"
            className="vmat-volume-btn"
            onClick={(e) => {
              e.stopPropagation();
              onToggleMute();
            }}
            title={volume === 0 ? "取消静音" : "静音"}
          >
            {volume === 0 ? <MutedOutlined /> : <SoundOutlined />}
          </button>
          <input
            type="range"
            className="vmat-volume-slider"
            min={0}
            max={1}
            step={0.05}
            value={volume}
            onChange={(e) => {
              e.stopPropagation();
              onVolumeChange(e);
            }}
            onClick={(e) => e.stopPropagation()}
          />
        </div>
      </div>
    </div>
  );
};

/* ============================================================
 * 列表行组件
 * ============================================================ */

interface VoiceRowProps {
  material: VoiceMaterial;
  isPlaying: boolean;
  currentTime: number;
  isSelected: boolean;
  batchMode: boolean;
  tagMap: Map<string, TagItem>;
  onPlay: () => void;
  onPause: () => void;
  onSeek: (time: number) => void;
  onEdit: () => void;
  onDelete: () => void;
  onToggleSelect: (id: string) => void;
}

const VoiceMaterialRow: React.FC<VoiceRowProps> = ({
  material,
  isPlaying,
  currentTime,
  isSelected,
  batchMode,
  tagMap,
  onPlay,
  onPause,
  onSeek,
  onEdit,
  onDelete,
  onToggleSelect,
}) => {
  const progressRef = useRef<HTMLDivElement>(null);

  const handleProgressMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!progressRef.current) return;
    e.preventDefault();
    const doSeek = (ev: MouseEvent) => {
      if (!progressRef.current) return;
      const rect = progressRef.current.getBoundingClientRect();
      const percent = Math.max(
        0,
        Math.min(1, (ev.clientX - rect.left) / rect.width),
      );
      onSeek(percent * material.duration);
    };
    doSeek(e.nativeEvent);
    const handleMove = (ev: MouseEvent) => doSeek(ev);
    const handleUp = () => {
      document.removeEventListener("mousemove", handleMove);
      document.removeEventListener("mouseup", handleUp);
    };
    document.addEventListener("mousemove", handleMove);
    document.addEventListener("mouseup", handleUp);
  };

  const progress =
    material.duration > 0 ? (currentTime / material.duration) * 100 : 0;

  return (
    <div
      className={`vmat-row ${genderClass(material.gender)}${isPlaying ? " playing" : ""}${isSelected ? " selected" : ""}${batchMode ? " batch-mode" : ""}`}
    >
      {/* 批量选择 checkbox */}
      {(batchMode || isSelected) && (
        <div
          className={`vmat-row-checkbox vmat-checkbox${isSelected ? " checked" : ""}`}
          onClick={(e) => {
            e.stopPropagation();
            onToggleSelect(material.id);
          }}
        >
          {isSelected && <CheckOutlined />}
        </div>
      )}

      {/* 播放按钮 */}
      <button
        type="button"
        className="vmat-row-play"
        onClick={(e) => {
          e.stopPropagation();
          isPlaying ? onPause() : onPlay();
        }}
        disabled={!material.fileUrl}
      >
        {isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
      </button>

      {/* 名称 + 描述 */}
      <div className="vmat-row-info">
        <h4 className="vmat-row-name">{material.name}</h4>
        {material.description && (
          <p className="vmat-row-desc">{material.description}</p>
        )}
      </div>

      {/* 性别 */}
      <span className={`vmat-row-gender ${genderClass(material.gender)}`}>
        {genderIcon(material.gender)}
        {genderLabel(material.gender)}
      </span>

      {/* 标签 */}
      <div className="vmat-row-tags">
        {material.tagIds.length === 0 ? (
          <span className="vmat-tag-empty" onClick={() => onEdit()}>
            添加标签
          </span>
        ) : (
          <>
            {material.tagIds.slice(0, MAX_ROW_TAGS).map((tagId, i) => (
              <Tag key={tagId} variant={TAG_VARIANTS[i % TAG_VARIANTS.length]}>
                {tagMap.get(tagId)?.name ?? tagId}
              </Tag>
            ))}
            {material.tagIds.length > MAX_ROW_TAGS && (
              <Tooltip
                title={material.tagIds
                  .slice(MAX_ROW_TAGS)
                  .map((id) => tagMap.get(id)?.name ?? id)
                  .join("、")}
              >
                <Tag className="vmat-tag-overflow">
                  +{material.tagIds.length - MAX_ROW_TAGS}
                </Tag>
              </Tooltip>
            )}
          </>
        )}
      </div>

      {/* 进度条（可拖拽） */}
      <div
        ref={progressRef}
        className="vmat-row-progress"
        onMouseDown={handleProgressMouseDown}
      >
        <div
          className="vmat-row-progress-bar"
          style={{ width: `${progress}%` }}
        />
        {isPlaying && (
          <div
            className="vmat-progress-thumb"
            style={{ left: `${progress}%` }}
          />
        )}
      </div>

      {/* 时长 */}
      <span className="vmat-row-time">
        {isPlaying
          ? formatDuration(currentTime)
          : formatDuration(material.duration)}
      </span>

      {/* 文件大小 */}
      <span className="vmat-row-size">{formatFileSize(material.fileSize)}</span>

      {/* 操作 */}
      <div className="vmat-row-actions">
        <button
          type="button"
          className="vmat-row-action-btn"
          onClick={(e) => {
            e.stopPropagation();
            onEdit();
          }}
          title="编辑"
        >
          <EditOutlined />
        </button>
        <button
          type="button"
          className="vmat-row-action-btn vmat-row-action-btn--danger"
          onClick={(e) => {
            e.stopPropagation();
            onDelete();
          }}
          title="删除"
        >
          <DeleteOutlined />
        </button>
      </div>
    </div>
  );
};

/* ============================================================
 * 主组件
 * ============================================================ */

const VoiceMaterialLibrary: React.FC = () => {
  const queryClient = useQueryClient();

  // ── 获取 voice 类型素材库（用于上传） ──────────────────────
  const { data: libraries = [] } = useQuery({
    queryKey: ["asset-libraries"],
    queryFn: getAssetLibraries,
    staleTime: 60_000,
  });

  const voiceLibrary = useMemo(
    () => libraries.find((lib) => lib.kind === "voice"),
    [libraries],
  );

  // 自动创建 voice 素材库（如果不存在）
  const createLibMutation = useMutation({
    mutationFn: () => createAssetLibrary({ name: "配音素材库", kind: "voice" }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["asset-libraries"] });
    },
  });

  // 页面加载时，如果没有 voice 库则创建
  useEffect(() => {
    if (libraries.length > 0 && !voiceLibrary && !createLibMutation.isPending) {
      createLibMutation.mutate();
    }
  }, [libraries, voiceLibrary, createLibMutation]);

  // ── 获取标签列表 ───────────────────────────────────────────
  const { data: tags = [] } = useQuery({
    queryKey: ["tags"],
    queryFn: getTags,
    staleTime: 60_000,
  });

  /** 标签 ID → TagItem 映射（用于卡片/行渲染） */
  const tagMap = useMemo(() => {
    const m = new Map<string, TagItem>();
    tags.forEach((t) => m.set(t.id, t));
    return m;
  }, [tags]);

  /** 创建标签 mutation（供 TagSelector 调用） */
  const createTagMutation = useMutation({
    mutationFn: (name: string) => createTag(name),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["tags"] });
    },
  });

  /** 创建标签并返回 TagItem（供 TagSelector 使用） */
  const handleCreateTag = useCallback(
    async (name: string): Promise<TagItem> => {
      return createTagMutation.mutateAsync(name);
    },
    [createTagMutation],
  );

  // ── 视图状态 ──────────────────────────────────────────────
  const [viewMode, setViewMode] = useState<ViewMode>("card");
  const [searchText, setSearchText] = useState("");
  const [filterGender, setFilterGender] = useState<string>("all");
  const [filterTagId, setFilterTagId] = useState<string>("all");

  // ── 获取配音素材列表（筛选参数透传后端） ─────────────────
  const filterKeyword = searchText.trim() || undefined;
  const filterGenderParam = filterGender !== "all" ? filterGender : undefined;
  const filterTagIdsParam = filterTagId !== "all" ? [filterTagId] : undefined;

  const { data: assets = [], isLoading } = useQuery({
    queryKey: [
      "assets",
      "voice",
      {
        keyword: filterKeyword,
        gender: filterGenderParam,
        tag_ids: filterTagIdsParam,
      },
    ],
    queryFn: () =>
      getAssetsByKind("voice", {
        keyword: filterKeyword,
        gender: filterGenderParam,
        tag_ids: filterTagIdsParam,
      }),
    staleTime: 30_000,
  });

  const materials: VoiceMaterial[] = useMemo(
    () => assets.map(mapAssetToMaterial),
    [assets],
  );

  // ── 获取预设音色列表（AI 配音用） ─────────────────────────
  const { data: presetVoicesData } = useQuery({
    queryKey: ["preset-voices"],
    queryFn: fetchPresetVoices,
    staleTime: 60_000,
  });
  const presetVoices: PresetVoiceItem[] = presetVoicesData?.items ?? [];

  // ── 上传 mutation ─────────────────────────────────────────
  const uploadMutation = useMutation({
    mutationFn: async (data: {
      file: File;
      name: string;
      gender: VoiceGender;
      description: string;
      tagIds: string[];
    }) => {
      setUploadProgress(0);
      try {
        // 1. 获取或等待 voice library
        let lib = voiceLibrary;
        if (!lib) {
          if (createLibMutation.isPending) {
            await createLibMutation.mutateAsync();
          }
          const libs = await queryClient.fetchQuery({
            queryKey: ["asset-libraries"],
            queryFn: getAssetLibraries,
          });
          lib = libs.find((l) => l.kind === "voice");
          if (!lib) throw new Error("无法创建配音素材库");
        }

        // 2. 上传文件（带进度）
        const { storage_key } = await uploadAssetDirect({
          file: data.file,
          library_id: lib.id,
          onProgress: (p) => setUploadProgress(p),
        });

        // 3. 获取音频时长
        const duration = await getAudioDuration(data.file);

        // 4. 创建素材记录
        const asset = await createAsset({
          library_id: lib.id,
          name: data.name,
          storage_key,
          mime_type: data.file.type || "audio/mpeg",
          metadata: buildMetadata({
            gender: data.gender,
            description: data.description,
            duration,
          }),
        });

        // 5. 打标签（标签走独立 API）
        if (data.tagIds.length > 0) {
          await tagAsset(asset.id, data.tagIds);
        }
      } finally {
        setUploadProgress(null);
      }
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["assets", "voice"] });
      queryClient.invalidateQueries({ queryKey: ["tags"] });
    },
    onError: (err: Error) => {
      message.error(err.message || "上传失败，请重试");
    },
  });

  // ── 编辑 mutation ─────────────────────────────────────────
  const editMutation = useMutation({
    mutationFn: async (data: {
      id: string;
      name: string;
      gender: VoiceGender;
      description: string;
      tagIds: string[];
    }) => {
      // 1. 更新基础信息
      await updateAsset(data.id, {
        name: data.name,
        metadata: buildMetadata({
          gender: data.gender,
          description: data.description,
        }),
      });

      // 2. 对比标签差异，调用 tag/untag API
      const currentAsset = materials.find((m) => m.id === data.id);
      const oldTagIds = currentAsset?.tagIds ?? [];
      const newTagIds = data.tagIds;

      const toAdd = newTagIds.filter((id) => !oldTagIds.includes(id));
      const toRemove = oldTagIds.filter((id) => !newTagIds.includes(id));

      if (toAdd.length > 0) {
        await tagAsset(data.id, toAdd);
      }
      for (const tagId of toRemove) {
        await untagAsset(data.id, tagId);
      }
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["assets", "voice"] });
      queryClient.invalidateQueries({ queryKey: ["tags"] });
    },
  });

  // ── 删除 mutation ─────────────────────────────────────────
  const deleteMutation = useMutation({
    mutationFn: (assetId: string) => deleteAsset(assetId),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["assets", "voice"] });
    },
  });

  // ── 播放状态 ──────────────────────────────────────────────
  const [playingId, setPlayingId] = useState<string | null>(null);
  const [currentTime, setCurrentTime] = useState(0);
  const audioRef = useRef<HTMLAudioElement | null>(null);

  // ── 弹窗状态 ──────────────────────────────────────────────
  const [uploadOpen, setUploadOpen] = useState(false);
  const [editingMaterial, setEditingMaterial] = useState<VoiceMaterial | null>(
    null,
  );

  // ── 批量操作 / 上传进度 / 音量 ────────────────────────────
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const [uploadProgress, setUploadProgress] = useState<number | null>(null);
  const [volume, setVolume] = useState(0.7);
  const [pausedMaterial, setPausedMaterial] = useState<VoiceMaterial | null>(
    null,
  );
  const [batchCustomTag, setBatchCustomTag] = useState("");

  // ── AI 配音（TTS 合成）状态 ────────────────────────────────
  const [ttsOpen, setTtsOpen] = useState(false);
  const [ttsText, setTtsText] = useState("");
  const [ttsVoiceId, setTtsVoiceId] = useState<string>("");
  const [ttsSpeed, setTtsSpeed] = useState(1.0);
  const [ttsJobId, setTtsJobId] = useState<string | null>(null);
  const [ttsStatus, setTtsStatus] = useState<
    "idle" | "synthesizing" | "done" | "error"
  >("idle");
  const [ttsAudioUrl, setTtsAudioUrl] = useState<string | null>(null);
  const [ttsError, setTtsError] = useState<string | null>(null);
  const ttsTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);

  // ── 播放控制 ──────────────────────────────────────────────
  const stopPlayback = useCallback(() => {
    if (audioRef.current) {
      audioRef.current.pause();
      audioRef.current = null;
    }
    setPlayingId(null);
    setCurrentTime(0);
    setPausedMaterial(null);
  }, []);

  const startPlayback = useCallback(
    (material: VoiceMaterial) => {
      if (!material.fileUrl) return;
      stopPlayback();

      const audio = new Audio(material.fileUrl);
      audio.volume = volume;
      audioRef.current = audio;

      audio.addEventListener("timeupdate", () => {
        setCurrentTime(audio.currentTime);
      });

      audio.addEventListener("ended", () => {
        setPlayingId(null);
        setCurrentTime(0);
        audioRef.current = null;
        setPausedMaterial(null);
      });

      audio.play().catch(() => {
        audioRef.current = null;
        setPlayingId(null);
      });

      setPlayingId(material.id);
      setCurrentTime(0);
      setPausedMaterial(null);
    },
    [stopPlayback, volume],
  );

  const handlePlay = useCallback(
    (material: VoiceMaterial) => {
      if (playingId === material.id) return;
      // 恢复暂停
      if (
        pausedMaterial?.id === material.id &&
        audioRef.current &&
        audioRef.current.paused
      ) {
        audioRef.current.play().catch(() => {});
        setPlayingId(material.id);
        setPausedMaterial(null);
        return;
      }
      startPlayback(material);
    },
    [playingId, pausedMaterial, startPlayback],
  );

  const handlePause = useCallback((material?: VoiceMaterial) => {
    if (audioRef.current) {
      audioRef.current.pause();
    }
    setPlayingId(null);
    if (material) setPausedMaterial(material);
  }, []);

  const handleSeek = useCallback(
    (material: VoiceMaterial, time: number) => {
      if (audioRef.current) {
        audioRef.current.currentTime = time;
        setCurrentTime(time);
      } else {
        startPlayback(material);
        setTimeout(() => {
          if (audioRef.current) {
            audioRef.current.currentTime = time;
          }
        }, 100);
      }
    },
    [startPlayback],
  );

  const handleVolumeChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const v = parseFloat(e.target.value);
      setVolume(v);
      if (audioRef.current) audioRef.current.volume = v;
    },
    [],
  );

  const toggleMute = useCallback(() => {
    if (volume > 0) {
      setVolume(0);
      if (audioRef.current) audioRef.current.volume = 0;
    } else {
      setVolume(0.7);
      if (audioRef.current) audioRef.current.volume = 0.7;
    }
  }, [volume]);

  // 组件卸载时清理 audio
  useEffect(() => {
    return () => {
      if (audioRef.current) {
        audioRef.current.pause();
        audioRef.current = null;
      }
    };
  }, []);

  /* ── 数据操作 ──────────────────────────────────────────── */

  const handleUpload = (
    data: Omit<VoiceMaterial, "id" | "createdAt"> & { file?: File },
  ) => {
    if (!data.file) return;
    uploadMutation.mutate(
      {
        file: data.file,
        name: data.name,
        gender: data.gender,
        description: data.description,
        tagIds: data.tagIds,
      },
      {
        onSuccess: () => {
          setUploadOpen(false);
        },
      },
    );
  };

  const handleEdit = (
    data: Omit<VoiceMaterial, "id" | "createdAt"> & { file?: File },
  ) => {
    if (!editingMaterial) return;
    editMutation.mutate({
      id: editingMaterial.id,
      name: data.name,
      gender: data.gender,
      description: data.description,
      tagIds: data.tagIds,
    });
    setEditingMaterial(null);
  };

  const handleDelete = (id: string) => {
    const material = materials.find((m) => m.id === id);
    if (!material) return;
    Modal.confirm({
      title: "确认删除",
      content: `确定删除配音素材「${material.name}」吗？删除后不可恢复。`,
      okText: "删除",
      okButtonProps: { danger: true },
      cancelText: "取消",
      onOk: () => {
        if (playingId === id) stopPlayback();
        deleteMutation.mutate(id);
      },
    });
  };

  /* ── 筛选 ─────────────────────────────────────────────── */

  const filtered = React.useMemo(() => {
    let list = materials;
    if (filterGender !== "all") {
      list = list.filter((m) => m.gender === filterGender);
    }
    if (filterTagId !== "all") {
      list = list.filter((m) => m.tagIds.includes(filterTagId));
    }
    if (searchText.trim()) {
      const q = searchText.trim().toLowerCase();
      list = list.filter(
        (m) =>
          m.name.toLowerCase().includes(q) ||
          m.description.toLowerCase().includes(q) ||
          m.tagIds.some((id) =>
            tagMap.get(id)?.name?.toLowerCase().includes(q),
          ),
      );
    }
    return list;
  }, [materials, filterGender, filterTagId, searchText, tagMap]);

  /* ── 标签使用计数（药丸条展示，按 tag ID 统计） ──────────── */

  const tagCountMap = React.useMemo(() => {
    const map: Record<string, number> = {};
    materials.forEach((m) =>
      m.tagIds.forEach((id) => {
        map[id] = (map[id] || 0) + 1;
      }),
    );
    return map;
  }, [materials]);

  /* ── 批量操作 ─────────────────────────────────────────── */

  const batchMode = selectedIds.size > 0;
  const allSelected =
    filtered.length > 0 && filtered.every((m) => selectedIds.has(m.id));

  const handleToggleSelect = useCallback((id: string) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }, []);

  const handleSelectAll = useCallback(() => {
    if (allSelected) setSelectedIds(new Set());
    else setSelectedIds(new Set(filtered.map((m) => m.id)));
  }, [allSelected, filtered]);

  const handleBatchDelete = useCallback(async () => {
    const ids = Array.from(selectedIds);
    let successCount = 0;
    for (const id of ids) {
      try {
        await deleteAsset(id);
        successCount++;
      } catch {
        /* ignore individual failures */
      }
      if (playingId === id) stopPlayback();
    }
    queryClient.invalidateQueries({ queryKey: ["assets", "voice"] });
    setSelectedIds(new Set());
    message.success(`已批量删除 ${successCount}/${ids.length} 个素材`);
  }, [selectedIds, playingId, stopPlayback, queryClient]);

  const handleBatchTag = useCallback(
    async (tagId: string) => {
      const ids = Array.from(selectedIds);
      let successCount = 0;
      for (const id of ids) {
        try {
          await tagAsset(id, [tagId]);
          successCount++;
        } catch {
          /* ignore individual failures */
        }
      }
      queryClient.invalidateQueries({ queryKey: ["assets", "voice"] });
      setSelectedIds(new Set());
      const tagName = tagMap.get(tagId)?.name ?? tagId;
      if (successCount === 0) {
        message.error(`批量打标签失败，请重试`);
      } else {
        message.success(
          `已为 ${successCount}/${ids.length} 个素材添加标签「${tagName}」`,
        );
      }
    },
    [selectedIds, queryClient, tagMap],
  );

  /** 批量打标签 — 自定义输入：按名称查找或创建标签，再批量打标 */
  const handleBatchCustomTag = useCallback(
    async (name: string) => {
      // 先查找同名标签（不区分大小写）
      let existing = tags.find(
        (t) => t.name.toLowerCase() === name.toLowerCase(),
      );
      if (!existing) {
        try {
          existing = await createTagMutation.mutateAsync(name);
        } catch {
          message.error(`创建标签「${name}」失败`);
          return;
        }
      }
      await handleBatchTag(existing.id);
    },
    [tags, createTagMutation, handleBatchTag],
  );

  // ── TTS 合成处理 ─────────────────────────────────────────
  /** 开始 AI 配音合成 */
  const handleTtsSynthesize = useCallback(async () => {
    if (!ttsText.trim()) {
      message.warning("请输入要合成的文本");
      return;
    }
    setTtsError(null);
    setTtsStatus("synthesizing");
    setTtsAudioUrl(null);
    setTtsJobId(null);

    try {
      const resp = await synthesizeSpeech({
        text: ttsText.trim(),
        voice_id: ttsVoiceId || undefined,
        speed: ttsSpeed,
      });
      setTtsJobId(resp.job_id);

      // 轮询任务状态
      ttsTimerRef.current = setInterval(async () => {
        try {
          const job = await getTTSJobStatus(resp.job_id);
          if (job.status === "completed") {
            clearInterval(ttsTimerRef.current!);
            ttsTimerRef.current = null;
            setTtsStatus("done");
            setTtsAudioUrl(job.output_audio_url);
          } else if (job.status === "failed") {
            clearInterval(ttsTimerRef.current!);
            ttsTimerRef.current = null;
            setTtsStatus("error");
            setTtsError(job.error_message || "合成失败");
          }
        } catch {
          clearInterval(ttsTimerRef.current!);
          ttsTimerRef.current = null;
          setTtsStatus("error");
          setTtsError("查询合成状态失败");
        }
      }, 2000);
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "合成请求失败";
      setTtsStatus("error");
      setTtsError(msg);
    }
  }, [ttsText, ttsVoiceId, ttsSpeed]);

  /** 保存 TTS 结果到素材库 */
  const handleTtsSave = useCallback(async () => {
    if (!ttsJobId) return;
    try {
      await saveTtsToLibrary(ttsJobId, {
        name: ttsText.slice(0, 20) || "AI配音",
      });
      message.success("已保存到配音素材库");
      queryClient.invalidateQueries({ queryKey: ["assets", "voice"] });
      setTtsOpen(false);
    } catch {
      message.error("保存失败");
    }
  }, [ttsJobId, ttsText, queryClient]);

  // TTS 定时器清理
  useEffect(() => {
    return () => {
      if (ttsTimerRef.current) clearInterval(ttsTimerRef.current);
    };
  }, []);

  /* ── 渲染 ─────────────────────────────────────────────── */

  const isUploading = uploadMutation.isPending;
  const isEditing = editMutation.isPending;

  const pageActions = (
    <div className="vmat-page-actions">
      <Button
        buttonSize="sm"
        icon={<RobotOutlined />}
        onClick={() => setTtsOpen(true)}
      >
        AI配音
      </Button>
      <Button
        buttonType="primary"
        buttonSize="sm"
        icon={<PlusOutlined />}
        onClick={() => setUploadOpen(true)}
      >
        上传配音
      </Button>
    </div>
  );

  return (
    <div className="vmat-page">
      <PageHead
        title="配音素材库"
        description="管理配音音频素材，支持上传、试听、编辑元信息"
        actions={pageActions}
      />

      {/* 工具栏：搜索 + 筛选 + 视图切换 */}
      <div className="vmat-toolbar">
        <div className="vmat-toolbar-left">
          <Input
            placeholder="搜索配音素材..."
            prefix={<SearchOutlined />}
            value={searchText}
            onChange={(e) => setSearchText(e.target.value)}
            allowClear
            style={{ width: 240 }}
          />
          <Select
            value={filterGender}
            onChange={setFilterGender}
            style={{ width: 120 }}
            options={[
              { value: "all", label: "全部性别" },
              { value: "male", label: "男声" },
              { value: "female", label: "女声" },
              { value: "child", label: "童声" },
              { value: "neutral", label: "中性" },
            ]}
          />
        </div>
        <div className="vmat-toolbar-right">
          <span className="vmat-result-count">共 {filtered.length} 个素材</span>
          <div className="vmat-view-toggle">
            <button
              type="button"
              className={`vmat-view-btn${viewMode === "card" ? " active" : ""}`}
              onClick={() => setViewMode("card")}
              title="卡片视图"
            >
              <AppstoreOutlined />
            </button>
            <button
              type="button"
              className={`vmat-view-btn${viewMode === "list" ? " active" : ""}`}
              onClick={() => setViewMode("list")}
              title="列表视图"
            >
              <UnorderedListOutlined />
            </button>
          </div>
        </div>
      </div>

      {/* ── 标签筛选药丸条 ─────────────────────────────────── */}
      {tags.length > 0 && (
        <div className="vmat-tag-filter-bar">
          <button
            type="button"
            className={`vmat-filter-pill${filterTagId === "all" ? " active" : ""}`}
            onClick={() => setFilterTagId("all")}
            tabIndex={0}
          >
            全部
          </button>
          {tags.map((tag) => (
            <button
              key={tag.id}
              type="button"
              className={`vmat-filter-pill${filterTagId === tag.id ? " active" : ""}`}
              onClick={() => setFilterTagId(tag.id)}
              tabIndex={0}
            >
              {tag.name}
              <span className="vmat-filter-pill-count">
                {tagCountMap[tag.id] || 0}
              </span>
            </button>
          ))}
        </div>
      )}

      {/* 加载中 */}
      {isLoading && (
        <div className="vmat-empty">
          <div className="vmat-empty-icon">
            <AudioOutlined />
          </div>
          <h3>加载中...</h3>
        </div>
      )}

      {/* 批量操作栏 */}
      {batchMode && (
        <div className="vmat-batch-bar">
          <div className="vmat-batch-bar-left">
            <div
              className={`vmat-checkbox${allSelected ? " checked" : ""}`}
              onClick={handleSelectAll}
            >
              {allSelected && <CheckOutlined />}
            </div>
            <span className="vmat-select-all" onClick={handleSelectAll}>
              {allSelected ? "取消全选" : "全选"}
            </span>
            <span className="vmat-batch-count">
              已选择 {selectedIds.size} 项
            </span>
          </div>
          <div className="vmat-batch-bar-right">
            <Popover
              content={
                <div className="vmat-tag-popover">
                  <div className="vmat-tag-pop-input-row">
                    <Input
                      placeholder="输入自定义标签后回车"
                      value={batchCustomTag}
                      onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                        setBatchCustomTag(e.target.value)
                      }
                      onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
                        if (e.key === "Enter" && batchCustomTag.trim()) {
                          handleBatchCustomTag(batchCustomTag.trim());
                          setBatchCustomTag("");
                        }
                      }}
                    />
                  </div>
                  {tags.map((tag) => (
                    <button
                      key={tag.id}
                      type="button"
                      className="vmat-tag-pop-btn"
                      onClick={() => handleBatchTag(tag.id)}
                    >
                      {tag.name}
                    </button>
                  ))}
                </div>
              }
              title="批量打标签"
              trigger="click"
            >
              <Button
                buttonType="ghost"
                buttonSize="sm"
                icon={<TagsOutlined />}
              >
                批量打标签
              </Button>
            </Popover>
            <Popconfirm
              title={`确定删除选中的 ${selectedIds.size} 个素材？`}
              onConfirm={handleBatchDelete}
              okText="删除"
              cancelText="取消"
            >
              <Button
                buttonType="danger"
                buttonSize="sm"
                icon={<DeleteOutlined />}
              >
                批量删除
              </Button>
            </Popconfirm>
          </div>
        </div>
      )}

      {/* 内容区 */}
      {!isLoading && filtered.length > 0 && viewMode === "card" && (
        <div className="vmat-grid">
          {filtered.map((m) => (
            <VoiceMaterialCard
              key={m.id}
              material={m}
              tagMap={tagMap}
              isPlaying={playingId === m.id}
              currentTime={playingId === m.id ? currentTime : 0}
              isSelected={selectedIds.has(m.id)}
              batchMode={batchMode}
              volume={volume}
              onPlay={() => handlePlay(m)}
              onPause={() => handlePause(m)}
              onSeek={(t) => handleSeek(m, t)}
              onEdit={() => setEditingMaterial(m)}
              onDelete={() => handleDelete(m.id)}
              onToggleSelect={handleToggleSelect}
              onVolumeChange={handleVolumeChange}
              onToggleMute={toggleMute}
            />
          ))}
        </div>
      )}

      {!isLoading && filtered.length > 0 && viewMode === "list" && (
        <div className={`vmat-list${batchMode ? " batch-mode" : ""}`}>
          {/* 列表头 */}
          <div className="vmat-list-header">
            {batchMode && <span className="vmat-lh-checkbox" />}
            <span className="vmat-lh-play" />
            <span className="vmat-lh-info">名称</span>
            <span className="vmat-lh-gender">性别</span>
            <span className="vmat-lh-tags">风格</span>
            <span className="vmat-lh-progress">试听</span>
            <span className="vmat-lh-time">时长</span>
            <span className="vmat-lh-size">大小</span>
            <span className="vmat-lh-actions">操作</span>
          </div>
          {filtered.map((m) => (
            <VoiceMaterialRow
              key={m.id}
              material={m}
              tagMap={tagMap}
              isPlaying={playingId === m.id}
              currentTime={playingId === m.id ? currentTime : 0}
              isSelected={selectedIds.has(m.id)}
              batchMode={batchMode}
              onPlay={() => handlePlay(m)}
              onPause={() => handlePause(m)}
              onSeek={(t) => handleSeek(m, t)}
              onEdit={() => setEditingMaterial(m)}
              onDelete={() => handleDelete(m.id)}
              onToggleSelect={handleToggleSelect}
            />
          ))}
        </div>
      )}

      {!isLoading && filtered.length === 0 && (
        <div className="vmat-empty">
          <div className="vmat-empty-icon">
            <AudioOutlined />
          </div>
          <h3>暂无配音素材</h3>
          <p>
            {searchText || filterGender !== "all" || filterTagId !== "all"
              ? "未找到匹配的素材，试试调整筛选条件"
              : "上传音频文件，开始管理配音素材"}
          </p>
          {!searchText && filterGender === "all" && filterTagId === "all" && (
            <Button
              buttonType="primary"
              buttonSize="md"
              icon={<UploadOutlined />}
              onClick={() => setUploadOpen(true)}
            >
              上传配音
            </Button>
          )}
        </div>
      )}

      {/* 上传弹窗 */}
      <Modal
        title="上传配音素材"
        open={uploadOpen}
        onCancel={() => setUploadOpen(false)}
        footer={null}
        width={520}
        destroyOnClose
      >
        <MaterialForm
          onSubmit={handleUpload}
          onCancel={() => setUploadOpen(false)}
          loading={isUploading}
          uploadProgress={uploadProgress}
          tags={tags}
          tagMap={tagMap}
          onCreateTag={handleCreateTag}
        />
      </Modal>

      {/* 编辑弹窗 */}
      <Modal
        title="编辑配音素材"
        open={!!editingMaterial}
        onCancel={() => setEditingMaterial(null)}
        footer={null}
        width={520}
        destroyOnClose
      >
        {editingMaterial && (
          <MaterialForm
            initial={editingMaterial}
            onSubmit={handleEdit}
            onCancel={() => setEditingMaterial(null)}
            loading={isEditing}
            tags={tags}
            tagMap={tagMap}
            onCreateTag={handleCreateTag}
          />
        )}
      </Modal>

      {/* AI 配音（TTS 合成）弹窗 */}
      <Modal
        title="AI 配音"
        open={ttsOpen}
        onCancel={() => {
          setTtsOpen(false);
          if (ttsTimerRef.current) {
            clearInterval(ttsTimerRef.current);
            ttsTimerRef.current = null;
          }
          setTtsStatus("idle");
          setTtsAudioUrl(null);
          setTtsError(null);
          setTtsJobId(null);
        }}
        footer={null}
        width={560}
        destroyOnClose
      >
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          {/* 文本输入 */}
          <div>
            <label
              style={{
                fontSize: 13,
                fontWeight: 500,
                marginBottom: 6,
                display: "block",
              }}
            >
              输入文本
            </label>
            <textarea
              rows={4}
              placeholder="请输入需要转换为语音的文本内容…"
              value={ttsText}
              onChange={(e) => setTtsText(e.target.value)}
              maxLength={2000}
              style={{
                width: "100%",
                padding: "8px 12px",
                border: "1px solid var(--border-color, #d9d9d9)",
                borderRadius: 6,
                fontSize: 13,
                resize: "vertical",
                fontFamily: "inherit",
              }}
            />
            <div
              style={{
                fontSize: 11,
                color: "var(--text-tertiary, #999)",
                marginTop: 4,
                textAlign: "right",
              }}
            >
              {ttsText.length}/2000
            </div>
          </div>

          {/* 音色选择 */}
          <div>
            <label
              style={{
                fontSize: 13,
                fontWeight: 500,
                marginBottom: 6,
                display: "block",
              }}
            >
              选择音色
            </label>
            <select
              value={ttsVoiceId}
              onChange={(e) => setTtsVoiceId(e.target.value)}
              style={{
                width: "100%",
                height: 36,
                padding: "0 10px",
                border: "1px solid var(--border-color, #d9d9d9)",
                borderRadius: 6,
                fontSize: 13,
                background: "var(--bg-primary, #fff)",
              }}
            >
              <option value="">默认音色</option>
              {presetVoices.map((v) => (
                <option key={v.voice_id} value={v.voice_id}>
                  {v.name}
                </option>
              ))}
            </select>
          </div>

          {/* 语速调节 */}
          <div>
            <label
              style={{
                fontSize: 13,
                fontWeight: 500,
                marginBottom: 6,
                display: "block",
              }}
            >
              语速：{ttsSpeed.toFixed(1)}x
            </label>
            <input
              type="range"
              min={0.5}
              max={2.0}
              step={0.1}
              value={ttsSpeed}
              onChange={(e) => setTtsSpeed(parseFloat(e.target.value))}
              style={{ width: "100%" }}
            />
          </div>

          {/* 合成按钮 */}
          <Button
            buttonType="primary"
            buttonSize="md"
            icon={
              ttsStatus === "synthesizing" ? (
                <LoadingOutlined />
              ) : (
                <RobotOutlined />
              )
            }
            onClick={handleTtsSynthesize}
            disabled={ttsStatus === "synthesizing" || !ttsText.trim()}
          >
            {ttsStatus === "synthesizing" ? "合成中…" : "开始合成"}
          </Button>

          {/* 错误提示 */}
          {ttsStatus === "error" && ttsError && (
            <div
              style={{
                padding: "8px 12px",
                background: "#fff2f0",
                borderRadius: 6,
                color: "#ff4d4f",
                fontSize: 13,
              }}
            >
              {ttsError}
            </div>
          )}

          {/* 合成结果 */}
          {ttsStatus === "done" && ttsAudioUrl && (
            <div
              style={{
                padding: 12,
                background: "var(--bg-surface, #f5f5f5)",
                borderRadius: 8,
              }}
            >
              <audio
                controls
                src={ttsAudioUrl}
                style={{ width: "100%", marginBottom: 12 }}
              />
              <Button
                buttonType="primary"
                buttonSize="sm"
                icon={<PlusOutlined />}
                onClick={handleTtsSave}
              >
                保存到素材库
              </Button>
            </div>
          )}
        </div>
      </Modal>
    </div>
  );
};

export default VoiceMaterialLibrary;
