/**
 * 书籍生成模块 - Prisma 数据库存储
 */

import crypto from 'crypto';
import { prisma } from '../../models';
import { Book, BookOutline, Chapter, ChapterGenStage, BookGenStage } from './book-generator.types';
import { Prisma } from '@prisma/client';
import { generateAudio } from '../tts/tts.service';
import { callLLMWithMessages, callLLMWithTools, ChatMessage } from '../../services/llm';
import { createBookTools } from '../../services/llm/book-tools';
import { SUBSECTION_CONTENT_SYSTEM_PROMPT } from './prompts/templates';
import { countWords } from './utils';
import { cleanThinkingText } from './utils/content-cleaner';
import { advanceChapter, regenerateChapter, safeTransitionChapter } from './stage-manager';
import { mergeChapterAudios } from '../player/player.service';
import { consumeAudioMinutes } from '../subscription/subscription.service';
import { runWithContext } from '../../services/llm-context';

/**
 * 取消书籍所有章节的音频生成（将 pending/processing 任务标记为 cancelled，回退章节阶段）
 */
export async function cancelAudioGeneration(bookId: string): Promise<{ cancelledCount: number; rolledBackChapters: number[] }> {
  const chapters = await prisma.bookChapter.findMany({
    where: { bookId: Number(bookId) },
    select: { id: true, level: true },
  });

  if (chapters.length === 0) {
    return { cancelledCount: 0, rolledBackChapters: [] };
  }

  const maxLevel = Math.max(...chapters.map(c => c.level || 0));
  const leafChapterIds = chapters.filter(c => c.level === maxLevel).map(c => c.id);

  // 找出所有 pending/processing 状态的 TTS 任务
  const activeTasks = await prisma.ttsTask.findMany({
    where: {
      chapterId: { in: leafChapterIds },
      taskType: 'tts',
      status: { in: ['pending', 'processing'] },
    },
    select: { id: true, chapterId: true },
  });

  if (activeTasks.length === 0) {
    return { cancelledCount: 0, rolledBackChapters: [] };
  }

  const taskIds = activeTasks.map(t => t.id);
  const affectedChapterIds = [...new Set(activeTasks.map(t => t.chapterId))];

  // 批量标记任务为 cancelled
  await prisma.ttsTask.updateMany({
    where: { id: { in: taskIds } },
    data: { status: 'cancelled' },
  });

  // 回退受影响章节的 genStage 到 content_completed
  await prisma.bookChapter.updateMany({
    where: { id: { in: affectedChapterIds }, genStage: 'audio_generating' },
    data: { genStage: 'content_completed' },
  });

  return {
    cancelledCount: taskIds.length,
    rolledBackChapters: affectedChapterIds,
  };
}

// ============ 删除书籍 ============

/**
 * 根据所有章节状态计算书籍阶段
 * 书籍阶段 = 所有章节中最低的阶段（最落后的章节决定了书籍的进度）
 */
function computeBookGenStage(chapters: { genStage: string }[]): BookGenStage {
  if (chapters.length === 0) return 'draft';

  // 章节阶段顺序（索引越大越"后"）
  const stageOrder = ['idle', 'outline_completed', 'content_generating', 'content_completed', 'audio_generating', 'audio_completed', 'video_generating', 'video_completed', 'failed'];

  // 找出最低阶段的索引
  let minIdx = stageOrder.length; // 默认最大
  for (const ch of chapters) {
    const idx = stageOrder.indexOf(ch.genStage);
    if (idx === -1) {
      console.warn(`[BookStore] 未知章节阶段: chapterId=${(ch as any).id}, genStage="${ch.genStage}"，跳过该章节`);
      continue;
    }
    if (idx < minIdx) {
      minIdx = idx;
    }
  }

  // 全部未知 → 回退到 draft
  if (minIdx >= stageOrder.length) {
    console.warn('[BookStore] 所有章节阶段未知，回退到 draft');
    return 'draft';
  }

  // 最低阶段索引对应的阶段
  const minStage = stageOrder[minIdx];

  // 映射到书籍阶段
  // idle → outlining（待生成大纲）
  // outline_completed → outline_completed（大纲已完成，待生成内容）
  // content_generating → content_generating（正在生成内容）
  // content_completed → content_completed
  // audio_generating → audio_generating
  // audio_completed → audio_completed
  // video_generating → video_generating
  // video_completed → video_completed
  // failed → failed
  const stageMap: Record<string, BookGenStage> = {
    'idle': 'outlining',
    'outline_completed': 'outline_completed',
    'content_generating': 'content_generating',
    'content_completed': 'content_completed',
    'audio_generating': 'audio_generating',
    'audio_completed': 'audio_completed',
    'video_generating': 'video_generating',
    'video_completed': 'video_completed',
    'failed': 'failed',
  };

  return stageMap[minStage] || 'draft';
}

/**
 * 计算内容 SHA256 哈希（用于 TTS 去重）
 * 相同内容 → 相同哈希 → 不重复生成音频
 */
function computeContentHash(content: string): string {
  return crypto.createHash('sha256').update(content.trim()).digest('hex');
}

/**
 * 自动检测：当某个叶节点音频完成后，检查其所属1级章节下所有叶节点
 * 是否都已就绪，若是则自动触发音频合并到该章节。
 */
