import path from 'path';
import fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import { config } from '../../config';
import { prisma } from '../../models';
import { VoiceParams, Voice } from '../../types';
import { AudioMerger } from './audio-merger';
import { aiSummaryService } from './ai-summary.service';
import { storageService } from '../../services/storage.service';
import { getTtsRegistry, getAvailableTtsProvider, startTtsHealthCheck } from './provider.registry';
import { ITtsProvider } from './provider.interface';
import { CircuitBreakerOpenError } from '../../common/circuit-breaker';
import { ProviderNode } from '../../common/provider-registry';
import { ttsLogger } from './tts-logger';
import axios from 'axios';

// ============ 统一音色定义（10个固定音色，前端使用）============
// 前端使用统一 ID，后端根据 Provider 类型映射到真实音色
const UNIFIED_VOICES: Voice[] = [
  { id: 'voice_01', name: '温柔女声',  gender: 'female', description: '柔和温暖，适合情感故事' },
  { id: 'voice_02', name: '磁性男声',  gender: 'male',   description: '低沉有力，适合悬疑推理' },
  { id: 'voice_03', name: '活泼女声',  gender: 'female', description: '清新明亮，适合儿童故事' },
  { id: 'voice_04', name: '知性女声',  gender: 'female', description: '知性稳重，适合科普知识' },
  { id: 'voice_05', name: '阳光男声',  gender: 'male',   description: '阳光活力，适合校园青春' },
  { id: 'voice_06', name: '沧桑男声',  gender: 'male',   description: '成熟沧桑，适合历史军事' },
  { id: 'voice_07', name: '甜美女声',  gender: 'female', description: '甜美可爱，适合爱情都市' },
  { id: 'voice_08', name: '清朗男声',  gender: 'male',   description: '清朗干练，适合职场商战' },
  { id: 'voice_09', name: '亲切女声',  gender: 'female', description: '亲切自然，适合日常叙事' },
  { id: 'voice_10', name: '稚嫩童声',  gender: 'female', description: '稚嫩天真，适合童话寓言' },
];

// 统一音色 → 阿里云 真实音色映射
const ALIYUN_VOICE_MAP: Record<string, string> = {
  voice_01: 'longanyang',    voice_02: 'longsanshu_v3', voice_03: 'longhuhu_v3',
  voice_04: 'longyue_v3',    voice_05: 'longyichen_v3', voice_06: 'longlaobo_v3',
  voice_07: 'longmiao_v3',   voice_08: 'longshuo_v3',   voice_09: 'longwan_v3',
  voice_10: 'longhuhu_v3',
};

function mapToProviderVoice(unifiedVoiceId: string, _provider: string): string {
  const mapped = ALIYUN_VOICE_MAP[unifiedVoiceId];
  if (mapped) return mapped;
  // 不在映射表中（如遗留的 'cherry' 等旧 MiniMax 音色）→ 用 CosyVoice 默认音色
  console.warn(`⚠️ [VoiceMap] 未识别的音色ID: "${unifiedVoiceId}"，降级使用默认音色 longyingling_v3`);
  return 'longyingling_v3';
}

// ============ Aliyun Instruct 情感/场景控制 ============
// 后期优化功能，暂不启用。启用时改为 true
const INSTRUCT_ENABLED = false;

interface EmotionScene {
  emotion: string;  // neutral | fearful | angry | sad | surprised | happy | disgusted
  scene: string;    // 闲聊互动 | 新闻播报 | 广告促销 | 比赛解说 | 一些儿童内容解说 | 语音导航 | 脱口秀表演
}

// 情感关键词库
const EMOTION_KEYWORDS: { emotion: string; keywords: string[] }[] = [
  { emotion: 'fearful',   keywords: ['恐怖', '可怕', '惊悚', '恐惧', '阴森', '黑暗', '鬼', '死亡', '谋杀', '悬疑', '危险', '深渊', '噩梦'] },
  { emotion: 'sad',       keywords: ['悲伤', '难过', '哭泣', '眼泪', '心痛', '遗憾', '孤独', '寂寞', '失落', '离别', '思念', '哀伤', '去世', '失去'] },
  { emotion: 'angry',     keywords: ['愤怒', '生气', '怒火', '仇恨', '战斗', '厮杀', '复仇', '战争', '侵略', '暴怒'] },
  { emotion: 'happy',     keywords: ['快乐', '开心', '幸福', '欢笑', '庆祝', '美好', '甜蜜', '温暖', '阳光', '喜悦', '高兴', '浪漫', '恋爱', '美好'] },
  { emotion: 'surprised', keywords: ['惊奇', '惊喜', '意外', '奇迹', '神奇', '魔法', '童话', '幻想', '奇妙'] },
  { emotion: 'disgusted', keywords: ['恶心', '厌恶', '肮脏', '丑陋', '卑鄙'] },
];

// 场景关键词库
const SCENE_KEYWORDS: { scene: string; keywords: string[] }[] = [
  { scene: '新闻播报',         keywords: ['新闻', '报道', '消息', '公告', '通知', '声明', '记者', '据悉', '据新华社', '人民日报'] },
  { scene: '一些儿童内容解说',  keywords: ['童话', '儿童', '小朋友', '宝宝', '故事', '小熊', '小兔', '公主', '王子', '森林', '魔法', '精灵'] },
  { scene: '广告促销',         keywords: ['促销', '优惠', '折扣', '限时', '秒杀', '购买', '抢购', '免费', '特价'] },
  { scene: '脱口秀表演',       keywords: ['搞笑', '幽默', '笑话', '段子', '吐槽', '趣事'] },
  { scene: '语音导航',         keywords: ['导航', '前方', '左转', '右转', '直行', '到达', '目的地', '路线'] },
  { scene: '比赛解说',         keywords: ['比赛', '进球', '得分', '冠军', '决赛', '球队', '选手', '比分'] },
];

