"""VideoComposeService — Phase 8 任务 2.09.

FFmpeg 视频合成编排服务：
  1. 根据 EditPlan + EditPlanClips 生成 FFmpeg filter_complex 命令
  2. 支持逐片段 scale / crop / trim / setpts 滤镜
  3. 支持转场效果（fade / slide / dissolve / wipe）
  4. 支持音频流合并
  5. 提供合成前校验逻辑

设计原则：
  - 本服务只负责 **命令生成 + 校验**，不执行 FFmpeg
  - Worker 层（Celery task）调用本服务生成命令后执行
  - API 层可调用 build_compose_command 做预览 / 调试
"""

from __future__ import annotations

import logging
import shutil
from dataclasses import dataclass, field
from typing import Any

from sqlalchemy.orm import Session

from packages.adapters.sqlalchemy_impl.edit_plan_clip_repository import (
    SQLAlchemyEditPlanClipRepository,
)
from packages.adapters.sqlalchemy_impl.edit_plan_repository import (
    SQLAlchemyEditPlanRepository,
)
from packages.domain.edit_plan import EditPlan, EditPlanStatus
from packages.domain.edit_plan_clip import EditPlanClip, EditPlanClipStatus
from packages.domain.template_clip_config import TransitionEffect

logger = logging.getLogger(__name__)

# ── 常量 ──────────────────────────────────────────────────────────────────────

DEFAULT_OUTPUT_WIDTH = 1280
DEFAULT_OUTPUT_HEIGHT = 720
DEFAULT_FPS = 25
DEFAULT_CODEC = "libx264"
DEFAULT_CRF = 23
DEFAULT_PRESET = "medium"

# xfade 转场映射：TransitionEffect → FFmpeg xfade transition 名称
_XFADE_TRANSITION_MAP: dict[str, str] = {
    TransitionEffect.FADE: "fade",
    TransitionEffect.SLIDE_LEFT: "slideleft",
    TransitionEffect.SLIDE_RIGHT: "slideright",
    TransitionEffect.DISSOLVE: "dissolve",
    TransitionEffect.WIPE: "wipeleft",
}

# 转场默认时长（秒）
DEFAULT_TRANSITION_DURATION = 0.5


# ── 数据结构 ──────────────────────────────────────────────────────────────────


@dataclass(frozen=True)
class ClipFilterChain:
    """单个片段的滤镜链描述。"""

    clip_id: str
    input_index: int
    video_label: str
    audio_label: str | None
    filters: list[str]
    duration: float


@dataclass(frozen=True)
class ComposeCommand:
    """完整的 FFmpeg 合成命令描述。"""

    command: list[str]
    """可直接传给 subprocess.run 的命令列表。"""

    filter_complex: str
    """-filter_complex 参数值（方便调试 / 日志）。"""

    input_paths: list[str]
    """输入文件路径列表。"""

    output_path: str
    """输出文件路径。"""

    estimated_duration: float
    """预估输出时长（秒）。"""

    clip_chains: list[ClipFilterChain]
    """每个片段的滤镜链描述。"""


@dataclass(frozen=True)
class ComposeValidation:
    """合成前校验结果。"""

    valid: bool
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    ready_clip_count: int = 0
    total_clip_count: int = 0


# ── 服务主体 ──────────────────────────────────────────────────────────────────


