#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AUDIO-MIXER
混音器 — 配音、BGM、音效、原视频音轨混音，支持对白时自动压低BGM。

功能:
1. 输入多轨音频 (对白/BGM/音效/原视频音轨)
2. 自动检测对白时段，压低BGM音量 (ducking)
3. 混音输出
4. 支持批量处理

依赖:
  ffmpeg (需要系统安装)
  pip install numpy  # 用于音频分析

用法:
  python audio-mixer.py --dialogue dialogue.mp3 --bgm bgm.mp3 --output mix.mp3
  python audio-mixer.py --config mix-config.json
"""

import os
import sys
import json
import argparse
import subprocess
from pathlib import Path
from datetime import datetime

PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "engines"))


class AudioMixer:
    """混音器"""

    def __init__(self, ffmpeg_path="ffmpeg"):
        self.ffmpeg_path = ffmpeg_path
        self._check_ffmpeg()

    def _check_ffmpeg(self):
        """检查 FFmpeg 是否可用"""
        try:
            result = subprocess.run(
                [self.ffmpeg_path, "-version"],
                capture_output=True,
                text=True,
                timeout=10
            )
            if result.returncode == 0:
                version_line = result.stdout.split("\n")[0]
                print(f"✅ FFmpeg 可用: {version_line}")
                return True
        except Exception as e:
            pass

        print(f"❌ FFmpeg 不可用: {self.ffmpeg_path}")
        print(f"   安装方法: brew install ffmpeg (macOS) 或 apt install ffmpeg (Ubuntu)")
        return False

    def mix_audio(self, dialogue=None, bgm=None, sfx=None, original=None,
                  output_path=None, ducking_threshold=-20, ducking_level=-10):
        """
        混音

        参数:
          dialogue: 对白音轨路径
          bgm: BGM 音轨路径
          sfx: 音效音轨路径 (可选，支持多个，传入列表)
          original: 原视频音轨路径 (可选)
          output_path: 输出路径
          ducking_threshold: 对白检测阈值 (dB，默认 -20dB)
          ducking_level: BGM 压低量 (dB，默认 -10dB = 压低到原来的 1/10)

        返回:
          {
            "success": bool,
            "output_path": str,
            "tracks_used": list,
            "warnings": list
          }
        """
        print(f"\n🎵 混音")
        tracks = []
        warnings = []

        # 检查输入文件
        if dialogue and Path(dialogue).exists():
            tracks.append(("dialogue", dialogue))
            print(f"  对白: {Path(dialogue).name}")
        elif dialogue:
            warnings.append(f"对白文件不存在: {dialogue}")

        if bgm and Path(bgm).exists():
            tracks.append(("bgm", bgm))
            print(f"  BGM: {Path(bgm).name}")
        elif bgm:
            warnings.append(f"BGM 文件不存在: {bgm}")

        if sfx:
            if isinstance(sfx, str):
                sfx = [sfx]
            for s in sfx:
                if Path(s).exists():
                    tracks.append(("sfx", s))
                    print(f"  音效: {Path(s).name}")
                else:
                    warnings.append(f"音效文件不存在: {s}")

        if original and Path(original).exists():
            tracks.append(("original", original))
            print(f"  原视频音轨: {Path(original).name}")
        elif original:
            warnings.append(f"原视频音轨不存在: {original}")

        if len(tracks) == 0:
            return {"success": False, "error": "没有可用的音轨"}

        # 确定输出路径
        if output_path is None:
            # 默认输出到对白文件同目录
            if dialogue:
                output_path = Path(dialogue).parent / f"{Path(dialogue).stem}_mixed.mp3"
            else:
                output_path = PROJECT_ROOT / "outputs" / "mixed_audio.mp3"

        output_path = Path(output_path)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # 生成 FFmpeg 命令
        if len(tracks) == 1:
            # 只有一条音轨，直接复制
            print(f"\n  ⚠️  只有一条音轨，直接复制")
            import shutil
            shutil.copy2(tracks[0][1], output_path)
            return {
                "success": True,
                "output_path": str(output_path),
                "tracks_used": [t[0] for t in tracks],
                "warnings": warnings
            }

        # 多条音轨，需要混音
        print(f"\n  🔧 生成 FFmpeg 命令...")

        if bgm and dialogue:
            # 有 BGM 和对白 → 使用 ducking
            print(f"  启用自动压低 BGM (ducking)")
            print(f"    对白检测阈值: {ducking_threshold}dB")
            print(f"    BGM 压低量: {ducking_level}dB")
            result = self._mix_with_ducking(
                dialogue, bgm, sfx, original, output_path,
                ducking_threshold, ducking_level
            )
        else:
            # 无 BGM 或 无对白 → 直接混音
            print(f"  直接混音 (无 ducking)")
            result = self._mix_simple(
                [t[1] for t in tracks],
                output_path
            )

        return result

    def _mix_with_ducking(self, dialogue, bgm, sfx, original, output_path,
                            ducking_threshold, ducking_level):
        """带侧链压缩(ducking) + loudnorm 响度标准化的混音"""

        inputs = []
        filter_complex = []
        input_idx = 0
        track_map = {}  # track_name → filter_label

        # 输入文件
        if dialogue:
            inputs.extend(["-i", dialogue])
            track_map["dialogue"] = input_idx
            input_idx += 1
        if bgm:
            inputs.extend(["-i", bgm])
            track_map["bgm"] = input_idx
            input_idx += 1
        if sfx:
            if isinstance(sfx, str):
                sfx = [sfx]
            for i, s in enumerate(sfx):
                inputs.extend(["-i", s])
                track_map[f"sfx{i}"] = input_idx
                input_idx += 1
        if original:
            inputs.extend(["-i", original])
            track_map["original"] = input_idx
            input_idx += 1

        # 侧链压缩: 对白作为 sidechain，BGM 被压缩
        # sidechaincompress 参数: threshold 阈值, ratio 压缩比, attack 起音, release 释音
        if "dialogue" in track_map and "bgm" in track_map:
            d_idx = track_map["dialogue"]
            b_idx = track_map["bgm"]
            # 对白音轨通过
            filter_complex.append(f"[{d_idx}:a]aformat=sample_fmts=fltp:channel_layouts=stereo[d_pre]")
            filter_complex.append(f"[{b_idx}:a]aformat=sample_fmts=fltp:channel_layouts=stereo[b_pre]")
            # 侧链压缩: BGM 被对白压缩
            filter_complex.append(
                f"[b_pre][d_pre]sidechaincompress="
                f"threshold={ducking_threshold/20:.3f}:ratio=4:"
                f"attack=25:release=200:level_sc={ducking_level/20:.3f}[bgm_comp]"
            )
            filter_complex.append(f"[d_pre]volume=1[dialogue_out]")
            bgm_label = "bgm_comp"
        else:
            bgm_label = None

        # 音效/原视频音轨
        mix_inputs = []
        if "dialogue" in track_map:
            mix_inputs.append("[dialogue_out]" if bgm_label else f"[{track_map['dialogue']}:a]")
        if bgm_label:
            mix_inputs.append(f"[{bgm_label}]")
        elif "bgm" in track_map:
            mix_inputs.append(f"[{track_map['bgm']}:a]")
        for key in track_map:
            if key.startswith("sfx"):
                filter_complex.append(f"[{track_map[key]}:a]volume=0.8[s_{key}]")
                mix_inputs.append(f"[s_{key}]")
        if "original" in track_map:
            filter_complex.append(f"[{track_map['original']}:a]volume=0.4[orig_out]")
            mix_inputs.append("[orig_out]")

        # 混音
        n_inputs = len(mix_inputs)
        amix_inputs = "".join(mix_inputs)
        filter_complex.append(
            f"{amix_inputs}amix=inputs={n_inputs}:duration=first:dropout_transition=3,"
            # 响度标准化: EBU R128, 目标 -14 LUFS (短视频平台标准)
            f"loudnorm=I=-14:LRA=11:TP=-1.5,"
            # 峰值限幅保护
            f"alimiter=limit=-1.0dB:attack=5:release=50[aout]"
        )

        cmd = [
            self.ffmpeg_path, "-y",
            *inputs,
            "-filter_complex", ";".join(filter_complex),
            "-map", "[aout]",
            "-codec:a", "libmp3lame",
            "-b:a", "192k",
            str(output_path)
        ]

        print(f"  📤 执行 FFmpeg...")
        print(f"    命令: ffmpeg {' '.join(cmd[1:5])}...")

        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=300
            )

            if result.returncode == 0:
                print(f"  ✅ 混音完成: {output_path.name}")
                return {
                    "success": True,
                    "output_path": str(output_path),
                    "tracks_used": list(track_map.keys()),
                    "warnings": [],
                    "method": "sidechain_compress+loudnorm"
                }
            else:
                print(f"  ❌ FFmpeg 失败: {result.stderr[-500:]}")
                return {
                    "success": False,
                    "error": result.stderr[-500:],
                    "returncode": result.returncode
                }

        except subprocess.TimeoutExpired:
            print(f"  ❌ FFmpeg 超时 (5分钟)")
            return {"success": False, "error": "Timeout"}

        except Exception as e:
            print(f"  ❌ 执行失败: {e}")
            return {"success": False, "error": str(e)}

    def _mix_simple(self, input_files, output_path):
        """简单混音 (无 ducking)"""
        inputs = []
        for f in input_files:
            inputs.extend(["-i", f])

        # amix 滤镜混音
        filter_complex = f"amix=inputs={len(input_files)}:duration=first:dropout_transition=2"

        cmd = [
            self.ffmpeg_path,
            "-y",
            *inputs,
            "-filter_complex", filter_complex,
            "-codec:a", "libmp3lame",
            "-b:a", "192k",
            str(output_path)
        ]

        print(f"  📤 执行 FFmpeg (简单混音)...")

        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=300
            )

            if result.returncode == 0:
                print(f"  ✅ 混音完成: {output_path.name}")
                return {
                    "success": True,
                    "output_path": str(output_path),
                    "method": "simple_mix"
                }
            else:
                print(f"  ❌ FFmpeg 失败: {result.stderr[-500:]}")
                return {"success": False, "error": result.stderr[-500:]}

        except Exception as e:
            print(f"  ❌ 执行失败: {e}")
            return {"success": False, "error": str(e)}

    def extract_audio_from_video(self, video_path, output_path=None):
        """
        从视频中提取音轨
        """
        if output_path is None:
            output_path = Path(video_path).parent / f"{Path(video_path).stem}.mp3"

        output_path = Path(output_path)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        cmd = [
            self.ffmpeg_path,
            "-y",
            "-i", video_path,
            "-vn",  # 不要视频
            "-codec:a", "libmp3lame",
            "-b:a", "192k",
            str(output_path)
        ]

        print(f"\n📤 从视频提取音轨: {Path(video_path).name}")

        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=300
            )

            if result.returncode == 0:
                print(f"  ✅ 提取完成: {output_path.name}")
                return {"success": True, "output_path": str(output_path)}
            else:
                print(f"  ❌ 提取失败: {result.stderr[-500:]}")
                return {"success": False, "error": result.stderr[-500:]}

        except Exception as e:
            print(f"  ❌ 执行失败: {e}")
            return {"success": False, "error": str(e)}

    def batch_mix(self, config_file):
        """
        批量混音 (从配置文件)

        config_file JSON 格式:
          {
            "output_dir": "./outputs/mixed/",
            "tracks": [
              {
                "dialogue": "dialogue/ep01-shot01.mp3",
                "bgm": "bgm/ep01-theme.mp3",
                "sfx": ["sfx/door-open.mp3"],
                "output": "mixed/ep01-shot01.mp3"
              },
              ...
            ]
          }
        """
        config_file = Path(config_file)
        if not config_file.exists():
            return {"success": False, "error": f"配置文件不存在: {config_file}"}

        with open(config_file, "r", encoding="utf-8") as f:
            config = json.load(f)

        output_dir = Path(config.get("output_dir", "./outputs/mixed/"))
        output_dir.mkdir(parents=True, exist_ok=True)

        tracks = config.get("tracks", [])
        print(f"\n📦 批量混音: {len(tracks)} 个任务")

        results = []
        for i, track_config in enumerate(tracks):
            print(f"\n  进度: [{i+1}/{len(tracks)}]")

            result = self.mix_audio(
                dialogue=track_config.get("dialogue"),
                bgm=track_config.get("bgm"),
                sfx=track_config.get("sfx"),
                original=track_config.get("original"),
                output_path=track_config.get("output", str(output_dir / f"mixed-{i+1:03d}.mp3"))
            )
            results.append(result)

        # 统计
        success_count = sum(1 for r in results if r.get("success"))
        print(f"\n✅ 批量完成: {success_count}/{len(results)} 成功")

        # 保存报告
        report_path = output_dir / "mix_report.json"
        with open(report_path, "w", encoding="utf-8") as f:
            json.dump({
                "total": len(results),
                "success": success_count,
                "results": results,
                "generated_at": datetime.now().isoformat()
            }, f, ensure_ascii=False, indent=2)

        print(f"  报告已保存: {report_path}")

        return results


def main():
    parser = argparse.ArgumentParser(description="AUDIO-MIXER")
    parser.add_argument("--dialogue", type=str, help="对白音轨路径")
    parser.add_argument("--bgm", type=str, help="BGM 音轨路径")
    parser.add_argument("--sfx", type=str, nargs="+", help="音效音轨路径 (多个)")
    parser.add_argument("--original", type=str, help="原视频音轨路径")
    parser.add_argument("--output", type=str, help="输出路径")
    parser.add_argument("--config", type=str, help="批量混音配置文件")
    parser.add_argument("--ducking-threshold", type=float, default=-20, help="对白检测阈值 (dB)")
    parser.add_argument("--ducking-level", type=float, default=-10, help="BGM 压低量 (dB)")
    parser.add_argument("--extract-from-video", type=str, help="从视频提取音轨")
    parser.add_argument("--ffmpeg-path", type=str, default="ffmpeg", help="FFmpeg 路径")

    args = parser.parse_args()

    mixer = AudioMixer(ffmpeg_path=args.ffmpeg_path)

    if args.extract_from_video:
        # 提取音轨模式
        result = mixer.extract_audio_from_video(args.extract_from_video, args.output)
        sys.exit(0 if result["success"] else 1)

    if args.config:
        # 批量模式
        results = mixer.batch_mix(args.config)
        sys.exit(0 if all(r.get("success") for r in results) else 1)

    if not args.dialogue and not args.bgm and not args.original:
        parser.print_help()
        sys.exit(1)

    # 单文件模式
    result = mixer.mix_audio(
        dialogue=args.dialogue,
        bgm=args.bgm,
        sfx=args.sfx,
        original=args.original,
        output_path=args.output,
        ducking_threshold=args.ducking_threshold,
        ducking_level=args.ducking_level
    )

    if result["success"]:
        print(f"\n✅ 成功: {result['output_path']}")
        sys.exit(0)
    else:
        print(f"\n❌ 失败: {result.get('error', 'Unknown error')}")
        sys.exit(1)


if __name__ == "__main__":
    main()