/** 根据文本内容分析情感 */
function detectEmotion(text: string): string {
  const scores: Record<string, number> = {};
  for (const { emotion, keywords } of EMOTION_KEYWORDS) {
    scores[emotion] = 0;
    for (const kw of keywords) {
      if (text.includes(kw)) scores[emotion]++;
    }
  }
  let best = 'neutral';
  let bestScore = 0;
  for (const [emotion, score] of Object.entries(scores)) {
    if (score > bestScore) { best = emotion; bestScore = score; }
  }
  return best;
}

/** 根据文本内容分析场景 */
function detectScene(text: string): string {
  const scores: Record<string, number> = {};
  for (const { scene, keywords } of SCENE_KEYWORDS) {
    scores[scene] = 0;
    for (const kw of keywords) {
      if (text.includes(kw)) scores[scene]++;
    }
  }
  let best = '闲聊互动';
  let bestScore = 0;
  for (const [scene, score] of Object.entries(scores)) {
    if (score > bestScore) { best = scene; bestScore = score; }
  }
  return best;
}

/** 根据文本内容动态生成 Aliyun Instruct 文本 */
export function getVoiceInstruct(text: string): string {
  const emotion = detectEmotion(text);
  const scene = detectScene(text);
  return `你正在进行${scene}，你说话的情感是${emotion}。`;
}

// 兼容旧代码的旧版音色列表（保留但不再推荐使用）
const LEGACY_VOICES: Voice[] = [
  { id: 'longanyang',    name: '龙安洋',   gender: 'male',   description: '阳光大男孩 20~30岁' },
  { id: 'longanhuan',    name: '龙安欢',   gender: 'female', description: '欢脱元气女 20~30岁' },
];

export function getAliyunVoice(voiceId: string): string {
  // 旧音色ID直接返回（新音色已统一映射）
  return voiceId;
}

// 获取或创建默认"我的音频"书籍
async function getOrCreateDefaultBook(userId: string): Promise<number> {
  const userIdNum = parseInt(userId);
  const bookTitle = '我的音频';

  // 先尝试查找现有默认书籍
  let book = await prisma.book.findFirst({
    where: {
      userId: userIdNum,
      title: bookTitle,
    },
  });

  if (book) {
    return book.id;
  }

  // 不存在则创建
  book = await prisma.book.create({
    data: {
      title: bookTitle,
      description: '我的语音合成音频收藏',
      userId: userIdNum,
      genStage: 'content_completed',
      bookScale: '2000',
      totalChapters: 0,
      estimatedWords: 0,
    },
  });

  console.log(`📚 创建默认书籍: bookId=${book.id}, userId=${userIdNum}`);
  return book.id;
}

// 判断是否使用长文本模式（>5000字符且启用realtime）
// 注意：WebSocket realtime 模式需要特殊的API权限和配置，如果连接失败会导致生成失败。
// 暂时强制禁用，使用 HTTP 分段模式
export function shouldUseLongText(text: string): boolean {
  return false; // 强制返回 false，禁用 realtime 模式
}

// 文本分段 - 统一 1000 字符上限，优先在标点处断开
// 策略：按自然句子累加，接近上限时在最后一个标点处截断，避免在字中间硬切
// 好处：TTS 在句末韵律自然，拼接后听感流畅
//
// 1000 字的依据（主流 TTS 接口上限，大多以千为单位）：
//   MiniMax 同步: 1万  |  阿里云 CosyVoice: 2万  |  讯飞 流式: ~4000字
//   百度 短文本: 5120字  |  OpenAI TTS: 4096字  |  Google Cloud: 5000字节
//   ElevenLabs: 3000~5000字  |  Azure: ~3000~5000字
//   不支持1000的接口（应走异步长文本API，而非短文本接口）：
//     火山引擎 非流式: 300字 | 腾讯云 基础: 150字 | 百度 短文本: 60字
const SEGMENT_MAX_LENGTH = 1000;
// 句末标点（中文 + 英文），用于寻找断点
const SENTENCE_END_RE = /[。！？；\n.!?;]/;

export function splitText(text: string, maxLength: number = SEGMENT_MAX_LENGTH): string[] {
  const segments: string[] = [];

  // 清理文本
  const cleanText = text.replace(/\r/g, '');

  // 第一步：按换行拆成段落，再按句末标点拆成句子
  // 这样保留了段落边界，又不会在一个句子中间断开
  const rawSentences: string[] = [];
  const paragraphs = cleanText.split(/\n+/);

  for (const para of paragraphs) {
    if (para.length === 0) continue;

    // 按句末标点拆分，保留标点
    const sentences = para.match(/[^。！？；.!?;]+[。！？；.!?;]?/g) || [para];
    for (const s of sentences) {
      if (s.trim().length > 0) {
        rawSentences.push(s);
      }
    }
  }

  // 第二步：将句子累加成段，接近上限时在最后一个标点处截断
  let current = '';

  for (const sentence of rawSentences) {
    // 当前段 + 这句还能放下
    if ((current + sentence).length <= maxLength) {
      current += sentence;
      continue;
    }

    // 放不下了，先把当前段输出
    if (current) {
      segments.push(current);
      current = '';
    }

    // 如果这句话本身就超长，需要在句内找标点断点
    if (sentence.length > maxLength) {
      let remaining = sentence;
      while (remaining.length > 0) {
        if (remaining.length <= maxLength) {
          current = remaining;
          break;
        }
        // 在上限附近向前找最后一个标点作为断点
        let breakPos = -1;
        for (let i = maxLength; i > Math.max(0, maxLength - 100); i--) {
          if (SENTENCE_END_RE.test(remaining[i])) {
            breakPos = i + 1; // 标点后一位
            break;
          }
        }
        // 找不到标点，尝试逗号/顿号等次级标点
        if (breakPos === -1) {
          for (let i = maxLength; i > Math.max(0, maxLength - 100); i--) {
            if (/[，、,]/.test(remaining[i])) {
              breakPos = i + 1;
              break;
            }
          }
        }
        // 实在找不到标点，只能在上限处硬切（最后手段）
        if (breakPos === -1) {
          breakPos = maxLength;
        }
        segments.push(remaining.slice(0, breakPos));
        remaining = remaining.slice(breakPos);
      }
    } else {
      current = sentence;
    }
  }

  if (current) segments.push(current);

  return segments;
}