class VideoComposeService:
    """FFmpeg 视频合成编排服务。

    职责：
    - 根据 EditPlan 及其 Clips 生成 FFmpeg filter_complex 命令
    - 校验合成前置条件
    - 提供合成状态查询

    用法::

        svc = VideoComposeService(db)
        validation = svc.validate_compose(plan_id)
        if validation.valid:
            cmd = svc.build_compose_command(plan_id, output_path="/tmp/out.mp4")
            subprocess.run(cmd.command, check=True)
    """

    def __init__(self, db: Session) -> None:
        self._db = db
        self._plan_repo = SQLAlchemyEditPlanRepository(db)
        self._clip_repo = SQLAlchemyEditPlanClipRepository(db)

    # ── 公开方法 ──────────────────────────────────────────────────────────

    def validate_compose(self, plan_id: str) -> ComposeValidation:
        """校验剪辑计划是否可以合成。

        检查项：
        1. 计划存在
        2. 计划状态为 editing 或 rendering
        3. 至少有一个 ready 状态的片段
        4. 每个 ready 片段都有 asset_id
        5. 每个 ready 片段都有 duration > 0
        """
        errors: list[str] = []
        warnings: list[str] = []

        plan = self._plan_repo.get(plan_id)
        if plan is None:
            return ComposeValidation(
                valid=False,
                errors=[f"剪辑计划不存在: {plan_id}"],
            )

        # 状态检查
        if plan.status not in (EditPlanStatus.EDITING, EditPlanStatus.RENDERING):
            errors.append(f"计划状态不正确，需要 editing 或 rendering，当前: {plan.status.value}")

        # 加载片段
        clips = self._clip_repo.list_by_plan(plan_id, skip=0, limit=10000)
        if not clips:
            errors.append("计划没有任何片段")
            return ComposeValidation(
                valid=False,
                errors=errors,
                total_clip_count=0,
            )

        # 按 order 排序
        clips.sort(key=lambda c: c.order)

        ready_count = 0
        pending_count = 0
        no_asset_count = 0
        no_duration_count = 0

        for clip in clips:
            if clip.status == EditPlanClipStatus.READY:
                ready_count += 1
                if not clip.asset_id:
                    errors.append(f"片段 {clip.id} (order={clip.order}) 没有分配素材")
                    no_asset_count += 1
                if clip.duration <= 0:
                    warnings.append(f"片段 {clip.id} (order={clip.order}) 时长为 0，将使用默认时长")
                    no_duration_count += 1
            elif clip.status == EditPlanClipStatus.PENDING:
                pending_count += 1
            elif clip.status == EditPlanClipStatus.FAILED:
                warnings.append(f"片段 {clip.id} (order={clip.order}) 状态为 failed，已跳过")

        if ready_count == 0:
            errors.append("没有就绪（ready）的片段可以合成")

        if pending_count > 0:
            warnings.append(f"有 {pending_count} 个片段仍处于 pending 状态")

        return ComposeValidation(
            valid=len(errors) == 0,
            errors=errors,
            warnings=warnings,
            ready_clip_count=ready_count,
            total_clip_count=len(clips),
        )

    def build_compose_command(
        self,
        plan_id: str,
        output_path: str,
        *,
        output_width: int = DEFAULT_OUTPUT_WIDTH,
        output_height: int = DEFAULT_OUTPUT_HEIGHT,
        fps: int = DEFAULT_FPS,
        codec: str = DEFAULT_CODEC,
        crf: int = DEFAULT_CRF,
        preset: str = DEFAULT_PRESET,
        transition_duration: float = DEFAULT_TRANSITION_DURATION,
    ) -> ComposeCommand:
        """构建 FFmpeg 合成命令。

        根据 EditPlan 的所有 ready 片段，生成完整的 filter_complex 命令。

        滤镜链逻辑：
        - 每个片段：scale → crop → setpts → trim → atrim
        - 多片段之间：concat 滤镜 或 xfade 转场
        - 最终输出：-map '[outv]' -map '[outa]'（如有音频）
        """
        plan = self._plan_repo.get(plan_id)
        if plan is None:
            raise ValueError(f"剪辑计划不存在: {plan_id}")

        clips = self._clip_repo.list_by_plan(plan_id, skip=0, limit=10000)
        if not clips:
            raise ValueError(f"剪辑计划没有片段: {plan_id}")

        # 只处理 ready 且有 asset_id 的片段
        ready_clips = [c for c in clips if c.status == EditPlanClipStatus.READY and c.asset_id]
        ready_clips.sort(key=lambda c: c.order)

        if not ready_clips:
            raise ValueError(f"剪辑计划没有可合成的片段: {plan_id}")

        # 构建每个片段的滤镜链
        clip_chains: list[ClipFilterChain] = []
        input_paths: list[str] = []

        for idx, clip in enumerate(ready_clips):
            chain = self._build_clip_filter(
                clip=clip,
                input_index=idx,
                output_width=output_width,
                output_height=output_height,
                fps=fps,
            )
            clip_chains.append(chain)
            input_paths.append(clip.asset_id)  # asset_id 存储的是 storage_key / URL

        # 构建 filter_complex
        filter_complex, estimated_duration = self._build_filter_complex(
            clip_chains=clip_chains,
            output_width=output_width,
            output_height=output_height,
            transition_duration=transition_duration,
            transitions=[c.transition_effect for c in ready_clips],
        )

        # 构建完整命令
        command: list[str] = ["ffmpeg", "-y"]

        # 输入文件
        for path in input_paths:
            command.extend(["-i", path])

        # filter_complex
        command.extend(["-filter_complex", filter_complex])

        # 映射输出流
        command.extend(["-map", "[outv]"])
        if self._has_audio(clip_chains):
            command.extend(["-map", "[outa]"])

        # 编码参数
        command.extend(
            [
                "-c:v",
                codec,
                "-crf",
                str(crf),
                "-preset",
                preset,
                "-c:a",
                "aac",
                "-b:a",
                "192k",
            ]
        )

        # 输出
        command.append(output_path)

        return ComposeCommand(
            command=command,
            filter_complex=filter_complex,
            input_paths=input_paths,
            output_path=output_path,
            estimated_duration=estimated_duration,
            clip_chains=clip_chains,
        )

    def build_single_clip_command(
        self,
        clip_id: str,
        output_path: str,
        *,
        output_width: int = DEFAULT_OUTPUT_WIDTH,
        output_height: int = DEFAULT_OUTPUT_HEIGHT,
        fps: int = DEFAULT_FPS,
    ) -> ComposeCommand:
        """为单个片段构建 FFmpeg 命令（预览 / 调试用）。"""
        clip = self._clip_repo.get(clip_id)
        if clip is None:
            raise ValueError(f"片段不存在: {clip_id}")
        if not clip.asset_id:
            raise ValueError(f"片段没有分配素材: {clip_id}")

        chain = self._build_clip_filter(
            clip=clip,
            input_index=0,
            output_width=output_width,
            output_height=output_height,
            fps=fps,
        )

        # 简单命令：input → filter → output
        filter_str = ",".join(chain.filters)
        command = [
            "ffmpeg",
            "-y",
            "-i",
            clip.asset_id,
            "-filter_complex",
            f"{filter_str}[outv]",
            "-map",
            "[outv]",
            "-c:v",
            DEFAULT_CODEC,
            "-crf",
            str(DEFAULT_CRF),
            "-preset",
            DEFAULT_PRESET,
            output_path,
        ]

        return ComposeCommand(
            command=command,
            filter_complex=filter_str,
            input_paths=[clip.asset_id],
            output_path=output_path,
            estimated_duration=clip.duration,
            clip_chains=[chain],
        )

    def get_compose_status(self, plan_id: str) -> dict[str, Any]:
        """获取合成状态摘要。"""
        plan = self._plan_repo.get(plan_id)
        if plan is None:
            raise ValueError(f"剪辑计划不存在: {plan_id}")

        clips = self._clip_repo.list_by_plan(plan_id, skip=0, limit=10000)
        clips.sort(key=lambda c: c.order)

        total_duration = sum(c.duration for c in clips if c.duration > 0)
        ready_clips = [c for c in clips if c.status == EditPlanClipStatus.READY]
        pending_clips = [c for c in clips if c.status == EditPlanClipStatus.PENDING]
        rendered_clips = [c for c in clips if c.status == EditPlanClipStatus.RENDERED]
        failed_clips = [c for c in clips if c.status == EditPlanClipStatus.FAILED]

        return {
            "plan_id": plan_id,
            "plan_status": plan.status.value,
            "total_clips": len(clips),
            "ready_clips": len(ready_clips),
            "pending_clips": len(pending_clips),
            "rendered_clips": len(rendered_clips),
            "failed_clips": len(failed_clips),
            "total_duration": total_duration,
            "can_compose": len(ready_clips) > 0
            and plan.status
            in (
                EditPlanStatus.EDITING,
                EditPlanStatus.RENDERING,
            ),
            "rendered_url": plan.config.get("rendered_url", ""),
        }

    # ── 内部方法 ──────────────────────────────────────────────────────────

    @staticmethod
    def _build_clip_filter(
        clip: EditPlanClip,
        input_index: int,
        output_width: int,
        output_height: int,
        fps: int,
    ) -> ClipFilterChain:
        """为单个片段构建滤镜链。

        滤镜顺序：
        1. scale — 等比缩放到目标分辨率（保证覆盖）
        2. crop — 居中裁剪到目标分辨率
        3. setpts — 重置时间戳 + 偏移
        4. trim — 视频时长裁剪
        5. atrim — 音频时长裁剪（如有音频流）
        """
        duration = clip.duration if clip.duration > 0 else 5.0  # 默认 5 秒
        start = clip.start_time

        filters: list[str] = []

        # 1. scale: 等比缩放，保证覆盖目标区域（scale to larger, then crop）
        filters.append(f"scale={output_width}:{output_height}" f":force_original_aspect_ratio=increase")

        # 2. crop: 居中裁剪
        filters.append(f"crop={output_width}:{output_height}")

        # 3. setpts: 重置时间戳
        if start > 0:
            filters.append(f"setpts=PTS-STARTPTS+{start}/TB")
        else:
            filters.append("setpts=PTS-STARTPTS")

        # 4. trim: 视频时长
        filters.append(f"trim=0:{duration}")
        filters.append("setpts=PTS-STARTPTS")  # trim 后需要重置 PTS

        video_label = f"v{input_index}"

        # 5. 音频标签：仅当片段类型可能有音频时才设置
        # title/subtitle 是纯文字/图片卡片，没有音频流
        clip_type = clip.clip_type.lower() if clip.clip_type else ""
        has_audio_stream = clip_type not in ("title", "subtitle")
        audio_label = f"a{input_index}" if has_audio_stream else None

        return ClipFilterChain(
            clip_id=clip.id,
            input_index=input_index,
            video_label=video_label,
            audio_label=audio_label,
            filters=filters,
            duration=duration,
        )

    @staticmethod
    def _build_filter_complex(
        clip_chains: list[ClipFilterChain],
        output_width: int,
        output_height: int,
        transition_duration: float,
        transitions: list[str],
    ) -> tuple[str, float]:
        """构建完整的 filter_complex 字符串。

        策略：
        - 单片段：直接输出
        - 多片段 + 全 cut：使用 concat 滤镜（高效）
        - 多片段 + 有转场：使用 xfade 滤镜链

        返回 (filter_complex_string, estimated_total_duration)。
        """
        n = len(clip_chains)

        if n == 0:
            return "", 0.0

        # ── 单片段 ─────────────────────────────────────────────────────
        if n == 1:
            chain = clip_chains[0]
            filter_str = _chain_filters(chain.filters, chain.video_label)
            # 音频
            if chain.audio_label:
                filter_str += f";[0:a]{chain.audio_label}"
            total_duration = chain.duration
            return filter_str, total_duration

        # ── 检查是否有转场 ─────────────────────────────────────────────
        has_transitions = any(t != TransitionEffect.CUT and t != "cut" for t in transitions)

        if not has_transitions:
            return _build_concat_filter(clip_chains)

        # ── 有转场：使用 xfade ─────────────────────────────────────────
        return _build_xfade_filter(
            clip_chains=clip_chains,
            transition_duration=transition_duration,
            transitions=transitions,
        )

    @staticmethod
    def _has_audio(clip_chains: list[ClipFilterChain]) -> bool:
        """是否有任何片段包含音频流。"""
        return any(c.audio_label is not None for c in clip_chains)


