from __future__ import annotations

import asyncio
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path

from app.config import settings
from app.services.upload_video_asset_service import (
    build_upload_url_from_path,
    ensure_shot_segment_dir,
    get_ffmpeg_bin,
)


@dataclass(slots=True)
class ShotSplitResult:
    url: str
    path: str
    file_size_bytes: int


def _date_dir_from_segment_id(segment_id: str) -> str:
    # 由调用方更适合按 created_at 传入；这里兜底按当前日期。
    from datetime import datetime

    return datetime.now().strftime("%Y/%m/%d")


def _safe_unlink(path: Path) -> None:
    try:
        if path.exists():
            path.unlink()
    except Exception:
        pass


def split_video_segment(
    *,
    source_path: str | Path,
    segment_id: str,
    start_second: float,
    end_second: float,
    date_dir: str | None = None,
) -> ShotSplitResult:
    """使用 ffmpeg 拆出单个视频片段，输出到 storage/uploads/shot_segments。"""
    source_path = Path(source_path)

    if not source_path.exists():
        raise RuntimeError(f"ffmpeg 拆镜失败：源视频不存在 {source_path}")

    start = max(float(start_second), 0.0)
    end = max(float(end_second), 0.0)
    duration = end - start

    if duration <= 0:
        raise RuntimeError(
            f"ffmpeg 拆镜失败：非法时间范围 start_second={start_second}, end_second={end_second}"
        )

    date_dir = date_dir or _date_dir_from_segment_id(segment_id)
    output_dir = ensure_shot_segment_dir(date_dir)
    output_path = output_dir / f"{segment_id}.mp4"

    # 注意：
    # 不能用 xxx.mp4.part，因为 ffmpeg 会按最后一个扩展名 .part 判断输出格式，导致：
    # Unable to choose an output format
    # 这里改为 xxx.part.mp4，让 ffmpeg 能识别 mp4 容器。
    part_path = output_dir / f"{segment_id}.part.mp4"

    _safe_unlink(part_path)

    timeout = int(getattr(settings, "SHOT_FFMPEG_TIMEOUT_SECONDS", 120) or 120)

    cmd = [
        get_ffmpeg_bin(),
        "-y",

        # 先 seek 到起始秒，再按 duration 切割，避免 -to 在不同 ffmpeg 参数位置下语义不一致。
        "-ss",
        f"{start:.3f}",
        "-i",
        str(source_path),
        "-t",
        f"{duration:.3f}",

        # 只取主视频流，音频可选，避免 map 0 把字幕/数据流带进去导致 mp4 封装失败。
        "-map",
        "0:v:0",
        "-map",
        "0:a:0?",

        # 当前是拆镜片段，重编码更稳，避免关键帧不准导致片段首尾异常。
        "-c:v",
        "libx264",
        "-preset",
        "veryfast",
        "-crf",
        "23",
        "-pix_fmt",
        "yuv420p",

        "-c:a",
        "aac",
        "-b:a",
        "128k",

        "-movflags",
        "+faststart",

        # 即使临时文件扩展名未来被改坏，也强制指定 mp4 muxer。
        "-f",
        "mp4",

        str(part_path),
    ]

    try:
        completed = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=timeout,
            check=False,
        )
    except subprocess.TimeoutExpired as exc:
        _safe_unlink(part_path)
        raise RuntimeError(
            f"ffmpeg 拆镜超时：timeout={timeout}s, start={start:.3f}, end={end:.3f}"
        ) from exc

    if completed.returncode != 0:
        _safe_unlink(part_path)
        raise RuntimeError(
            f"ffmpeg 拆镜失败: {completed.stderr.strip() or completed.stdout.strip()}"
        )

    if not part_path.exists() or part_path.stat().st_size <= 0:
        _safe_unlink(part_path)
        raise RuntimeError("ffmpeg 拆镜失败：输出文件为空")

    os.replace(part_path, output_path)

    return ShotSplitResult(
        url=build_upload_url_from_path(output_path),
        path=str(output_path),
        file_size_bytes=output_path.stat().st_size,
    )


async def split_video_segment_async(
    *,
    source_path: str | Path,
    segment_id: str,
    start_second: float,
    end_second: float,
    date_dir: str | None = None,
) -> ShotSplitResult:
    """异步拆镜入口。

    ffmpeg 本身是同步阻塞命令，不能直接在 Celery 进程内唯一 event loop 中执行。
    这里通过 asyncio.to_thread 跑同步拆镜函数，避免阻塞 asyncpg / Redis / HTTP 等异步任务。
    """
    return await asyncio.to_thread(
        split_video_segment,
        source_path=source_path,
        segment_id=segment_id,
        start_second=start_second,
        end_second=end_second,
        date_dir=date_dir,
    )