// ============ TTS Provider 调度（使用注册表 + 熔断器）============
//
// 优先级顺序由 Registry 中的 priority 决定:
//   1. MiniMax (priority=1) - 异步长文本，业界最长
//   2. 阿里云 Qwen-TTS (priority=2) - 同步快速
//   3. Mock (priority=100) - 降级兜底
//
// 熔断器: 连续 3 次失败自动熔断，MiniMax 冷却 60s，阿里云 30s
//
// 分段策略: 统一 1000 字符/段，兼容所有 TTS 模型
// 各家 TTS API 限制参考（2026年5月）:
// ┌──────────────┬────────────────────┬───────────┬──────────────────────┐
// │ 服务商        │ 接口类型            │ 最大长度   │ 备注                 │
// ├──────────────┼────────────────────┼───────────┼──────────────────────┤
// │ MiniMax      │ 异步长文本          │ 1,000,000 │ 业界最长             │
// │ 阿里云       │ 非流式/单向流式     │ 20,000    │ CosyVoice            │
// │ 火山引擎/豆包 │ 异步长文本          │ 100,000   │ 音频保存7天          │
// │ 百度         │ 长文本异步          │ 100,000   │ 一次性合成            │
// │ 讯飞         │ 长文本TTS          │ 100,000   │ 万字级别快速合成      │
// │ 腾讯云       │ 长文本语音合成      │ 10,000+   │ 每个speak标签≤150字  │
// │ OpenAI       │ TTS-1/HD           │ 4,096     │ 隐藏限制             │
// └──────────────┴────────────────────┴───────────┴──────────────────────┘

/**
 * TTS 直接生成请求（复用有声书生成立逻辑）
 * 
 * 与有声书的核心区别：内容由用户直接提供（跳过 AI 内容生成），只创建单章节。
 * 创建章节后通过 TtsTask 队列异步生成音频，与有声书完全相同的后续流程。
 * 
 * @returns { chapterId, bookId } - 前端通过轮询 genStage 或 WebSocket 获取进度
 */
export async function requestTtsGeneration(
  userId: string,
  text: string,
  voiceId: string,
  voiceParams: VoiceParams,
  options?: {
    bookId?: string;
    chapterTitle?: string;
  }
): Promise<{
  chapterId: number;
  bookId: number;
}> {
  const userIdNum = userId ? parseInt(userId) : null;

  // 1. 获取或创建默认书籍
  let targetBookId: number;
  if (options?.bookId) {
    targetBookId = parseInt(options.bookId);
    // 验证书籍存在
    const book = await prisma.book.findUnique({ where: { id: targetBookId } });
    if (!book) {
      throw new Error('指定的书籍不存在');
    }
  } else {
    targetBookId = await getOrCreateDefaultBook(userId);
  }

  // 2. 在默认书籍下创建章节（内容=用户输入，跳过AI生成）
  const chapterTitle = options?.chapterTitle || text.replace(/[\n\r]/g, ' ').substring(0, 30) + (text.length > 30 ? '...' : '');
  const wordCount = (text.match(/[\u4e00-\u9fa5]/g) || []).length;

  // 找到当前书籍的最大章节序号
  const maxChapter = await prisma.bookChapter.findFirst({
    where: { bookId: targetBookId, parentId: 0 },
    orderBy: { number: 'desc' },
  });
  const chapterNumber = (maxChapter?.number || 0) + 1;

  const chapter = await prisma.bookChapter.create({
    data: {
      bookId: targetBookId,
      parentId: 0,      // 顶层章节
      level: 1,         // 章级别
      number: chapterNumber,
      title: chapterTitle,
      content: text,
      wordCount,
      genStage: 'content_completed',  // 跳过AI内容生成，直接标记内容已完成
    },
  });

  console.log(`📝 [TTS] 创建章节: chapterId=${chapter.id}, bookId=${targetBookId}, 标题="${chapterTitle}", 字数=${wordCount}`);

  // 3. 通过有声书队列生成音频（与有声书完全相同的后续流程）
  const { bookStore } = await import('../book-generator/book-generator.store.js');
  await bookStore.generateChapterAudioById(chapter.id, userIdNum || undefined, voiceId);

  console.log(`✅ [TTS] 音频任务已入队: chapterId=${chapter.id}, voiceId=${voiceId}`);

  return {
    chapterId: chapter.id,
    bookId: targetBookId,
  };
}