# ── 模块级辅助函数 ────────────────────────────────────────────────────────────


def _chain_filters(filters: list[str], output_label: str) -> str:
    """将滤镜列表串联为 FFmpeg 滤镜字符串。"""
    filter_body = ",".join(filters)
    return f"[0:v]{filter_body}[{output_label}]"


def _build_concat_filter(
    clip_chains: list[ClipFilterChain],
) -> tuple[str, float]:
    """构建 concat 滤镜（无转场，高效拼接）。

    格式：
    [0:v]filters[v0]; [1:v]filters[v1]; ...
    [v0][v1]...[vN]concat=n=N:v=1:a=0[outv]
    """
    n = len(clip_chains)
    parts: list[str] = []
    total_duration = 0.0

    # 每个片段的滤镜链
    for idx, chain in enumerate(clip_chains):
        filter_body = ",".join(chain.filters)
        parts.append(f"[{idx}:v]{filter_body}[{chain.video_label}]")
        total_duration += chain.duration

    # concat 滤镜
    concat_inputs = "".join(f"[{c.video_label}]" for c in clip_chains)
    concat_filter = f"{concat_inputs}concat=n={n}:v=1:a=0[outv]"
    parts.append(concat_filter)

    # 音频 concat（如果有）
    audio_parts: list[str] = []
    for idx, chain in enumerate(clip_chains):
        if chain.audio_label:
            audio_parts.append(f"[{idx}:a]atrim=0:{chain.duration},asetpts=PTS-STARTPTS[{chain.audio_label}]")

    if audio_parts:
        parts.extend(audio_parts)
        audio_inputs = "".join(f"[{c.audio_label}]" for c in clip_chains if c.audio_label)
        audio_count = sum(1 for c in clip_chains if c.audio_label)
        if audio_count > 0:
            parts.append(f"{audio_inputs}concat=n={audio_count}:v=0:a=1[outa]")

    return ";".join(parts), total_duration


