"""
视频剪辑模式处理器
支持四种剪辑模式：一镜到底、画中画、口播、口播+画中画
"""

import logging
import os
import subprocess
import sys
import tempfile
from dataclasses import dataclass

if sys.version_info >= (3, 11):
    from enum import StrEnum
else:
    from enum import Enum

    class StrEnum(str, Enum):
        pass


from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)


# 从 domain 层导入 EditingMode，避免重复定义
from packages.domain.editing_mode import EditingMode


class PIPPosition(StrEnum):
    """画中画位置枚举"""

    TOP_LEFT = "top_left"
    TOP_RIGHT = "top_right"
    BOTTOM_LEFT = "bottom_left"
    BOTTOM_RIGHT = "bottom_right"


@dataclass
class EditingModeConfig:
    """剪辑模式配置"""

    mode: EditingMode
    output_width: int = 1280
    output_height: int = 720
    output_fps: int = 25
    pip_position: PIPPosition = PIPPosition.TOP_RIGHT
    pip_scale: float = 0.25  # 画中画占主画面的比例
    transition_duration: float = 0.5  # 转场时长（秒）
    output_codec: str = "libx264"
    output_preset: str = "medium"
    output_crf: int = 23


class EditingModeProcessor:
    """剪辑模式处理器"""

    def __init__(self, config: EditingModeConfig, work_dir: Optional[str] = None):
        """
        初始化剪辑模式处理器

        Args:
            config: 剪辑模式配置
            work_dir: 工作目录，默认使用系统临时目录
        """
        self.config = config
        self.work_dir = work_dir or tempfile.gettempdir()
        self._ffmpeg_bin = "ffmpeg"
        self._ffprobe_bin = "ffprobe"

    def process(
        self,
        video_paths: list[str],
        audio_path: Optional[str] = None,
        output_path: Optional[str] = None,
    ) -> str:
        """
        根据模式处理视频，返回输出文件路径

        Args:
            video_paths: 视频素材路径列表
            audio_path: 音频路径（用于口播模式）
            output_path: 输出文件路径，默认自动生成

        Returns:
            输出文件路径
        """
        if not video_paths:
            raise ValueError("video_paths cannot be empty")

        self._validate_inputs(video_paths, audio_path)

        if output_path is None:
            output_path = self._generate_output_path()

        logger.info(f"Processing videos with mode: {self.config.mode}, count: {len(video_paths)}")

        try:
            if self.config.mode == EditingMode.ONE_TAKE:
                return self._one_take(video_paths, output_path)
            elif self.config.mode == EditingMode.PIP:
                return self._pip(video_paths, output_path)
            elif self.config.mode == EditingMode.VOICE_OVER:
                return self._voice_over(video_paths, audio_path, output_path)
            elif self.config.mode == EditingMode.VOICE_PIP:
                return self._voice_pip(video_paths, audio_path, output_path)
            else:
                raise ValueError(f"Unsupported editing mode: {self.config.mode}")
        except Exception as e:
            logger.error(f"Error processing videos: {e}")
            raise

    def _validate_inputs(self, video_paths: list[str], audio_path: Optional[str]) -> None:
        """验证输入文件"""
        for path in video_paths:
            if not os.path.exists(path):
                raise FileNotFoundError(f"Video file not found: {path}")
            if not os.path.getsize(path) > 0:
                raise ValueError(f"Video file is empty: {path}")

        if audio_path and not os.path.exists(audio_path):
            raise FileNotFoundError(f"Audio file not found: {audio_path}")

    def _generate_output_path(self) -> str:
        """生成输出文件路径"""
        os.makedirs(self.work_dir, exist_ok=True)
        return os.path.join(self.work_dir, f"output_{self.config.mode}_{os.getpid()}.mp4")

    def _run_ffmpeg(self, command: list[str], capture_output: bool = True) -> tuple:
        """执行 FFmpeg 命令"""
        logger.debug(f"Running FFmpeg: {' '.join(command)}")
        try:
            result = subprocess.run(
                command,
                check=True,
                stdout=subprocess.PIPE if capture_output else None,
                stderr=subprocess.PIPE if capture_output else None,
                text=capture_output,
            )
            return result.stdout or "", result.stderr or ""
        except subprocess.CalledProcessError as e:
            stderr = e.stderr.decode() if e.stderr else str(e)
            logger.error(f"FFmpeg error: {stderr}")
            raise RuntimeError(f"FFmpeg execution failed: {stderr}") from e

    def _get_video_info(self, video_path: str) -> dict:
        """获取视频信息"""
        try:
            result = subprocess.run(
                [
                    self._ffprobe_bin,
                    "-v",
                    "error",
                    "-show_entries",
                    "stream=width,height,r_frame_rate,duration,codec_name",
                    "-show_entries",
                    "format=duration,size",
                    "-of",
                    "json",
                    video_path,
                ],
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
            import json

            data = json.loads(result.stdout)
            streams = data.get("streams", [{}])
            video_stream = next((s for s in streams if s.get("codec_type") == "video"), streams[0] if streams else {})
            fmt = data.get("format", {})

            fps_str = video_stream.get("r_frame_rate", "25/1")
            fps_parts = fps_str.split("/")
            fps = float(fps_parts[0]) / float(fps_parts[1]) if len(fps_parts) == 2 else float(fps_parts[0])

            return {
                "width": int(video_stream.get("width", 0)),
                "height": int(video_stream.get("height", 0)),
                "fps": fps,
                "duration": float(fmt.get("duration", 0)),
                "codec": video_stream.get("codec_name", "unknown"),
                "size": int(fmt.get("size", 0)),
            }
        except Exception as e:
            logger.warning(f"Failed to get video info for {video_path}: {e}")
            return {"width": 0, "height": 0, "fps": 25, "duration": 0, "codec": "unknown", "size": 0}

    def _get_pip_position_offset(
        self, main_width: int, main_height: int, pip_width: int, pip_height: int
    ) -> tuple[int, int]:
        """获取画中画位置偏移量"""
        margin = 10
        position_offsets = {
            PIPPosition.TOP_LEFT: (margin, margin),
            PIPPosition.TOP_RIGHT: (main_width - pip_width - margin, margin),
            PIPPosition.BOTTOM_LEFT: (margin, main_height - pip_height - margin),
            PIPPosition.BOTTOM_RIGHT: (main_width - pip_width - margin, main_height - pip_height - margin),
        }
        return position_offsets.get(self.config.pip_position, position_offsets[PIPPosition.TOP_RIGHT])

    def _normalize_video(self, input_path: str, output_path: str) -> dict:
        """标准化视频格式：先统一帧率，再缩放/填充"""
        command = [
            self._ffmpeg_bin,
            "-y",
            "-i",
            input_path,
            "-r",
            str(self.config.output_fps),  # 先统一帧率
            "-vf",
            f"scale={self.config.output_width}:{self.config.output_height}:force_original_aspect_ratio=decrease,pad={self.config.output_width}:{self.config.output_height}:(ow-iw)/2:(oh-ih)/2,setsar=1",
            "-r",
            str(self.config.output_fps),
            "-c:v",
            self.config.output_codec,
            "-preset",
            self.config.output_preset,
            "-crf",
            str(self.config.output_crf),
            "-pix_fmt",
            "yuv420p",
            "-movflags",
            "+faststart",
            "-an",
            output_path,
        ]
        self._run_ffmpeg(command)
        return self._get_video_info(output_path)

    def _one_take(self, video_paths: list[str], output_path: str) -> str:
        """一镜到底模式：顺序拼接视频，添加淡入淡出转场"""
        if len(video_paths) == 1:
            return self._normalize_video(video_paths[0], output_path)

        normalized_paths = []
        for i, path in enumerate(video_paths):
            normalized = os.path.join(self.work_dir, f"normalized_{i}_{os.getpid()}.mp4")
            self._normalize_video(path, normalized)
            normalized_paths.append(normalized)

        durations = [self._get_video_info(p)["duration"] for p in normalized_paths]

        if len(normalized_paths) <= 5:
            output_path = self._one_take_with_xfade(normalized_paths, durations, output_path)
        else:
            output_path = self._one_take_simple_concat(normalized_paths, output_path)

        for p in normalized_paths:
            try:
                if p != output_path:
                    os.remove(p)
            except Exception as e:
                logger.warning(f"Operation failed in apps/worker/video_processing/editing_modes.py: {e}", exc_info=True)

        return output_path

    def _one_take_with_xfade(self, normalized_paths: list[str], durations: list[float], output_path: str) -> str:
        """使用 xfade 滤镜实现转场"""
        if len(normalized_paths) == 2:
            transition = self.config.transition_duration
            offset1 = durations[0] - transition / 2

            command = [
                self._ffmpeg_bin,
                "-y",
                "-i",
                normalized_paths[0],
                "-i",
                normalized_paths[1],
                "-filter_complex",
                f"[0:v][1:v]xfade=transition=fade:duration={transition}:offset={offset1}[v]",
                "-map",
                "[v]",
                "-c:v",
                self.config.output_codec,
                "-preset",
                self.config.output_preset,
                "-crf",
                str(self.config.output_crf),
                "-pix_fmt",
                "yuv420p",
                output_path,
            ]
            self._run_ffmpeg(command)
            return output_path
        else:
            return self._one_take_simple_concat(normalized_paths, output_path)

    def _one_take_simple_concat(self, normalized_paths: list[str], output_path: str) -> str:
        """使用 concat demuxer 简单拼接"""
        concat_file = os.path.join(self.work_dir, f"concat_list_{os.getpid()}.txt")
        with open(concat_file, "w") as f:
            for path in normalized_paths:
                f.write(f"file '{os.path.abspath(path)}'\n")

        command = [
            self._ffmpeg_bin,
            "-y",
            "-f",
            "concat",
            "-safe",
            "0",
            "-i",
            concat_file,
            "-c",
            "copy",
            output_path,
        ]
        self._run_ffmpeg(command)

        try:
            os.remove(concat_file)
        except Exception as e:
            logger.warning(f"Operation failed in apps/worker/video_processing/editing_modes.py: {e}", exc_info=True)

        return output_path

    def _pip(self, video_paths: list[str], output_path: str) -> str:
        """画中画模式：主视频全屏，后续视频叠加在角落"""
        if not video_paths:
            raise ValueError("No video paths provided")

        main_video = video_paths[0]
        main_normalized = os.path.join(self.work_dir, f"main_{os.getpid()}.mp4")
        main_info = self._normalize_video(main_video, main_normalized)

        if len(video_paths) == 1:
            os.rename(main_normalized, output_path)
            return output_path

        pip_width = int(self.config.output_width * self.config.pip_scale)
        pip_height = int(self.config.output_height * self.config.pip_scale)
        x_offset, y_offset = self._get_pip_position_offset(
            self.config.output_width, self.config.output_height, pip_width, pip_height
        )

        pip_normalized = os.path.join(self.work_dir, f"pip_{os.getpid()}.mp4")
        pip_info = self._get_video_info(video_paths[1])

        if pip_info["duration"] > main_info["duration"]:
            temp_pip = os.path.join(self.work_dir, f"pip_temp_{os.getpid()}.mp4")
            command = [
                self._ffmpeg_bin,
                "-y",
                "-i",
                video_paths[1],
                "-t",
                str(main_info["duration"]),
                "-vf",
                f"scale={pip_width}:{pip_height}",
                "-c:v",
                self.config.output_codec,
                "-preset",
                self.config.output_preset,
                "-crf",
                str(self.config.output_crf),
                "-pix_fmt",
                "yuv420p",
                temp_pip,
            ]
            self._run_ffmpeg(command)
            pip_normalized_input = temp_pip
        else:
            command = [
                self._ffmpeg_bin,
                "-y",
                "-i",
                video_paths[1],
                "-vf",
                f"scale={pip_width}:{pip_height}",
                "-c:v",
                self.config.output_codec,
                "-preset",
                self.config.output_preset,
                "-crf",
                str(self.config.output_crf),
                "-pix_fmt",
                "yuv420p",
                pip_normalized,
            ]
            self._run_ffmpeg(command)
            pip_normalized_input = pip_normalized

        if main_info["duration"] > pip_info["duration"]:
            looped_pip = os.path.join(self.work_dir, f"pip_looped_{os.getpid()}.mp4")
            command = [
                self._ffmpeg_bin,
                "-y",
                "-stream_loop",
                "-1",
                "-i",
                pip_normalized_input,
                "-t",
                str(main_info["duration"]),
                "-vf",
                f"scale={pip_width}:{pip_height}",
                "-c:v",
                self.config.output_codec,
                "-preset",
                self.config.output_preset,
                "-crf",
                str(self.config.output_crf),
                "-pix_fmt",
                "yuv420p",
                looped_pip,
            ]
            self._run_ffmpeg(command)
            pip_normalized_input = looped_pip

        command = [
            self._ffmpeg_bin,
            "-y",
            "-i",
            main_normalized,
            "-i",
            pip_normalized_input,
            "-filter_complex",
            f"[0:v][1:v]overlay={x_offset}:{y_offset}[v]",
            "-map",
            "[v]",
            "-c:v",
            self.config.output_codec,
            "-preset",
            self.config.output_preset,
            "-crf",
            str(self.config.output_crf),
            "-pix_fmt",
            "yuv420p",
            output_path,
        ]
        self._run_ffmpeg(command)

        for temp_file in [main_normalized, pip_normalized]:
            if temp_file and temp_file != output_path:
                try:
                    os.remove(temp_file)
                except Exception as e:
                    logger.warning(
                        f"Operation failed in apps/worker/video_processing/editing_modes.py: {e}", exc_info=True
                    )

        return output_path

    def _voice_over(self, video_paths: list[str], audio_path: Optional[str], output_path: str) -> str:
        """口播模式：背景画面 + 配音"""
        if not audio_path:
            raise ValueError("audio_path is required for VOICE_OVER mode")

        if not video_paths:
            raise ValueError("No background video provided")

        audio_info = self._get_video_info(audio_path)
        audio_duration = audio_info["duration"]

        bg_normalized = os.path.join(self.work_dir, f"bg_{os.getpid()}.mp4")
        bg_info = self._normalize_video(video_paths[0], bg_normalized)

        if bg_info["duration"] < audio_duration:
            looped_bg = os.path.join(self.work_dir, f"bg_looped_{os.getpid()}.mp4")
            command = [
                self._ffmpeg_bin,
                "-y",
                "-stream_loop",
                "-1",
                "-i",
                bg_normalized,
                "-t",
                str(audio_duration),
                "-vf",
                f"scale={self.config.output_width}:{self.config.output_height}",
                "-c:v",
                self.config.output_codec,
                "-preset",
                self.config.output_preset,
                "-crf",
                str(self.config.output_crf),
                "-pix_fmt",
                "yuv420p",
                looped_bg,
            ]
            self._run_ffmpeg(command)
            bg_normalized = looped_bg
        elif bg_info["duration"] > audio_duration:
            temp_bg = os.path.join(self.work_dir, f"bg_trimmed_{os.getpid()}.mp4")
            command = [
                self._ffmpeg_bin,
                "-y",
                "-i",
                bg_normalized,
                "-t",
                str(audio_duration),
                "-c:v",
                "copy",
                temp_bg,
            ]
            self._run_ffmpeg(command)
            bg_normalized = temp_bg

        blurred_bg = os.path.join(self.work_dir, f"bg_blurred_{os.getpid()}.mp4")
        command = [
            self._ffmpeg_bin,
            "-y",
            "-i",
            bg_normalized,
            "-vf",
            f"boxblur=5:5,scale={self.config.output_width}:{self.config.output_height}",
            "-c:v",
            self.config.output_codec,
            "-preset",
            self.config.output_preset,
            "-crf",
            str(self.config.output_crf),
            "-pix_fmt",
            "yuv420p",
            blurred_bg,
        ]
        self._run_ffmpeg(command)

        command = [
            self._ffmpeg_bin,
            "-y",
            "-i",
            blurred_bg,
            "-i",
            audio_path,
            "-filter_complex",
            "[0:v]drawbox=x=0:y=0:w=iw:h=ih:color=black@0.3:t=fill[v]",
            "-map",
            "[v]",
            "-map",
            "1:a",
            "-c:v",
            self.config.output_codec,
            "-preset",
            self.config.output_preset,
            "-crf",
            str(self.config.output_crf),
            "-pix_fmt",
            "yuv420p",
            "-shortest",
            output_path,
        ]
        self._run_ffmpeg(command)

        for temp_file in [bg_normalized, blurred_bg]:
            try:
                if temp_file != output_path:
                    os.remove(temp_file)
            except Exception as e:
                logger.warning(f"Operation failed in apps/worker/video_processing/editing_modes.py: {e}", exc_info=True)

        return output_path

    def _voice_pip(self, video_paths: list[str], audio_path: Optional[str], output_path: str) -> str:
        """口播+画中画模式：口播视频在角落，其他视频作为背景"""
        if not video_paths:
            raise ValueError("No video paths provided")

        if len(video_paths) == 1:
            return self._normalize_video(video_paths[0], output_path)

        voice_video = video_paths[0]
        bg_video = video_paths[1] if len(video_paths) > 1 else video_paths[0]

        voice_normalized = os.path.join(self.work_dir, f"voice_{os.getpid()}.mp4")
        voice_info = self._normalize_video(voice_video, voice_normalized)

        bg_normalized = os.path.join(self.work_dir, f"bg_{os.getpid()}.mp4")
        bg_info = self._normalize_video(bg_video, bg_normalized)

        final_duration = min(voice_info["duration"], bg_info["duration"])

        pip_width = int(self.config.output_width * self.config.pip_scale)
        pip_height = int(self.config.output_height * self.config.pip_scale)
        x_offset, y_offset = self._get_pip_position_offset(
            self.config.output_width, self.config.output_height, pip_width, pip_height
        )

        voice_adjusted = os.path.join(self.work_dir, f"voice_adj_{os.getpid()}.mp4")
        command = [
            self._ffmpeg_bin,
            "-y",
            "-i",
            voice_normalized,
            "-t",
            str(final_duration),
            "-vf",
            f"scale={pip_width}:{pip_height}",
            "-c:v",
            self.config.output_codec,
            "-preset",
            self.config.output_preset,
            "-crf",
            str(self.config.output_crf),
            "-pix_fmt",
            "yuv420p",
            voice_adjusted,
        ]
        self._run_ffmpeg(command)

        bg_adjusted = os.path.join(self.work_dir, f"bg_adj_{os.getpid()}.mp4")
        command = [
            self._ffmpeg_bin,
            "-y",
            "-i",
            bg_normalized,
            "-t",
            str(final_duration),
            "-c:v",
            "copy",
            bg_adjusted,
        ]
        self._run_ffmpeg(command)

        if audio_path:
            command = [
                self._ffmpeg_bin,
                "-y",
                "-i",
                bg_adjusted,
                "-i",
                voice_adjusted,
                "-i",
                audio_path,
                "-filter_complex",
                f"[0:v][1:v]overlay={x_offset}:{y_offset}[v]",
                "-map",
                "[v]",
                "-map",
                "2:a",
                "-shortest",
                "-c:v",
                self.config.output_codec,
                "-preset",
                self.config.output_preset,
                "-crf",
                str(self.config.output_crf),
                "-pix_fmt",
                "yuv420p",
                output_path,
            ]
        else:
            command = [
                self._ffmpeg_bin,
                "-y",
                "-i",
                bg_adjusted,
                "-i",
                voice_adjusted,
                "-filter_complex",
                f"[0:v][1:v]overlay={x_offset}:{y_offset}[v]",
                "-map",
                "[v]",
                "-map",
                "1:a",
                "-shortest",
                "-c:v",
                self.config.output_codec,
                "-preset",
                self.config.output_preset,
                "-crf",
                str(self.config.output_crf),
                "-pix_fmt",
                "yuv420p",
                output_path,
            ]
        self._run_ffmpeg(command)

        for temp_file in [voice_normalized, voice_adjusted, bg_normalized, bg_adjusted]:
            try:
                if temp_file != output_path:
                    os.remove(temp_file)
            except Exception as e:
                logger.warning(f"Operation failed in apps/worker/video_processing/editing_modes.py: {e}", exc_info=True)

        return output_path


def create_processor(mode: str, work_dir: Optional[str] = None, **kwargs) -> EditingModeProcessor:
    """便捷工厂函数：创建剪辑模式处理器"""
    try:
        editing_mode = EditingMode(mode)
    except ValueError:
        raise ValueError(f"Invalid editing mode: {mode}. Valid modes: {[m.value for m in EditingMode]}")

    config = EditingModeConfig(
        mode=editing_mode,
        output_width=kwargs.get("output_width", 1280),
        output_height=kwargs.get("output_height", 720),
        output_fps=kwargs.get("output_fps", 25),
        pip_position=PIPPosition(kwargs.get("pip_position", "top_right")),
        pip_scale=kwargs.get("pip_scale", 0.25),
        transition_duration=kwargs.get("transition_duration", 0.5),
    )

    return EditingModeProcessor(config=config, work_dir=work_dir)