// 生成音频（异步模式，不再创建 Audio 记录）
export async function generateAudio(
  userId: string,
  text: string,
  voiceId: string,
  voiceParams: VoiceParams,
  onComplete?: (audioUrl: string, duration: number) => void,
  options?: {
    bookId?: string;
    chapterId?: number;
    chapterTitle?: string;
    ttsProvider?: 'aliyun' | 'minimax';
  }
): Promise<{
  audioId: string;
  audioUrl: string;
  bookId: number;
}> {
  // 使用 UUID 作为音频 ID
  const audioId = uuidv4();
  const audioDir = path.join(config.upload.dir, audioId);

  if (!fs.existsSync(audioDir)) {
    fs.mkdirSync(audioDir, { recursive: true });
  }

  console.log('📝 开始音频生成:', audioId, '文本长度:', text.length, 'bookId:', options?.bookId);

  // 解析或创建目标书籍 ID
  let targetBookId: number;
  if (options?.bookId) {
    targetBookId = parseInt(options.bookId);
  } else {
    targetBookId = await getOrCreateDefaultBook(userId);
  }

  // 创建 AudioRecord 记录（状态为 processing）
  const wordCount = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
  try {
    await prisma.audioRecord.create({
      data: {
        audioId,
        userId: userId ? parseInt(userId) : null,
        title: options.chapterTitle || '未命名音频',
        text: text.length > 10000 ? text.substring(0, 10000) : text,
        wordCount,
        voiceId,
        voiceParams: JSON.stringify(voiceParams),
        status: 'processing',
        bookId: targetBookId,
      },
    });
    console.log('💾 AudioRecord 已创建:', audioId);
  } catch (error) {
    console.error('❌ 创建 AudioRecord 失败:', error);
  }

  // 等待音频生成完成
  try {
    const result = await processAudioGeneration(audioId, text, voiceId, voiceParams, audioDir, onComplete, {
      ...options,
      userId,
      bookId: String(targetBookId),
      ttsProvider: options?.ttsProvider,
    });
    return result;
  } catch (error: any) {
    console.error('❌ 音频生成失败:', error.message);
    // 更新 AudioRecord 状态为失败
    try {
      await prisma.audioRecord.update({
        where: { audioId },
        data: { status: 'failed', errorMsg: error.message },
      });
    } catch (dbError) {
      console.error('❌ 更新 AudioRecord 失败状态失败:', dbError);
    }
    // 创建失败标记文件
    const failedMarker = path.join(audioDir, 'failed');
    fs.writeFileSync(failedMarker, error.message);

    // 推送 WebSocket 事件（音频生成失败）
    if (options?.bookId && options?.chapterId) {
      const { pushAudioGenerationComplete } = await import('../../services/websocket.service.js');
      pushAudioGenerationComplete(options.bookId, options.chapterId, 'failed');
    }

    throw error;
  }
}

/**
 * 异步处理音频生成（使用 Provider 注册表 + 熔断器）
 */