export async function tryAutoMerge(
  completedLeafId: number,
  bookId: number | null,
  leafLevel: number | null,
  leafParentId: number | null,
) {
  if (!bookId || leafLevel == null || leafLevel <= 1) return;

  try {
    // 1. 找到该叶节点所属的1级章节
    let chapterId: number | null = null;

    if (leafLevel === 2) {
      chapterId = leafParentId;
    } else if (leafLevel === 3 && leafParentId != null) {
      const parentSection = await prisma.bookChapter.findUnique({
        where: { id: leafParentId },
        select: { parentId: true, level: true },
      });
      if (parentSection && parentSection.level === 2) {
        chapterId = parentSection.parentId;
      }
    }

    if (!chapterId) return;

    // 2. 计算该书的最大层级
    const allChapters = await prisma.bookChapter.findMany({
      where: { bookId },
      select: { id: true, level: true, parentId: true, audioUrl: true, audioDuration: true },
    });

    if (allChapters.length === 0) {
      console.warn('[tryAutoMerge] 书籍无章节，跳过');
      return;
    }
    const maxLevel = Math.max(...allChapters.map(c => c.level));
    if (maxLevel <= 1) return;

    // 3. 收集属于该章节的所有叶节点
    // 构建 parentId → children 映射用于遍历子树
    const childrenMap = new Map<number, number[]>();
    const nodeMap = new Map<number, (typeof allChapters)[number]>();
    for (const ch of allChapters) {
      nodeMap.set(ch.id, ch);
      if (ch.parentId) {
        if (!childrenMap.has(ch.parentId)) childrenMap.set(ch.parentId, []);
        childrenMap.get(ch.parentId)!.push(ch.id);
      }
    }

    // BFS 收集章节下所有后代节点
    const descendantIds: number[] = [];
    const queue = [chapterId];
    while (queue.length > 0) {
      const current = queue.shift()!;
      const children = childrenMap.get(current) || [];
      for (const childId of children) {
        descendantIds.push(childId);
        queue.push(childId);
      }
    }

    // 父节点ID集合（用于判断是否是叶节点）
    const parentIdSet = new Set(allChapters.map(c => c.parentId).filter(Boolean));

    // 筛选叶节点：在章节后代中 且 不是任何节点的父节点
    const leafNodesUnderChapter = allChapters.filter(
      ch => descendantIds.includes(ch.id) && !parentIdSet.has(ch.id),
    );

    if (leafNodesUnderChapter.length === 0) return;

    // 4. 检查是否所有叶节点都有音频
    const allHaveAudio = leafNodesUnderChapter.every(
      ch => ch.audioUrl && ch.audioUrl !== '',
    );

    if (!allHaveAudio) {
      const missingCount = leafNodesUnderChapter.filter(
        ch => !ch.audioUrl || ch.audioUrl === '',
      ).length;
      console.log(
        `[AutoMerge] 章节${chapterId}: ${leafNodesUnderChapter.length - missingCount}/${leafNodesUnderChapter.length} 个叶节点音频就绪，等待剩余 ${missingCount} 个...`,
      );
      return;
    }

    // 5. 检查该章节是否已有合并音频（幂等）
    // 但如果父章节的合并音频时长与子节时长总和不匹配，需要重新合并
    const chapter = nodeMap.get(chapterId);
    if (chapter?.audioUrl && chapter.audioUrl.includes('_merged')) {
      // 检查合并音频时长是否与子节时长总和匹配
      const childDurations = leafNodesUnderChapter
        .filter(ch => ch.audioUrl)
        .map(ch => ch.audioDuration || 0);
      const totalChildDuration = childDurations.reduce((sum, d) => sum + d, 0);
      const parentDuration = chapter.audioDuration || 0;

      // 如果合并音频时长 < 子节总时长的 80%，说明合并不完整，需要重新合并
      if (parentDuration > 0 && totalChildDuration > 0 && parentDuration < totalChildDuration * 0.8) {
        console.log(`[AutoMerge] 章节${chapterId}合并音频时长(${parentDuration}s) < 子节总时长(${totalChildDuration}s)的80%，需要重新合并`);
        // 清除旧的合并音频
        await prisma.bookChapter.update({
          where: { id: chapterId },
          data: { audioUrl: '', audioDuration: 0 },
        });
      } else {
        console.log(`[AutoMerge] 章节${chapterId}已有合并音频，跳过`);
        return;
      }
    }

    // 6. 触发合并
    console.log(
      `[AutoMerge] 🎵 章节${chapterId}下所有${leafNodesUnderChapter.length}个叶节点音频已就绪，开始自动合并...`,
    );

    const mergedUrl = await mergeChapterAudios(chapterId);
    if (mergedUrl) {
      console.log(`[AutoMerge] ✅ 章节${chapterId}音频自动合并完成: ${mergedUrl}`);
    } else {
      console.warn(`[AutoMerge] ⚠️ 章节${chapterId}合并返回空结果`);
    }
  } catch (err) {
    console.error(`[AutoMerge] 自动合并检测失败:`, err);
  }
}

/**
 * 构建小节内容生成消息
 */
function buildSubsectionContentMessages(
  topic: string,
  bookDescription: string,
  chapterTitle: string,
  chapterSummary: string,
  sectionTitle: string,
  sectionSummary: string,
  subsection: any,
  writingStyle?: string
): ChatMessage[] {
  const keyPoints = typeof subsection.keyPoints === 'string'
    ? JSON.parse(subsection.keyPoints)
    : (subsection.keyPoints || []);

  // 从 bookDescription 中提取写作风格
  const styleMatch = bookDescription.match(/写作风格：([^\\n]+)/);
  const style = writingStyle || styleMatch?.[1] || '';

  return [
    { role: 'system', content: SUBSECTION_CONTENT_SYSTEM_PROMPT },
    {
      role: 'user',
      content: `书名：《${topic}》
${bookDescription || ''}
${style ? `写作风格：${style}` : ''}
章标题：${chapterTitle}
章概述：${chapterSummary || ''}
节标题：${sectionTitle}
节概述：${sectionSummary}
小节标题：${subsection.title}
小节概述：${subsection.summary || ''}
核心知识点：${keyPoints.join('、')}
预估字数：${subsection.estimatedWords || 500}字

请撰写该小节的正文内容。`,
    },
  ];
}

// ============ 类型转换 ============

function parseOutlineJson(jsonStr: string | null): BookOutline | null {
  if (!jsonStr) return null;
  try {
    return JSON.parse(jsonStr);
  } catch {
    return null;
  }
}

function chaptersFromDb(dbChapters: any[], bookId: number, excludeContent: boolean = false): Chapter[] {
  return dbChapters.map((c) => ({
    id: String(c.id),
    bookId: String(c.bookId),
    number: c.number,
    title: c.title,
    content: excludeContent ? '' : (c.content || ''),
    wordCount: c.wordCount,
    summary: c.summary || undefined,
    generatedAt: c.generatedAt || undefined,
    error: c.errorMsg || undefined,
    audioUrl: c.audioUrl || undefined,
    audioDuration: c.audioDuration || 0,
    videoUrl: c.videoUrl || undefined,
    videoDuration: c.videoDuration || undefined,
    isPublic: c.isPublic || false,
    level: c.level,        // 层级：1=章, 2=节, 3=小节
    parentId: c.parentId,  // 父节点ID（0表示章）
    genStage: c.genStage || undefined,  // 线性阶段状态
  }));
}

function outlineChapterFromDb(dbChapter: any) {
  return {
    number: dbChapter.number,
    title: dbChapter.title,
    summary: dbChapter.summary || '',
    keyPoints: dbChapter.keyPoints ? JSON.parse(dbChapter.keyPoints) : [],
    estimatedWords: dbChapter.estimatedWords,
  };
}

// ============ 存储类 ============

export class BookStore {
  /**
   * 安全解析JSON
   */
  private safeParseJson(jsonStr: string | null): any[] {
    if (!jsonStr) return [];
    try {
      return JSON.parse(jsonStr);
    } catch {
      return [];
    }
  }

  /**
   * 从数据库章节记录构建树形结构的outline
   */
  private buildOutlineFromChapters(chapters: any[]): BookOutline | null {
    if (!chapters || chapters.length === 0) return null;

    // 获取所有level=1的章
    const level1Chapters = chapters.filter(c => c.level === 1);
    if (level1Chapters.length === 0) return null;

    // 构建映射表
    const chapterMap = new Map<number, any>();
    const sectionMap = new Map<number, any>();

    chapters.forEach(c => {
      if (c.level === 1) {
        chapterMap.set(c.id, {
          number: c.number,
          title: c.title,
          summary: c.summary || '',
          keyPoints: this.safeParseJson(c.keyPoints),
          estimatedWords: c.estimatedWords,
          sections: []
        });
      } else if (c.level === 2) {
        sectionMap.set(c.id, {
          number: c.number,
          title: c.title,
          summary: c.summary || '',
          keyPoints: this.safeParseJson(c.keyPoints),
          estimatedWords: c.estimatedWords,
          subsections: []
        });
      }
    });

    // 构建节和小节的关系
    chapters.forEach(c => {
      if (c.level === 2 && c.parentId) {
        const chapter = chapterMap.get(c.parentId);
        const section = sectionMap.get(c.id);
        if (chapter && section) {
          chapter.sections.push(section);
        }
      } else if (c.level === 3 && c.parentId) {
        const section = sectionMap.get(c.parentId);
        if (section) {
          section.subsections.push({
            number: c.number,
            title: c.title,
            summary: c.summary || '',
            keyPoints: this.safeParseJson(c.keyPoints),
            estimatedWords: c.estimatedWords
          });
        }
      }
    });

    return {
      mainTheme: '',
      structureLogic: '',
      chapters: Array.from(chapterMap.values())
    };
  }