def _build_xfade_filter(
    clip_chains: list[ClipFilterChain],
    transition_duration: float,
    transitions: list[str],
) -> tuple[str, float]:
    """构建 xfade 转场滤镜链。

    每两个相邻片段之间插入 xfade 转场。
    offset = 前一个片段的累积时长 - 转场时长。

    格式（2 片段）：
    [0:v]filters[v0]; [1:v]filters[v1];
    [v0][v1]xfade=transition=fade:duration=0.5:offset=4.5[outv]

    格式（3+ 片段）：
    [v0][v1]xfade=...[tmp1]; [tmp1][v2]xfade=...[outv]
    """
    n = len(clip_chains)
    parts: list[str] = []
    total_duration = 0.0

    # 每个片段的滤镜链
    for idx, chain in enumerate(clip_chains):
        filter_body = ",".join(chain.filters)
        parts.append(f"[{idx}:v]{filter_body}[{chain.video_label}]")
        total_duration += chain.duration

    # xfade 链
    if n == 1:
        # 单片段不需要 xfade
        parts.append(f"[{clip_chains[0].video_label}]copy[outv]")
        return ";".join(parts), total_duration

    # 计算每个转场的 offset
    cumulative = 0.0
    prev_label = clip_chains[0].video_label

    for i in range(1, n):
        cumulative += clip_chains[i - 1].duration
        offset = max(0.0, cumulative - transition_duration * i)

        # 获取转场类型
        transition = transitions[i] if i < len(transitions) else "cut"
        xfade_transition = _XFADE_TRANSITION_MAP.get(transition, "fade")

        if i == n - 1:
            # 最后一个转场，输出到 [outv]
            out_label = "outv"
        else:
            out_label = f"xf{i}"

        parts.append(
            f"[{prev_label}][{clip_chains[i].video_label}]"
            f"xfade=transition={xfade_transition}"
            f":duration={transition_duration}"
            f":offset={offset:.3f}"
            f"[{out_label}]"
        )
        prev_label = out_label

    # 总时长需要减去转场重叠部分
    total_duration -= transition_duration * (n - 1)

    # 音频 crossfade（简化处理：使用 adelay + amix）
    audio_labels = [c.audio_label for c in clip_chains if c.audio_label]
    if len(audio_labels) >= 2:
        # 简单拼接音频（不做 crossfade）
        audio_inputs = "".join(f"[{label}]" for label in audio_labels)
        parts.append(f"{audio_inputs}concat=n={len(audio_labels)}:v=0:a=1[outa]")
    elif len(audio_labels) == 1:
        parts.append(f"[{audio_labels[0]}]acopy[outa]")

    return ";".join(parts), max(0.0, total_duration)