async function processAudioGeneration(
  audioId: string,
  text: string,
  voiceId: string,
  voiceParams: VoiceParams,
  audioDir: string,
  onComplete?: (audioUrl: string, duration: number) => void,
  options?: {
    userId?: string;
    bookId?: string;
    chapterId?: number;
    chapterTitle?: string;
    ttsProvider?: 'aliyun' | 'minimax';
  }
) {
  const logMsg = `开始处理音频 ID: ${audioId}, 文本长度: ${text.length}, voiceId: ${voiceId}`;
  ttsLogger.info(logMsg);

  const registry = getTtsRegistry();

  // 解析首选 Provider 名称（与注册表命名规则一致：${vendorKey}-tts）
  // 兼容前端传 'aliyun' → 实际 vendor key 是 'bailian'
  const vendorAlias: Record<string, string> = { aliyun: 'bailian' };
  const vendorKey = vendorAlias[options?.ttsProvider ?? ''] ?? options?.ttsProvider;
  const preferredName = vendorKey ? `${vendorKey}-tts` : undefined;

  // 带降级的执行：优先使用 preferredName，失败后通过 getNextAvailable 轮转
  // 与 LLM 的 invokeWithRetry 保持一致：可恢复错误先重试，再切换供应商
  const getInitialNode = (): ProviderNode<ITtsProvider> => {
    if (preferredName) {
      const pref = registry.get(preferredName);
      if (pref && pref.enabled && !pref.breaker.isOpen()) return pref;
      const next = registry.getNextAvailable(preferredName);
      if (next) return next;
    }
    const primary = registry.getPrimary();
    if (!primary) throw new Error('没有可用的 TTS Provider');
    return primary;
  };

  const visited = new Set<string>();
  let currentNode = getInitialNode();
  const maxProviders = registry.listEnabled().length;

  while (currentNode && visited.size < maxProviders) {
    if (visited.has(currentNode.provider.name)) break;
    visited.add(currentNode.provider.name);

    const tts = currentNode.provider;
    const node = currentNode;

    try {
    console.log(`🔊 使用 TTS Provider: ${tts.name} (vendor=${tts.vendor}, mode=${tts.mode})`);
    ttsLogger.debug(`Provider: ${tts.name}, vendor=${tts.vendor}, mode=${tts.mode}`);

    // 解析音色名称（使用统一音色映射到具体Provider的真实音色）
    const voiceName = mapToProviderVoice(voiceId, tts.vendor);

    // 为 Aliyun 注入 Instruct 情感/场景控制（后期优化，暂不启用）
    if (INSTRUCT_ENABLED && tts.vendor === 'bailian') {
      const instructText = getVoiceInstruct(text);
      if (instructText) {
        voiceParams = { ...voiceParams, instructText };
      }
    }

    // 按模型配置分段：有 maxTextLength 就用它的 80%，没配置就用默认 1000
    const segmentMax = tts.maxTextLength ? Math.floor(tts.maxTextLength * 0.8) : 1000;
    const segments = splitText(text, segmentMax);
    console.log(`📝 文本已分段: ${segments.length} 段, 阈值=${segmentMax}字 (Provider: ${tts.name})`);

    // 并行生成各段音频（使用 Provider 自身的并发设置，每段带重试）
    const audioFiles: string[] = [];
    const cloudUrls: string[] = [];
    const concurrency = tts.concurrency;

    for (let i = 0; i < segments.length; i += concurrency) {
      const batch = segments.slice(i, i + concurrency);

      // 统一调用 synthesizeSegmentWithRetry（熔断保护 + 可恢复错误重试1次）
      const results = await Promise.all(
        batch.map(async (segment, idx) => {
          const segPath = path.join(audioDir, `segment_${i + idx}.mp3`);
          return synthesizeSegmentWithRetry(tts, node, segment, voiceName, voiceParams, segPath);
        })
      );

      results.forEach(r => {
        if (r.startsWith('cloud:')) {
          cloudUrls.push(r.substring(6));
        } else {
          audioFiles.push(r);
        }
      });
    }

    console.log(`📁 生成了 ${audioFiles.length} 个音频文件, ${cloudUrls.length} 个云端URL`);

    let audioUrl = '';
    let duration = 0;
    let size = 0;

    if (cloudUrls.length > 0 && audioFiles.length === 0 && cloudUrls.length === 1) {
      console.log('☁️ 下载单个云端音频并上传...');
      const cloudUrl = cloudUrls[0];
      const response = await axios.get(cloudUrl, { responseType: 'arraybuffer', timeout: 60000 });
      const buffer = Buffer.from(response.data);
      const tempPath = path.join(audioDir, 'output.mp3');
      if (!fs.existsSync(audioDir)) fs.mkdirSync(audioDir, { recursive: true });
      fs.writeFileSync(tempPath, buffer);
      audioUrl = await storageService.uploadAudio(tempPath, audioId);
      duration = await AudioMerger.getDuration(tempPath);
      size = fs.statSync(tempPath).size;
    } else {
      // 下载所有云端URL到本地
      const allLocalFiles = [...audioFiles];
      if (cloudUrls.length > 0) {
        for (let ci = 0; ci < cloudUrls.length; ci++) {
          try {
            const response = await axios.get(cloudUrls[ci], { responseType: 'arraybuffer', timeout: 60000 });
            const localPath = path.join(audioDir, `cloud_segment_${ci}.mp3`);
            fs.writeFileSync(localPath, Buffer.from(response.data));
            allLocalFiles.push(localPath);
          } catch (dlErr: any) {
            console.error(`❌ 云端音频 ${ci + 1} 下载失败:`, dlErr.message);
          }
        }
      }

      if (allLocalFiles.length === 0) {
        throw new Error('所有音频文件下载失败，无法生成音频');
      }

      const outputPath = path.join(audioDir, 'output.mp3');
      const mergedFile = await AudioMerger.merge(allLocalFiles, outputPath);
      size = fs.statSync(mergedFile).size;
      duration = await AudioMerger.getDuration(mergedFile);
      audioUrl = await storageService.uploadAudio(mergedFile, audioId);
      console.log(`📁 音频已合并并上传 (${duration}s):`, audioUrl);
    }

    // AI 生成标题、摘要和标签
    const [title] = await Promise.all([
      aiSummaryService.generateTitle(text),
      aiSummaryService.generateSummary(text, 200),
      aiSummaryService.extractTags(text),
    ]);
    const finalAudioUrl = audioUrl;

    // 保存到书籍章节（先不带LRC，因为异步生成可能还没完成）
    const targetBookId = options?.bookId ? parseInt(options.bookId) : null;
    const targetChapterId = options?.chapterId ?? null;
    if (targetBookId && targetChapterId) {
      try {
        await prisma.bookChapter.update({
          where: { id: targetChapterId },
          data: {
            audioUrl: finalAudioUrl,
            audioDuration: duration,
            generatedAt: new Date(),
          },
        });
        console.log(`✅ 已更新书籍章节音频 (chapterId: ${targetChapterId})`);
      } catch (error) {
        console.error('❌ 保存到书籍章节失败:', error);
      }
    }

    // LRC 歌词时间轴（异步，不阻塞主流程，失败可接受）
    const localAudioPath = path.join(audioDir, 'output.mp3');
    Promise.resolve().then(async () => {
      try {
        const lrc = await generateLrc(text, duration, localAudioPath);
        if (lrc && targetChapterId) {
          prisma.bookChapter.update({
            where: { id: targetChapterId },
            data: { lrcLyrics: lrc },
          }).catch(() => {});
        }
      } catch (lrcErr: any) {
        console.warn(`⚠️ LRC 生成失败: ${lrcErr.message}`);
      }
    });

    // 更新 AudioRecord 为完成
    try {
      await prisma.audioRecord.update({
        where: { audioId },
        data: {
          status: 'completed',
          audioUrl: finalAudioUrl,
          audioDuration: duration,
          audioSize: size,
          title: options?.chapterTitle || title,
        },
      });
    } catch (error) {
      console.error('❌ 更新 AudioRecord 失败:', error);
    }

    // 完成回调
    if (onComplete) {
      try { await onComplete(finalAudioUrl, duration); } catch (callbackErr) {
        console.error(`[TTS] onComplete回调失败:`, callbackErr);
      }
    }

    // WebSocket 推送
    if (options?.bookId && options?.chapterId) {
      const { pushAudioGenerationComplete } = await import('../../services/websocket.service.js');
      pushAudioGenerationComplete(options.bookId, options.chapterId, 'completed');
    }

    console.log('✅ 音频生成完成:', audioId, 'URL:', finalAudioUrl);
      return { audioId, audioUrl: finalAudioUrl, bookId: targetBookId };

    } catch (error: any) {
      const errorMsg = error?.message || '';
      
      // 如果还有下一个 Provider 可降级，就尝试
      const nextNode = registry.getNextAvailable(tts.name);
      if (nextNode && !visited.has(nextNode.provider.name)) {
        ttsLogger.fallback(tts.name, nextNode.provider.name, errorMsg.substring(0, 80));

        // 标记当前 Provider 的健康状态（用于后续请求）
        if (['quota', 'balance', 'insufficient', 'usage limit'].some(k => errorMsg.toLowerCase().includes(k))) {
          registry.markExhausted(tts.name, errorMsg, 4 * 60 * 60 * 1000);
          ttsLogger.quota(tts.name, 'exhausted', errorMsg);
        }

        currentNode = nextNode;
        continue;
      }

      // 没有更多 Provider 可降级
      const triedList = [...visited, tts.name].join(' → ');
      const detailMsg = `所有 Provider 尝试失败 [${triedList}]，最后错误: ${errorMsg.substring(0, 100)}`;
      ttsLogger.synthesisFailed(audioId, tts.name, errorMsg, false);
      throw new Error(detailMsg);
    }
  }

  // 所有 Provider 都试过了
  throw new Error('所有 TTS Provider 都已尝试，均无法生成音频');
}