  /**
   * 创建书籍
   */
  async create(data: {
    userId?: number;
    title: string;
    subtitle?: string;
    description: string;
    targetAudience?: string;
    style?: string;
    bookScale?: string;
    totalChapters?: number;
    estimatedWords?: number;
  }): Promise<Book> {
    const book = await prisma.book.create({
      data: {
        userId: data.userId,
        title: data.title,
        subtitle: data.subtitle,
        description: data.description,
        targetAudience: data.targetAudience || '通用',
        style: data.style || '专业严谨',
        bookScale: data.bookScale || '1000',
        totalChapters: data.totalChapters ?? 10,
        estimatedWords: data.estimatedWords ?? 0,
        genStage: 'draft',
        progress: 0,
        isPublished: false, // 预发布：等书籍完成后再发布
      },
      include: { chapters: true },
    });

    return this.toBook(book);
  }

  /**
   * 获取书籍
   * @param id 书籍ID
   * @param filterPublic 是否过滤公开音频（默认false，返回所有）
   * @param userId 当前用户ID（用于判断是否所有者）
   */
  async getById(id: string, filterPublic: boolean = false, userId?: number): Promise<Book | null> {
    const book = await prisma.book.findUnique({
      where: { id: parseInt(id) },
      include: { 
        chapters: { 
          orderBy: [
            { level: 'asc' },
            { number: 'asc' }
          ] 
        } 
      },
    });
    
    if (!book) return null;
    
    // 构建树形结构的outline（从数据库章节记录构建）
    let outline: BookOutline | null = null;
    try {
      outline = this.buildOutlineFromChapters(book.chapters);
    } catch (error) {
      console.error('[BookStore] buildOutlineFromChapters 失败:', error);
    }
    
    let result = this.toBook(book, false);  // 需要返回章节content
    
    console.log('[BookStore.getById] bookScale:', result.bookScale);
    console.log('[BookStore.getById] result keys:', Object.keys(result));
    
    // 用数据库构建的outline替换outlineJson解析的
    if (outline) {
      result.outline = outline;
    }
    
    // 返回所有章节（level=1,2,3），前端需要完整数据来构建三级树形结构
    // outline中已包含完整的树形结构(章→节→小节)
    // result.chapters = result.chapters.filter((c) => c.level === 1);  // 旧代码：只返回章
    
    // 如果需要过滤公开音频
    if (filterPublic && userId) {
      const isOwner = book.userId === userId;
      result.chapters = result.chapters.filter((c) => {
        // 所有者可以看到所有音频
        if (isOwner) return true;
        // 非所有者只能看到公开的音频
        return c.audioUrl && c.isPublic === true;
      });
    }
    
    return result;
  }

  /**
   * 按 bookId + number + level 查找章节记录
   */
  async findChapter(bookId: number, number: number, level: number): Promise<{ id: number; parentId: number | null; title: string } | null> {
    const chapter = await prisma.bookChapter.findFirst({
      where: { bookId, number, level },
    });
    return chapter;
  }