/**
 * 片段级合成（带熔断保护 + 可恢复错误重试1次）
 * 与 LLM invokeWithRetry 策略一致：瞬时波动先重试，持续性故障再切换
 */
async function synthesizeSegmentWithRetry(
  tts: ITtsProvider,
  node: ProviderNode<ITtsProvider>,
  segment: string,
  voiceName: string,
  voiceParams: VoiceParams,
  segPath: string,
): Promise<string> {
  try {
    return await node.breaker.call(() =>
      tts.synthesize(segment, voiceName, voiceParams, segPath),
    );
  } catch (err: any) {
    // 熔断器开启 → 直接抛出，让外层切换 Provider
    if (err instanceof CircuitBreakerOpenError) throw err;

    const errorMsg = err?.message || '';
    // 不可恢复错误 → 直接抛出（如认证失败、参数错误）
    if (!config.models.shouldSwitchModel(err)) throw err;

    // 可恢复错误：延迟后重试1次（与 LLM 相同的 2s 延迟）
    ttsLogger.warn(`片段合成失败，2s后重试: ${errorMsg.substring(0, 80)}`);
    await new Promise(resolve => setTimeout(resolve, 2000));

    return await node.breaker.call(() =>
      tts.synthesize(segment, voiceName, voiceParams, segPath),
    );
  }
}

/**
 * 获取音频状态（基于文件系统）
 */
export async function getAudioStatus(audioId: string): Promise<{ status: string; audio?: any }> {
  const audioDir = path.join(config.upload.dir, audioId);
  const outputPath = path.join(audioDir, 'output.mp3');
  const failedMarker = path.join(audioDir, 'failed');

  // 检查是否失败
  if (fs.existsSync(failedMarker)) {
    const errorMsg = fs.readFileSync(failedMarker, 'utf-8');
    return { status: 'failed', audio: { error: errorMsg } };
  }

  // 检查是否生成完成
  if (fs.existsSync(outputPath)) {
    const stats = fs.statSync(outputPath);
    const duration = await AudioMerger.getDuration(outputPath);
    return {
      status: 'completed',
      audio: {
        audioUrl: `/uploads/${audioId}/output.mp3`,
        audioDuration: duration,
        audioSize: stats.size,
      },
    };
  }

  // 检查目录是否存在
  if (fs.existsSync(audioDir)) {
    // 检查是否为僵尸任务：目录存在超过 5 分钟但没有任何文件
    const dirStat = fs.statSync(audioDir);
    const dirAge = Date.now() - dirStat.mtimeMs;
    const files = fs.readdirSync(audioDir);

    if (files.length === 0 && dirAge > 120000) {
      // 空目录且超过 2 分钟，判定为僵尸任务
      console.warn(`⚠️ 检测到僵尸任务: ${audioId}，目录创建于 ${Math.round(dirAge / 1000)}s 前，无任何文件`);
      // 更新数据库状态为失败
      try {
        await prisma.audioRecord.update({
          where: { audioId },
          data: { status: 'failed', errorMsg: '任务超时：生成进程异常终止' },
        });
      } catch (e) {}
      return { status: 'failed', audio: { error: '生成进程异常终止' } };
    }

    return { status: 'processing' };
  }

  // 不存在
  return { status: 'not_found' };
}

// 获取可用音色（统一10个音色）
export function getVoices(): Voice[] {
  return UNIFIED_VOICES;
}

// ============ LRC 歌词生成 ============

/** 格式化秒数为 [MM:SS.XX] */
function formatLrcTimestamp(seconds: number): string {
  const m = Math.floor(seconds / 60);
  const s = seconds % 60;
  const cs = Math.round((s - Math.floor(s)) * 100);
  const sec = Math.floor(s);
  return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${cs.toString().padStart(2, '0')}`;
}

/** 清洗文本中的换行符（LRC每行只能有一个时间戳，文本中不能含换行） */
function sanitizeLrcText(s: string): string {
  return s.replace(/\n+/g, ' ').trim();
}

/** 统计可见字符数（去除空白） */
function countVisible(s: string): number {
  return s.replace(/\s/g, '').length;
}

/** 最大每行字符数（超过则进一步拆分） */
const MAX_CHARS_PER_LINE = 50;

/**
 * 将文本拆分为适合 LRC 显示的小句子
 * 策略：
 * 1. 先按 Markdown 标题（#/##）切分，标题独立一行
 * 2. 按句末标点（。！？）切分
 * 3. 按分号/冒号（；：）切分
 * 4. 按逗号（，,）切分
 * 5. 按换行切分
 * 6. 兜底：按固定字符数切分
 * 每行不超过 MAX_CHARS_PER_LINE 字符
 */
function splitIntoLrcSentences(text: string): string[] {
  const result: string[] = [];
  const maxChars = MAX_CHARS_PER_LINE;

  // 第一步：按 Markdown 标题切分，标题独立成行
  const titleParts = text.split(/(?=^#{1,3}\s)/m);
  
  for (const part of titleParts) {
    const trimmed = part.trim();
    if (!trimmed) continue;

    // 如果是标题行（单独一行）
    const titleMatch = trimmed.match(/^(#{1,3}\s+.+?)(\n|$)/);
    if (titleMatch) {
      const titleLine = titleMatch[1].trim();
      const rest = trimmed.substring(titleMatch[0].length).trim();
      
      // 标题单独一行
      result.push(titleLine);
      
      // 处理标题后面的正文
      if (rest) {
        result.push(...splitBodyIntoSentences(rest, maxChars));
      }
    } else {
      // 没有标题，直接处理正文
      result.push(...splitBodyIntoSentences(trimmed, maxChars));
    }
  }

  return result.filter(s => countVisible(s) > 0);
}

/**
 * 将正文拆分为小句子
 * 优先级：句号 > 分号/冒号 > 逗号 > 换行 > 固定长度
 */
function splitBodyIntoSentences(text: string, maxChars: number): string[] {
  const result: string[] = [];

  // 先按段落（空行/换行）粗分
  const paragraphs = text.split(/\n+/).filter(p => p.trim());
  
  for (const para of paragraphs) {
    const trimmed = para.trim();
    if (!trimmed) continue;

    // 如果整段就小于 maxChars，直接加入
    if (countVisible(trimmed) <= maxChars) {
      result.push(trimmed);
      continue;
    }

    // 按句末标点切分（中文：。！？，英文：.!?）
    let sentences = splitByPunctuation(trimmed, /[。！？.!?]+/, maxChars);

    // 如果切分后仍有超长句子，按顿号再切
    sentences = furtherSplit(sentences, /[、]/, maxChars);
    
    // 兜底：强制按字符数切分
    sentences = forceSplitByCharCount(sentences, maxChars);
    
    result.push(...sentences);
  }

  return result;
}

/** 按指定标点切分，标点附在前面那句末尾 */
function splitByPunctuation(text: string, punctRegex: RegExp, maxChars: number): string[] {
  const parts: string[] = [];
  let remaining = text;

  while (remaining.length > 0) {
    // 查找下一个标点位置
    const match = remaining.match(punctRegex);
    if (!match || match.index === undefined) {
      // 没有更多标点，剩余部分整体加入
      parts.push(remaining);
      break;
    }

    const cutPos = match.index + match[0].length;
    const sentence = remaining.substring(0, cutPos).trim();

    if (sentence) {
      parts.push(sentence);
    }
    remaining = remaining.substring(cutPos).trim();
  }

  return parts;
}


/** 对已切分的句子，如果某些句子仍然超长，用更细粒度的标点再切 */
function furtherSplit(sentences: string[], punctRegex: RegExp, maxChars: number): string[] {
  const result: string[] = [];
  for (const s of sentences) {
    if (countVisible(s) <= maxChars) {
      result.push(s);
      continue;
    }
    // 用更细粒度标点再切
    const subParts = splitByPunctuation(s, punctRegex, maxChars);
    result.push(...subParts);
  }
  return result;
}

/** 兜底：强制按字符数切分超长句子 */
function forceSplitByCharCount(sentences: string[], maxChars: number): string[] {
  const result: string[] = [];
  for (const s of sentences) {
    if (countVisible(s) <= maxChars) {
      result.push(s);
      continue;
    }
    // 按可见字符数强制切分
    let buf = '';
    let visibleCount = 0;
    for (const ch of s) {
      buf += ch;
      if (!/\s/.test(ch)) visibleCount++;
      if (visibleCount >= maxChars) {
        result.push(buf.trim());
        buf = '';
        visibleCount = 0;
      }
    }
    if (buf.trim()) result.push(buf.trim());
  }
  return result;
}

/**
 * 生成 LRC 歌词时间轴
 * 
 * 核心算法：**按句子拆分 + 均匀语速分配**
 * 
 * 原理：
 * - TTS 生成的音频语速相对均匀
 * - 用 总时长/总字数 得到真实平均语速（秒/字）
 * - 每句时长 = 该句字数 × 平均语速
 * - 这样保证所有句子时间之和 = 总时长，且短句少分时间、长句多分时间
 */
export function generateLrc(text: string, duration: number, audioPath?: string): string {
  if (!text || duration <= 0) return '';

  try {
    const sentences = splitIntoLrcSentences(text);
    if (sentences.length === 0) return '';

    ttsLogger.lrc(`拆分: ${sentences.length} 句, 总时长=${duration}s`);

    // 计算总可见字符数
    const totalChars = sentences.reduce((sum, s) => sum + countVisible(s), 0);
    if (totalChars === 0) return '';

    // 计算句间停顿（TTS 引擎在句号、换行等位置天然有停顿）
    // 句间停顿: 0.25s/句, 段落间停顿: 0.6s/段
    let totalPause = 0;
    for (let i = 0; i < sentences.length - 1; i++) {
      const thisSentence = sentences[i].trim();
      const nextSentence = sentences[i + 1].trim();
      // 段落间：当前句以空行结尾，或下一句是标题
      if (!thisSentence || !nextSentence || /^#{1,3}\s/.test(nextSentence)) {
        totalPause += 0.6;
      } else {
        totalPause += 0.25;
      }
    }
    // 末尾停顿
    totalPause += 0.5;

    // 有效朗读时长 = 总时长 - 停顿时间（至少保留 70% 给朗读）
    const speechDuration = Math.max(duration * 0.7, duration - totalPause);
    const speechRate = speechDuration / totalChars; // 秒/字
    ttsLogger.lrc(`语速: ${speechRate.toFixed(3)}s/字, 总字数=${totalChars}, 停顿=${totalPause.toFixed(1)}s, 有效朗读=${speechDuration.toFixed(1)}s`);

    const lines: string[] = [];
    let currentTime = 0;

    for (let i = 0; i < sentences.length; i++) {
      const charCount = countVisible(sentences[i]);
      const isTitle = /^#{1,3}\s/.test(sentences[i].trim());
      // 标题行语速较慢（通常有停顿），给予 1.5 倍时长系数
      const titleBoost = isTitle ? 1.5 : 1.0;
      const lineDuration = charCount * speechRate * titleBoost;

      lines.push(`[${formatLrcTimestamp(currentTime)}] ${sanitizeLrcText(sentences[i])}`);
      
      ttsLogger.lrc(`[${formatLrcTimestamp(currentTime)}] ${charCount}字 ${sentences[i].substring(0, 30)}...`);
      
      currentTime += lineDuration;
    }

    // 由于标题行增加了时长系数，实际计算总时长可能超过音频时长
    // 用线性缩放将所有时间戳归一化到 [0, duration] 区间
    if (currentTime > duration && lines.length > 0) {
      const scaleFactor = duration / currentTime;
      const rescaledLines: string[] = [];
      let rescaledTime = 0;
      for (let i = 0; i < sentences.length; i++) {
        const charCount = countVisible(sentences[i]);
        const isTitle = /^#{1,3}\s/.test(sentences[i].trim());
        const titleBoost = isTitle ? 1.5 : 1.0;
        const rawLineDuration = charCount * speechRate * titleBoost;
        const scaledLineDuration = rawLineDuration * scaleFactor;
        rescaledLines.push(`[${formatLrcTimestamp(rescaledTime)}] ${sanitizeLrcText(sentences[i])}`);
        rescaledTime += scaledLineDuration;
      }
      ttsLogger.lrc(`标题补偿缩放: 原始总时长=${currentTime.toFixed(2)}s, 缩放因子=${scaleFactor.toFixed(3)}`);
      return rescaledLines.join('\n');
    }

    // 确保最后一行不超过总时长
    ttsLogger.lrc(`完成: ${lines.length} 行, 末尾时间=${currentTime.toFixed(2)}s, 音频时长=${duration}s`);

    return lines.join('\n');
  } catch (err: any) {
    ttsLogger.error('generateLrc 异常', err as Error);
    console.error(`❌ generateLrc 异常:`, (err as Error).message);
    return buildSimpleLrc(text, duration);
  }
}

/** 最简降级：按句号或换行分割，再不行按固定长度切分 */
function buildSimpleLrc(text: string, duration: number): string {
  // 中英文句末标点（句号、感叹号、问号）
  let sentences = text.split(/(?<=[。！？.!?])\s*/).filter(s => s.trim());
  if (sentences.length <= 1) {
    sentences = text.split(/\n+/).filter(s => s.trim());
  }
  if (sentences.length <= 1 && text.length > 20) {
    // 英文按句号切分后再按固定长度兜底
    const englishSentences = text.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0);
    if (englishSentences.length > 1) {
      sentences = englishSentences;
    } else {
      const chunkSize = 40;
      sentences = [];
      for (let i = 0; i < text.length; i += chunkSize) {
        sentences.push(text.substring(i, i + chunkSize));
      }
    }
  }
  if (sentences.length === 0) return '';

  const totalChars = sentences.reduce((sum, s) => sum + countVisible(s), 0);
  if (totalChars === 0) return '';

  const speechRate = duration / totalChars;
  const lines: string[] = [];
  let t = 0;
  for (const s of sentences) {
    lines.push(`[${formatLrcTimestamp(t)}] ${sanitizeLrcText(s)}`);
    t += countVisible(s) * speechRate;
  }
  return lines.join('\n');
}

// 获取可用 TTS 服务商（从注册表读取）
export function getAvailableProviders() {
  const registry = getTtsRegistry();
  return registry.listEnabled().map(n => ({
    id: n.provider.vendor,
    name: n.provider.name,
    enabled: n.enabled,
    state: n.breaker.getState(),
  }));
}

// 生成预览音频（短文本）
export async function generatePreview(
  voiceId: string,
  voiceParams?: { speed?: number; pitch?: number; volume?: number },
  providerType?: 'aliyun' | 'minimax'
): Promise<{ audioId: string; audioUrl: string }> {
  const previewText = '你好，欢迎使用AI有声书';
  const audioId = 'preview-' + uuidv4();
  const audioDir = path.join(config.upload.dir, audioId);

  if (!fs.existsSync(audioDir)) {
    fs.mkdirSync(audioDir, { recursive: true });
  }

  const params = {
    speed: voiceParams?.speed ?? 1.0,
    pitch: voiceParams?.pitch ?? 0,
    volume: voiceParams?.volume ?? 50,
  };

  console.log(`[TTS Preview] params: speed=${params.speed}, pitch=${params.pitch}, volume=${params.volume}`);

  const registry = getTtsRegistry();

  // 优先使用的 Provider
  const preferredName = providerType === 'minimax' ? 'minimax-tts' : providerType === 'aliyun' ? 'bailian-tts' : undefined;
  const startNode = preferredName ? registry.get(preferredName) : undefined;

  // 按优先级尝试所有可用 Provider
  const candidates = startNode
    ? [startNode, ...registry.listAvailable().filter(n => n !== startNode)]
    : registry.listAvailable();

  let lastError: Error | null = null;
  for (const node of candidates) {
    if (node.exhausted || node.breaker.isOpen()) continue;

    const tts = node.provider;
    const voiceName = mapToProviderVoice(voiceId, tts.vendor);

    if (tts.mode === 'mock') {
      const mockPath = path.join(audioDir, 'preview.mp3');
      fs.writeFileSync(mockPath, Buffer.alloc(1024));
      return { audioId, audioUrl: `/uploads/${audioId}/preview.mp3` };
    }

    try {
      // 先创建空文件让 aliyun 下载能定位目录
      const ext = tts.vendor.startsWith('minimax') ? 'mp3' : 'wav';
      const outputPath = path.join(audioDir, `preview.${ext}`);
      if (!fs.existsSync(outputPath)) {
        fs.writeFileSync(outputPath, Buffer.alloc(0));
      }
      const actualPath = await node.breaker.call(() => tts.synthesize(previewText, voiceName, params, outputPath));
      const audioUrl = await storageService.uploadAudio(actualPath, audioId);
      console.log(`✅ [TTS Preview] ${tts.name} 生成成功`);
      return { audioId, audioUrl };
    } catch (error: any) {
      console.error(`❌ [TTS Preview] ${tts.name} 失败:`, error.message);
      lastError = error;

      // 额度耗尽标记
      if (['quota', 'balance', 'insufficient', 'usage limit'].some(k => error.message?.toLowerCase().includes(k))) {
        registry.markExhausted(tts.name, error.message, 4 * 60 * 60 * 1000);
      }
      continue;
    }
  }

  console.error('❌ 预览生成失败:', lastError?.message);
  throw lastError || new Error('所有 TTS Provider 都已尝试');
}

// ============ 初始化 ============
// 首次调用时初始化注册表并启动健康检查
startTtsHealthCheck();