  /**
   * 获取用户的所有书籍
   * @param userId 当前用户ID
   * @param includePublic 是否包含公开书籍（用于首页显示）
   */
  async getAllByUser(userId?: number, includePublic: boolean = true): Promise<Book[]> {
    let books;
    
    if (userId) {
      // 查询指定用户的书籍（包括 userId 为该用户或为 null 的书籍）
      // userId 为 null 表示"游客"创建的书籍，也返回给当前用户查看
      books = await prisma.book.findMany({
        where: {
          OR: [
            { userId }, // 自己创建的书籍
            { userId: null }, // 游客创建的书籍（兼容旧数据）
          ]
        },
        include: { chapters: true },
        orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],
      });
    } else {
      // 未登录用户或不需要用户过滤
      books = await prisma.book.findMany({
        where: includePublic ? {} : undefined,
        include: { chapters: true },
        orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],
      });
    }
    
    return books.map((b) => this.toBook(b));
  }
  
  /**
   * 获取公开的书籍列表（用于首页展示）
   * 只返回 isPublished=true 的书籍，且只返回有音频的章节
   */
  async getPublicBooks(): Promise<Book[]> {
    const books = await prisma.book.findMany({
      where: {
        isPublished: true,
        chapters: {
          some: {
            audioUrl: { not: '' },
          }
        }
      },
      include: { chapters: {
        where: { audioUrl: { not: '' } }, // 只返回有音频的章节
        orderBy: { number: 'asc' }
      }},
      orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],
    });
    return books.map((b) => this.toBook(b));
  }

  /**
   * 更新书籍
   */
  async update(id: string, data: Partial<{
    genStage: string;
    failedStage: string;
    progress: number;
    outlineJson: string;
    outline: any;
    foreword: string;
    afterword: string;
    estimatedWords: number;
    errorMsg: string;
    totalChapters: number;
    bookAnalysis: string;
    title: string;
    bookScale: string;
  }>): Promise<Book | null> {
    // 如果传入 outline 对象，转换为 outlineJson 字符串
    const updateData: any = { ...data, updatedAt: new Date() };
    if (data.outline) {
      updateData.outlineJson = JSON.stringify(data.outline);
      delete updateData.outline;
    }

    const book = await prisma.book.update({
      where: { id: parseInt(id) },
      data: updateData,
      include: { chapters: { where: { level: 1 }, orderBy: { number: 'asc' } } },
    });
    return this.toBook(book);
  }

  /**
   * 删除书籍
   */
  async delete(id: string): Promise<boolean> {
    try {
      await prisma.book.delete({ where: { id: parseInt(id) } });
      return true;
    } catch {
      return false;
    }
  }

  /**
   * 创建章节
   */
  async createChapter(data: {
    bookId: string;
    number: number;
    title: string;
    summary?: string;
    keyPoints?: string[];
    estimatedWords?: number;
  }): Promise<void> {
    await prisma.bookChapter.create({
      data: {
        bookId: parseInt(data.bookId),
        number: data.number,
        title: data.title,
        summary: data.summary,
        keyPoints: data.keyPoints ? JSON.stringify(data.keyPoints) : null,
        estimatedWords: data.estimatedWords || 5000,
        genStage: 'outline_completed',
      },
    });
  }

  /**
   * 批量创建章节（支持重新生成）
   * 使用 upsert 避免唯一约束冲突
   */
  async createChapters(bookId: string, chapters: Array<{
    number: number;
    title: string;
    summary?: string;
    keyPoints?: string[];
    estimatedWords?: number;
  }>): Promise<void> {
    const bookIdNum = parseInt(bookId);
    // 使用 upsert 避免重复创建（parentId=0表示章级别）
    for (const c of chapters) {
      const upserted = await prisma.bookChapter.upsert({
        where: {
          bookId_parentId_level_number: {
            bookId: bookIdNum,
            parentId: 0,
            level: 1,
            number: c.number,
          }
        },
        update: {
          title: c.title,
          summary: c.summary,
          keyPoints: c.keyPoints ? JSON.stringify(c.keyPoints) : null,
          estimatedWords: c.estimatedWords || 5000,
          genStage: 'outline_completed',
        } as any,
        create: {
          bookId: bookIdNum,
          parentId: 0,
          level: 1,
          number: c.number,
          title: c.title,
          summary: c.summary,
          keyPoints: c.keyPoints ? JSON.stringify(c.keyPoints) : null,
          estimatedWords: c.estimatedWords || 5000,
          genStage: 'outline_completed',
        } as any,
      });
    }
  }

  /**
   * 创建章节条目（支持任意层级：章/节/小节）
   * @param bookIdNum 书籍ID
   * @param item 节/小节数据
   * @param parentId 父节点ID（null表示章这一级）
   * @param level 层级：1=章, 2=节, 3=小节
   * @returns 创建的记录ID
   */
  async createChapterItem(bookIdNum: number, item: {
    number: number;
    title: string;
    summary?: string;
    keyPoints?: string[];
    estimatedWords?: number;
  }, parentId: number | null, level: number): Promise<number> {
    // 防御：LLM 可能返回 undefined/null 的字段
    let chapterNumber = item.number;
    if (chapterNumber == null || chapterNumber === undefined || isNaN(chapterNumber as number)) {
      const maxExisting = await prisma.bookChapter.findFirst({
        where: { bookId: bookIdNum, parentId: parentId ?? 0, level },
        orderBy: { number: 'desc' },
        select: { number: true },
      });
      chapterNumber = (maxExisting?.number ?? 0) + 1;
      console.warn(`[BookStore] chapter number missing/invalid, auto-assigned: ${chapterNumber} (bookId=${bookIdNum}, level=${level})`);
    }

    // 防御：title 也不能是 undefined
    const safeTitle = item.title || `章节${chapterNumber}`;
    if (!item.title) {
      console.warn(`[BookStore] chapter title missing, using fallback: ${safeTitle} (bookId=${bookIdNum})`);
    }

    // 使用 upsert 避免重复创建
    const upserted = await prisma.bookChapter.upsert({
      where: {
        bookId_parentId_level_number: {
          bookId: bookIdNum,
          parentId: parentId,
          level: level,
          number: chapterNumber,
        }
      },
      update: {
        title: safeTitle,
        summary: item.summary || '',
        keyPoints: item.keyPoints ? JSON.stringify(item.keyPoints) : null,
        estimatedWords: item.estimatedWords || 1000,
        genStage: 'outline_completed',
      } as any,
      create: {
        bookId: bookIdNum,
        parentId,
        level,
        number: chapterNumber,
        title: safeTitle,
        summary: item.summary || '',
        keyPoints: item.keyPoints ? JSON.stringify(item.keyPoints) : null,
        estimatedWords: item.estimatedWords || 1000,
        genStage: 'outline_completed',
      } as any,
    });
    return upserted.id;
  }

  /**
   * 批量创建章节条目
   */
  async createChapterItems(bookIdNum: number, items: Array<{
    number: number;
    title: string;
    summary?: string;
    keyPoints?: string[];
    estimatedWords?: number;
  }>, parentId: number | null, level: number): Promise<number[]> {
    const ids: number[] = [];
    for (const item of items) {
      const id = await this.createChapterItem(bookIdNum, item, parentId, level);
      ids.push(id);
    }
    return ids;
  }

  /**
   * 更新章节内容
   * @param level 可选层级过滤，避免同 number 不同 level 的章节串写
   */
  async updateChapter(bookId: string, chapterNumber: number, data: Partial<{
    content: string;
    wordCount: number;
    genStage: string;
    errorMsg: string;
  }>, level?: number): Promise<Chapter | null> {
    const where: any = {
      bookId: parseInt(bookId),
      number: chapterNumber,
    };
    if (level !== undefined) where.level = level;
    const chapter = await prisma.bookChapter.findFirst({ where });
    if (!chapter) return null;

    const updated = await prisma.bookChapter.update({
      where: { id: chapter.id },
      data: {
        ...data,
        generatedAt: data.content ? new Date() : undefined,
      },
    });

    return {
      id: String(updated.id),
      bookId: String(updated.bookId),
      number: updated.number,
      title: updated.title,
      content: updated.content || '',
      wordCount: updated.wordCount,
      genStage: updated.genStage as ChapterGenStage | undefined,
      summary: updated.summary || undefined,
      generatedAt: updated.generatedAt || undefined,
      error: (updated as any).errorMsg || undefined,
    };
  }

  /**
   * 按 ID 更新章节内容
   */
  async updateChapterById(id: number, data: Partial<{
    content: string;
    wordCount: number;
    genStage: string;
    errorMsg: string;
    contentError: string;
    audioUrl: string;
    audioDuration: number;
    lrcLyrics: string | null;
    videoUrl: string;
    videoDuration: number;
  }>): Promise<Chapter | null> {
    // 转换 errorMsg -> contentError (Prisma字段名)
    const prismaData: any = { ...data };
    if ('errorMsg' in prismaData) {
      prismaData.contentError = prismaData.errorMsg;
      delete prismaData.errorMsg;
    }

    const updated = await prisma.bookChapter.update({
      where: { id },
      data: {
        ...prismaData,
        generatedAt: data.content ? new Date() : undefined,
      },
    });

    return {
      id: String(updated.id),
      bookId: String(updated.bookId),
      number: updated.number,
      title: updated.title,
      content: updated.content || '',
      wordCount: updated.wordCount,
      genStage: updated.genStage as ChapterGenStage | undefined,
      summary: updated.summary || undefined,
      generatedAt: updated.generatedAt || undefined,
      error: updated.contentError || undefined,
      audioUrl: updated.audioUrl || undefined,
      audioDuration: updated.audioDuration || 0,
      videoUrl: updated.videoUrl || undefined,
      videoDuration: updated.videoDuration || undefined,
    };
  }

  /**
   * 获取书籍的章节
   */
  async getChapters(bookId: string): Promise<Chapter[]> {
    const chapters = await prisma.bookChapter.findMany({
      where: { bookId: parseInt(bookId) },
      orderBy: { number: 'asc' },
    });
    return chaptersFromDb(chapters, parseInt(bookId));
  }

  /**
   * 获取书籍的完整章节树（章→节→小节）
   * 按 level 和 number 排序
   */
  async getChapterTree(bookId: string): Promise<any[]> {
    const chapters = await prisma.bookChapter.findMany({
      where: { bookId: parseInt(bookId) },
      orderBy: [{ level: 'asc' }, { number: 'asc' }],
    });
    return chapters;
  }

  /**
   * 统计书籍完成章节数
   */
  async countCompletedChapters(bookId: string): Promise<number> {
    return prisma.bookChapter.count({
      where: {
        bookId: parseInt(bookId),
        genStage: 'video_completed',
      },
    });
  }

  /**
   * 发布书籍（将 isPublished 设为 true，同时公开所有章节）
   */
  async publishAlbum(bookId: string): Promise<void> {
    await prisma.$transaction([
      prisma.book.update({
        where: { id: parseInt(bookId) },
        data: { isPublished: true },
      }),
      // 公开所有有音频的章节
      prisma.bookChapter.updateMany({
        where: {
          bookId: parseInt(bookId),
          audioUrl: { not: '' },
        },
        data: { isPublic: true },
      }),
    ]);
  }

  /**
   * 取消发布书籍（将 isPublished 设为 false，同时取消公开所有章节）
   */
  async unpublishAlbum(bookId: string): Promise<void> {
    await prisma.$transaction([
      prisma.book.update({
        where: { id: parseInt(bookId) },
        data: { isPublished: false },
      }),
      // 取消公开所有章节
      prisma.bookChapter.updateMany({
        where: {
          bookId: parseInt(bookId),
        },
        data: { isPublic: false },
      }),
    ]);
  }

  /**
   * 切换书籍公开状态
   */
  async togglePublish(bookId: string): Promise<boolean> {
    const book = await prisma.book.findUnique({
      where: { id: parseInt(bookId) },
      select: { isPublished: true },
    });
    
    const newStatus = !book?.isPublished;
    
    if (newStatus) {
      await this.publishAlbum(bookId);
    } else {
      await this.unpublishAlbum(bookId);
    }
    
    return newStatus;
  }

  /**
   * 为书籍章节生成音频并关联（更新 BookChapter.audioUrl）
   */
  async generateChapterAudio(bookId: string, chapterNumber: number, userId?: number): Promise<{
    audioUrl: string;
  } | null> {
    const chapter = await prisma.bookChapter.findFirst({
      where: { bookId: parseInt(bookId), number: chapterNumber },
      include: { book: true },
    });

    if (!chapter || !chapter.content) {
      return null;
    }

    // 生成音频（异步模式，通过回调更新章节）
    const result = await generateAudio(
      userId ? String(userId) : String(chapter.book?.userId || '0'),
      chapter.content,
      'longyingling_v3',
      { speed: 1.0, pitch: 0, volume: 50 },
      async (audioUrl: string, duration: number) => {
        // 音频生成完成后更新章节
        await prisma.bookChapter.update({
          where: { id: chapter.id },
          data: {
            audioUrl,
            audioDuration: duration,
          },
        });
        console.log(`✅ 章节${chapterNumber}音频生成完成:`, audioUrl);
      }
    );

    return {
      audioUrl: result.audioUrl, // 初始为空字符串，实际URL通过回调更新
    };
  }

  /**
   * 按 ID 生成章节音频（数据库队列 + 内容哈希去重）
   *
   * 流程：
   *   1. 计算内容 SHA256 哈希
   *   2. 检查是否已有相同哈希的已完成任务 → 复用
   *   3. 检查是否有进行中的任务 → 跳过（不中断）
   *   4. 创建 TtsTask 记录 → 队列处理器异步执行
   *
   * 关键保护：
   *   - 同一内容不会重复生成 TTS
   *   - 已在 audio_generating 的章节不会被回退
   *   - 无递归调用，重试由队列处理器平铺循环控制
   */
  async generateChapterAudioById(chapterId: number, userId?: number, voiceId?: string): Promise<{
    audioUrl: string;
  } | null> {
    // ===== 步骤 1：读取章节状态 =====
    const chapterBefore = await prisma.bookChapter.findUnique({
      where: { id: chapterId },
      include: { book: true },
    });

    if (!chapterBefore) {
      console.warn(`[Audio] 章节不存在: ${chapterId}`);
      return null;
    }

    // 检查内容是否存在
    if (!chapterBefore.content || chapterBefore.genStage === 'idle') {
      console.warn(`[Audio] 章节内容未生成完成: ${chapterId}, genStage: ${chapterBefore.genStage}`);
      return null;
    }

    // ===== 步骤 2：内容哈希去重 =====
    const contentHash = computeContentHash(chapterBefore.content);

    // 2a. 检查是否有相同内容的已完成 TTS 任务
    const existingCompleted = await prisma.ttsTask.findFirst({
      where: { chapterId, taskType: 'tts', contentHash, status: 'completed' },
      orderBy: { completedAt: 'desc' },
    });
    if (existingCompleted?.audioUrl) {
      // 章节的 audioUrl 还在 → 内容没变、音频也没被清 → 可安全复用
      if (chapterBefore.audioUrl) {
        console.log(`[Audio] 章节${chapterId}内容未变化，复用已有音频`);
        return { audioUrl: chapterBefore.audioUrl };
      }
      // audioUrl 已被清空（可能用户主动重整音频）→ 不复用，走新建任务
      console.log(`[Audio] 章节${chapterId}内容未变但音频已清空，重新生成`);
    }

    // 2b. 检查是否有进行中的 TTS 任务（pending 或 processing）
    const inProgressTask = await prisma.ttsTask.findFirst({
      where: { chapterId, taskType: 'tts', status: { in: ['pending', 'processing'] } },
    });
    if (inProgressTask) {
      console.log(`[Audio] 章节${chapterId}已有进行中的任务#${inProgressTask.id}(状态=${inProgressTask.status})，不重复创建`);
      return { audioUrl: '' }; // 返回空，前端通过 genStage 轮询
    }

    // ===== 步骤 3：已有音频且 genStage 正确 → 直接返回 =====
    const alreadyDoneStages = ['audio_completed', 'video_generating', 'video_completed'];
    if (alreadyDoneStages.includes(chapterBefore.genStage) && chapterBefore.audioUrl) {
      console.log(`[Audio] 章节${chapterId}音频已就绪(genStage=${chapterBefore.genStage})，跳过`);
      return { audioUrl: chapterBefore.audioUrl };
    }

    // ===== 步骤 4：推进 genStage 到 audio_generating =====
    // 如果已是 audio_generating，说明之前的任务中断了，先回退再前进
    if (chapterBefore.genStage === 'audio_generating') {
      console.log(`[Audio] 章节${chapterId}上次生成中断，回退后重新排队`);
      await regenerateChapter(chapterId, 'content_completed').catch(() => {});
    }
    // 如果已是 audio_completed/video_* 等更后的阶段，也先回退
    if (alreadyDoneStages.includes(chapterBefore.genStage) || chapterBefore.genStage === 'video_completed') {
      await regenerateChapter(chapterId, 'content_completed').catch(() => {});
    }

    await advanceChapter(chapterId, 'audio_generating');

    // 注：移除乐观锁二次检查。advanceChapter 自身使用 safeTransitionChapter
    // （乐观锁 UPDATE ... WHERE genStage=current），如果冲突会返回 count=0
    // 而不会错误推进。二次检查在高并发下可能误判合法请求。

    // ===== 步骤 5：复用已有 failed 任务或创建新任务 =====
    // 关键：同一 chapterId 只能有一个活跃任务，避免重复创建
    const existingFailed = await prisma.ttsTask.findFirst({
      where: { chapterId, taskType: 'tts', status: 'failed' },
      orderBy: { createdAt: 'desc' },
    });
    
    let task: any;
    if (existingFailed) {
      // 复用已有 failed 任务，重置为 pending，但累加 retryCount（保留历史重试记录）
      const prevRetryCount = existingFailed.retryCount || 0;
      // 关键：更新 voiceId 为当前有效的音色，避免旧任务用已禁用的 MiniMax 音色导致循环失败
      const taskVoiceId = voiceId || 'longyingling_v3';
      task = await prisma.ttsTask.update({
        where: { id: existingFailed.id },
        data: {
          status: 'pending',
          content: chapterBefore.content,
          contentHash,
          voiceId: taskVoiceId,
          retryCount: prevRetryCount,  // 保留历史重试次数，不重置为0
          errorMsg: null,
          startedAt: null,
          completedAt: null,
        },
      });
      console.log(`[Audio] 复用已有任务#${existingFailed.id}: chapterId=${chapterId}, voiceId=${taskVoiceId}, 累计重试=${prevRetryCount}次`);
    } else {
      console.log(`[Audio] 创建TTS任务: chapterId=${chapterId}, 内容长度=${chapterBefore.content.length}, hash=${contentHash.substring(0, 12)}...`);
      task = await prisma.ttsTask.create({
      data: {
        taskType: 'tts',
        chapterId,
        bookId: chapterBefore.bookId,
        userId: userId || chapterBefore.book?.userId || null,
        contentHash,
        content: chapterBefore.content, // 保存提交时的内容副本
        voiceId: voiceId || 'longyingling_v3',  // 支持外部传入音色，默认兼容有声书
        status: 'pending',
      },
    });
    console.log(`[Audio] TTS任务 #${task.id} 已创建，等待队列处理器处理`);
    }

    // 返回空 audioUrl，前端通过 genStage 或 WebSocket 获取进度
    return { audioUrl: '' };
  }

  /**
   * [DEPRECATED] 创建章节内容生成任务（数据库队列 + 内容哈希去重）
   *
   * ⚠️ 当前无 content 队列处理器（tts-queue.ts仅创建了tts队列）。
   * 内容生成已改为 LangGraph 节点直接调用 LLM，不再使用数据库队列。
   * 此方法保留仅用于向后兼容，请勿调用。
   *
   * 与 TTS 队列共用 TtsTask 表，通过 taskType='content' 区分。
   * hash 基于章节关键属性（标题、父级、预估字数等），
   * 相同参数不重复提交 LLM 请求。
   *
   * @returns taskId 或 null（如果已有进行中/已完成任务）
   */
  async enqueueContentGeneration(chapterId: number, bookId?: number): Promise<number | null> {
    const chapter = await prisma.bookChapter.findUnique({
      where: { id: chapterId },
      include: { book: true },
    });
    if (!chapter) {
      console.warn(`[Content] 章节不存在: ${chapterId}`);
      return null;
    }

    // ===== 拼接关键属性计算哈希 =====
    const attrString = [
      chapter.bookId,
      chapter.title,
      chapter.parentId,
      chapter.level,
      chapter.estimatedWords,
    ].join('|');
    const contentHash = computeContentHash(attrString);

    // 去重：已完成的内容任务
    const existingCompleted = await prisma.ttsTask.findFirst({
      where: { chapterId, taskType: 'content', contentHash, status: 'completed' },
      orderBy: { completedAt: 'desc' },
    });
    if (existingCompleted) {
      console.log(`[Content] 章节${chapterId}相同参数已有完成记录(hash=${contentHash.substring(0, 8)}...)，跳过`);
      return null;
    }

    // 去重：进行中的任务
    const inProgressTask = await prisma.ttsTask.findFirst({
      where: { chapterId, taskType: 'content', status: { in: ['pending', 'processing'] } },
    });
    if (inProgressTask) {
      console.log(`[Content] 章节${chapterId}已有进行中的内容任务#${inProgressTask.id}，不重复创建`);
      return inProgressTask.id;
    }

    // 已有内容且 genStage 正常 → 跳过
    if (chapter.content && ['content_completed', 'audio_generating', 'audio_completed'].includes(chapter.genStage)) {
      console.log(`[Content] 章节${chapterId}已有内容(genStage=${chapter.genStage})，跳过`);
      return null;
    }

    // 推进状态
    if (chapter.genStage !== 'content_generating') {
      if (chapter.genStage !== 'idle' && chapter.genStage !== 'outline_completed') {
        await regenerateChapter(chapterId, 'content_generating').catch(() => {});
      } else {
        await advanceChapter(chapterId, 'content_generating').catch(() => {});
      }
    }

    const task = await prisma.ttsTask.create({
      data: {
        taskType: 'content',
        chapterId,
        bookId: bookId || chapter.bookId,
        userId: chapter.book?.userId || null,
        contentHash,
        content: attrString, // 保存提交时的属性快照
        status: 'pending',
      },
    });

    console.log(`[Content] 内容任务 #${task.id} 已创建, chapterId=${chapterId}, hash=${contentHash.substring(0, 12)}...`);
    return task.id;
  }

  /**
   * 处理内容生成队列任务（由 ContentQueue 调用）
   *
   * [DEPRECATED] 实际的 LLM 文本生成逻辑，平铺循环重试（最多 3 次），无递归。
   *
   * ⚠️ 无 content 队列处理器调用此方法。内容生成由 LangGraph 节点直接完成。
   */
  async processContentTask(taskId: number): Promise<void> {
    const task = await prisma.ttsTask.findUnique({ where: { id: taskId } });
    if (!task || task.status !== 'processing' || task.taskType !== 'content') {
      console.warn(`[ContentTask] 任务#${taskId}状态异常，跳过`);
      return;
    }

    const chapterId = task.chapterId;
    console.log(`[ContentTask] 开始处理任务#${taskId}, chapterId=${chapterId}`);

    const MAX_RETRIES = 3;
    for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
      try {
        console.log(`[ContentTask] #${taskId} 第${attempt + 1}次尝试生成内容...`);

        const chapter = await prisma.bookChapter.findUnique({
          where: { id: chapterId },
          include: { book: true },
        });
        if (!chapter) {
          await this._failTask(taskId, `章节${chapterId}不存在`);
          return;
        }

        const bookId = String(chapter.bookId);
        // 委托给现有的生成方法（它已经包含完整的 LLM 调用逻辑）
        await this.generateSingleChapterContent(bookId, chapterId);

        // 验证生成结果
        const updated = await prisma.bookChapter.findUnique({ where: { id: chapterId } });
        if (updated?.content && updated.genStage === 'content_completed') {
          await prisma.ttsTask.update({
            where: { id: taskId },
            data: {
              status: 'completed',
              completedAt: new Date(),
              retryCount: attempt,
            },
          });
          console.log(`[ContentTask] ✅ 任务#${taskId} 完成`);
          await this.tryCleanupBookTasks(chapterId);
          return;
        }

        // 内容为空或状态不对 → 视为失败
        throw new Error(updated?.content ? `状态异常: ${updated.genStage}` : '生成内容为空');

      } catch (err: any) {
        const errorMsg = err?.message || String(err);
        console.error(`[ContentTask] ❌ 任务#${taskId} 第${attempt + 1}次失败:`, errorMsg);

        if (attempt < MAX_RETRIES) {
          const delay = Math.min(3000 * Math.pow(2, attempt) + Math.random() * 2000, 60000);
          console.log(`[ContentTask] #${taskId} ${(delay / 1000).toFixed(1)}s 后重试...`);
          await prisma.ttsTask.update({
            where: { id: taskId },
            data: { retryCount: attempt + 1, errorMsg: errorMsg.substring(0, 500) },
          });
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }

        await this._failTask(taskId, errorMsg);
        return;
      }
    }

    await this._failTask(taskId, '重试耗尽');
  }

  /**
   * 等待内容生成队列任务完成（调用方轮询，最多等待 30 分钟）
   *
   * @returns 生成后的章节内容
   */
  async awaitContentTask(taskId: number, timeoutMs: number = 30 * 60 * 1000): Promise<{
    content: string;
    wordCount: number;
    genStage: string;
  }> {
    const task = await prisma.ttsTask.findUnique({ where: { id: taskId } });
    if (!task) throw new Error(`任务#${taskId}不存在`);
    if (task.taskType !== 'content') throw new Error(`任务#${taskId}不是内容生成类型`);

    const start = Date.now();
    let lastLogTime = 0;

    while (Date.now() - start < timeoutMs) {
      const chapter = await prisma.bookChapter.findUnique({
        where: { id: task.chapterId },
        select: { content: true, wordCount: true, genStage: true, contentError: true },
      });
      if (!chapter) throw new Error(`章节${task.chapterId}不存在`);

      // 完成
      if (chapter.genStage === 'content_completed' && chapter.content) {
        console.log(`[ContentTask] 任务#${taskId} 轮询完成 (${((Date.now() - start) / 1000).toFixed(0)}s)`);
        return { content: chapter.content, wordCount: chapter.wordCount, genStage: 'content_completed' };
      }

      // 失败
      if (chapter.genStage === 'failed') {
        const updated = await prisma.ttsTask.findUnique({ where: { id: taskId }, select: { errorMsg: true } });
        throw new Error(updated?.errorMsg || chapter.contentError || '内容生成失败');
      }

      // 进度日志（每 30 秒）
      const now = Date.now();
      if (now - lastLogTime > 30000) {
        lastLogTime = now;
        const elapsed = ((now - start) / 1000).toFixed(0);
        console.log(`[ContentTask] 任务#${taskId} 等待中... genStage=${chapter.genStage}, 已等${elapsed}s`);
      }

      await new Promise(resolve => setTimeout(resolve, 3000));
    }

    throw new Error(`内容生成超时 (${timeoutMs / 1000}s)`);
  }

  /**
   * 处理 TTS 队列任务（由 tts-queue.ts 调用）
   *
   * 只尝试 1 次。Provider 层会自动遍历所有可用 Provider。
   * 失败后标记 failed，由用户手动重新生成。
   */
  async processTtsTask(taskId: number): Promise<void> {
    const task = await prisma.ttsTask.findUnique({ where: { id: taskId } });
    if (!task || task.status !== 'processing') {
      console.warn(`[TtsTask] 任务#${taskId}状态异常(status=${task?.status})，跳过`);
      return;
    }

    const chapterId = task.chapterId;
    const content = task.content;
    if (!content) {
      await this._failTask(taskId, '任务内容为空');
      return;
    }

    // 最小字数校验：过短文本 TTS API 会报 InvalidParameter，触发熔断
    if (content.trim().length < 10) {
      await this._failTask(taskId, '文本过短（少于10字），无法生成音频');
      return;
    }

    console.log(`[TtsTask] 开始处理任务#${taskId}, chapterId=${chapterId}, 内容长度=${content.length}`);

    try {
      const chapter = await prisma.bookChapter.findUnique({
        where: { id: chapterId },
        include: { book: true },
      });
      if (!chapter) {
        await this._failTask(taskId, `章节${chapterId}不存在`);
        return;
      }

      const userId = chapter.book?.userId || task.userId || 1;
      const bookId = chapter.bookId;

      // 设置上下文，后续 TTS 调用自动携带 userId/bookId/chapterId
      await runWithContext({ userId, bookId, chapterId }, async () => {
      const result = await generateAudio(
        String(userId),
        content,
        task.voiceId || 'longyingling_v3',
        { speed: 1.0, pitch: 0, volume: 50 },
        async (audioUrl: string, duration: number) => {
          console.log(`[TtsTask] #${taskId} 音频就绪: ${audioUrl?.substring(0, 60)}...`);
          try {
            await advanceChapter(chapterId, 'audio_completed');
            try {
              const userId = chapter.book?.userId || task.userId || 1;
              const audioMinutes = Math.ceil(duration / 60);
              await consumeAudioMinutes(userId, audioMinutes, `书籍「${chapter.book?.title || '未知'}」- ${chapter.title} (${duration}s)`);
            } catch (quotaErr: any) {
              console.warn(`[Quota] 消耗音频配额失败:`, quotaErr.message);
            }
            tryAutoMerge(chapterId, chapter.bookId, chapter.level, chapter.parentId);
          } catch (advanceErr: any) {
            console.warn(`[TtsTask] #${taskId} genStage推进失败: ${advanceErr.message}，音频已就绪`);
          }
        },
        {
          bookId: chapter.bookId ? String(chapter.bookId) : undefined,
          chapterId: chapter.id,
          chapterTitle: chapter.title,
        }
      );

      await prisma.ttsTask.update({
        where: { id: taskId },
        data: { status: 'completed', audioUrl: result.audioUrl, completedAt: new Date() },
      });

      console.log(`[TtsTask] ✅ 任务#${taskId} 完成 bookId=${bookId} chapterId=${chapterId} chars=${content?.length || 0}`);
      await this.tryCleanupBookTasks(chapterId);
      }); // runWithContext 结束

    } catch (err: any) {
      const errorMsg = err?.message || String(err);
      console.error(`[TtsTask] ❌ 任务#${taskId} 失败 bookId=${task.bookId} chapterId=${chapterId}:`, errorMsg);
      await this._failTask(taskId, errorMsg);

      // 不可恢复错误（参数错误等）→ 标记章节为 failed，阻止 AudioScanner 反复重试
      const lowerMsg = errorMsg.toLowerCase();
      if (lowerMsg.includes('invalidparameter') || lowerMsg.includes('invalid_parameter')) {
        try {
          await prisma.bookChapter.update({
            where: { id: chapterId },
            data: { genStage: 'failed', contentError: errorMsg.substring(0, 500) },
          });
          console.log(`[TtsTask] 🔒 章节${chapterId}标记为failed（不可恢复错误），不再自动重试`);
        } catch (chapterErr) {
          console.warn(`[TtsTask] 标记章节失败状态失败:`, chapterErr);
        }
      }
    }
  }

  /**
   * 标记任务失败（不回退章节状态，由用户手动重新生成）
   */
  private async _failTask(taskId: number, errorMsg: string): Promise<void> {
    console.error(`[TtsTask] ❌ 任务#${taskId} 最终失败:`, errorMsg);

    await prisma.ttsTask.update({
      where: { id: taskId },
      data: {
        status: 'failed',
        errorMsg: errorMsg.substring(0, 1000),
        completedAt: new Date(),
      },
    });
  }

  /**
   * 任务成功后尝试清理整本书的任务记录
   * 当一本书所有章节都已生成完毕（>= audio_completed），清空该书的所有队列任务
   */
  private async tryCleanupBookTasks(chapterId: number): Promise<void> {
    try {
      const chapter = await prisma.bookChapter.findUnique({
        where: { id: chapterId },
        select: { bookId: true },
      });
      if (!chapter?.bookId) return;

      // 检查该书的章节是否全部完成
      const allChapters = await prisma.bookChapter.findMany({
        where: { bookId: chapter.bookId },
        select: { genStage: true },
      });
      if (allChapters.length === 0) return;

      const doneStages = ['audio_completed', 'video_generating', 'video_completed'];
      const allDone = allChapters.every(c => doneStages.includes(c.genStage));
      if (!allDone) return;

      // 全书完成，清理任务记录
      const result = await prisma.ttsTask.deleteMany({
        where: { bookId: chapter.bookId },
      });
      if (result.count > 0) {
        console.log(`[Cleanup] 书籍#${chapter.bookId}已完成，清理了 ${result.count} 条队列任务`);
      }
    } catch (err: any) {
      console.warn(`[Cleanup] 清理任务失败:`, err.message);
    }
  }

  /**
   * 重新生成单个章节内容（仅叶节点）
   * 直接生成，不触发整个书籍生成流程
   */
  async generateSingleChapterContent(bookId: string, chapterId: number): Promise<void> {
    const book = await this.getById(bookId);
    if (!book) {
      console.error(`[generateSingleChapterContent] 书籍不存在: ${bookId}`);
      return;
    }

    const chapter = await prisma.bookChapter.findUnique({
      where: { id: chapterId },
    });
    if (!chapter) {
      console.error(`[generateSingleChapterContent] 章节不存在: ${chapterId}`);
      return;
    }

    // 构建父节点映射
    const chaptersAndSections = await prisma.bookChapter.findMany({
      where: { bookId: parseInt(bookId), level: { in: [1, 2] } }
    });

    const chapterMap = new Map<number, any>();
    const sectionMap = new Map<number, any>();
    chaptersAndSections.forEach(c => {
      if (c.level === 1) chapterMap.set(c.id, c);
      if (c.level === 2) sectionMap.set(c.id, c);
    });

    const parentSection = sectionMap.get(chapter.parentId || 0);
    const parentChapter = parentSection ? chapterMap.get(parentSection.parentId || 0) : null;

    const chapterTitle = parentChapter?.title || '未知章';
    const sectionTitle = parentSection?.title || '未知节';
    const chapterSummary = parentChapter?.summary || '';
    const sectionSummary = parentSection?.summary || '';

    // 判断是否是短文
    const isShortArticle = !parentSection && chapter.level === 1;

    // 从 description 中提取写作风格要求
    const styleMatch = book.description?.match(/写作风格：([^\\n]+)/);
    const writingStyle = styleMatch ? styleMatch[1] : (book.style || '');

    let messages: ChatMessage[];
    if (isShortArticle) {
      messages = [
        { role: 'system', content: SUBSECTION_CONTENT_SYSTEM_PROMPT },
        {
          role: 'user',
          content: `书名：《${book.title}》
${book.description || ''}
${writingStyle ? `写作风格：${writingStyle}` : ''}
章标题：${chapter.title}
章概述：${chapter.summary || ''}
预估字数：${chapter.estimatedWords || 500}字

请撰写该章节的正文内容。`,
        },
      ];
    } else {
      messages = buildSubsectionContentMessages(
        book.title,
        book.description || '',
        chapterTitle,
        chapterSummary,
        sectionTitle,
        sectionSummary,
        chapter
      );
    }

    const bookTools = createBookTools(bookId, this);

    try {
      let content: string;
      try {
        const result = await callLLMWithTools(messages, bookTools);
        content = cleanThinkingText(result.text);
      } catch {
        content = cleanThinkingText(await callLLMWithMessages(messages));
      }

      const wordCount = countWords(content);

      await this.updateChapterById(chapterId, {
        content,
        wordCount,
      });

      // 推进到 content_completed（如已越过则跳过，不抛异常）
      const currentChapter = await prisma.bookChapter.findUnique({ where: { id: chapterId } });
      if (currentChapter) {
        const order = ['idle', 'outline_completed', 'content_generating', 'content_completed', 'audio_generating', 'audio_completed', 'video_generating', 'video_completed'];
        const curIdx = order.indexOf(currentChapter.genStage);
        const tgtIdx = order.indexOf('content_completed');
        // 只有当前在 content_completed 之前才推进（避免竞态：音频已推进到 audio_generating）
        if (curIdx >= 0 && curIdx < tgtIdx) {
          try {
            // 状态机要求：outline_completed → content_generating → content_completed
            if (currentChapter.genStage !== 'content_generating') {
              await advanceChapter(chapterId, 'content_generating').catch(() => {});
            }
            await advanceChapter(chapterId, 'content_completed');
          } catch (advanceErr: any) {
            console.warn(`[generateSingleChapterContent] genStage推进失败 chapterId=${chapterId}: ${advanceErr.message}, 可能已被其他流程推进`);
          }
        }
      }

      console.log(`✅ 章节「${chapter.title}」内容重新生成完成，字数: ${wordCount}`);
    } catch (error) {
      const errorMsg = error instanceof Error ? error.message : '失败';
      await this.updateChapterById(chapterId, {
        contentError: errorMsg,
      });
      // 仅当LLM调用失败时才回退到 content_generating
      console.error(`❌ 章节${chapterId}内容重新生成失败:`, errorMsg);
    }
  }

  /**
   * 转换数据库模型到 Book 类型
   * @param excludeContent 是否排除章节内容（用于列表/详情页，只返回标题不返回正文）
   */
  private toBook(dbBook: {
    id: number;
    userId: number | null;
    title: string;
    subtitle: string | null;
    description: string;
    targetAudience: string;
    style: string;
    bookScale: string;
    totalChapters: number;
    estimatedWords: number;
    progress: number;
    isPublished: boolean;
    genStage?: string;
    failedStage?: string;
    outlineJson: string | null;
    foreword: string | null;
    afterword: string | null;
    errorMsg: string | null;
    bookAnalysis?: string | null;
    createdAt: Date;
    updatedAt: Date;
    chapters: any[];
  }, excludeContent: boolean = false): Book {
    const outline = parseOutlineJson(dbBook.outlineJson);

    return {
      id: String(dbBook.id),
      userId: dbBook.userId || undefined,
      title: dbBook.title,
      subtitle: dbBook.subtitle || undefined,
      description: dbBook.description,
      targetAudience: dbBook.targetAudience,
      style: dbBook.style,
      bookScale: dbBook.bookScale,
      totalChapters: dbBook.totalChapters,
      estimatedWords: dbBook.estimatedWords,
      progress: dbBook.progress,
      isPublished: dbBook.isPublished,
      genStage: computeBookGenStage(dbBook.chapters),
      failedStage: dbBook.failedStage || undefined,
      chapters: chaptersFromDb(dbBook.chapters, dbBook.id, excludeContent),
      outline: outline || undefined,
      metadata: {
        foreword: dbBook.foreword || undefined,
        afterword: dbBook.afterword || undefined,
      },
      bookAnalysis: dbBook.bookAnalysis || undefined,
      error: dbBook.errorMsg || undefined,
      createdAt: dbBook.createdAt,
      updatedAt: dbBook.updatedAt,
    };
  }
}

// 导出单例
export const bookStore = new BookStore();
