"""视频对比工具 — 基于 FFmpeg 的像素级质量对比.

使用 SSIM + PSNR 双指标评估两个视频的相似度：
- SSIM (Structural Similarity): 结构相似性，范围 [0, 1]，越接近 1 越相似
- PSNR (Peak Signal-to-Noise Ratio): 峰值信噪比，单位 dB，越高越好

灰度验收标准：
- 平均 SSIM >= 0.95 → 视觉上几乎无差异（P0 场景必达）
- 最低 SSIM >= 0.90 → 最严重帧差异可接受
- 平均 PSNR >= 28dB → 质量达标
"""

from __future__ import annotations

import json
import re
import shutil
import subprocess  # nosec B404
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any

FFMPEG_BIN: str = shutil.which("ffmpeg") or "ffmpeg"
FFPROBE_BIN: str = shutil.which("ffprobe") or "ffprobe"


@dataclass
class VideoDiffResult:
    """视频对比结果."""

    video_a: str
    video_b: str
    width: int
    height: int
    duration_a: float
    duration_b: float
    avg_ssim: float
    min_ssim: float
    avg_psnr: float  # dB
    min_psnr: float
    frame_count: int
    duration_diff: float  # 时长差（秒）
    resolution_match: bool
    passed: bool  # 是否通过阈值

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


def probe_video_info(video_path: str) -> dict[str, Any]:
    """获取视频信息（宽、高、时长、fps）."""
    try:
        result = subprocess.run(  # nosec B603
            [
                FFPROBE_BIN,
                "-v",
                "error",
                "-select_streams",
                "v:0",
                "-show_entries",
                "stream=width,height,r_frame_rate,duration",
                "-show_entries",
                "format=duration",
                "-of",
                "json",
                video_path,
            ],
            check=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=10,
        )
        info = json.loads(result.stdout)
        stream = info.get("streams", [{}])[0]
        fmt = info.get("format", {})

        width = int(stream.get("width", 1280))
        height = int(stream.get("height", 720))
        fps_str = stream.get("r_frame_rate", "25/1")
        if "/" in fps_str:
            num, den = fps_str.split("/")
            fps = float(num) / float(den) if float(den) > 0 else 25.0
        else:
            fps = float(fps_str) if fps_str else 25.0
        duration = float(fmt.get("duration", 0)) or float(stream.get("duration", 0))

        return {"width": width, "height": height, "duration": duration, "fps": round(fps, 2)}
    except Exception:
        return {"width": 1280, "height": 720, "duration": 0.0, "fps": 25.0}


def compute_video_diff(
    video_a: str | Path,
    video_b: str | Path,
    *,
    ssim_threshold: float = 0.95,
    psnr_threshold: float = 28.0,
    duration_tolerance: float = 0.1,
) -> VideoDiffResult:
    """计算两个视频的像素差异.

    使用 FFmpeg ssim + psnr 滤镜一次性计算两个指标。

    Args:
        video_a: 视频A路径（基线）
        video_b: 视频B路径（对比）
        ssim_threshold: SSIM 合格阈值（默认 0.90）
        psnr_threshold: PSNR 合格阈值（默认 25dB）
        duration_tolerance: 时长容忍度（秒，默认 0.1s）

    Returns:
        VideoDiffResult 对比结果

    Raises:
        subprocess.CalledProcessError: FFmpeg 执行失败
    """
    info_a = probe_video_info(str(video_a))
    info_b = probe_video_info(str(video_b))

    duration_diff = abs(info_a["duration"] - info_b["duration"])
    resolution_match = info_a["width"] == info_b["width"] and info_a["height"] == info_b["height"]

    # ssim 和 psnr 的 stats_file 都输出到 stdout
    # 用行格式区分：SSIM 行含 "All:"，PSNR 行含 "psnr_avg:"
    command = [
        FFMPEG_BIN,
        "-i",
        str(video_a),
        "-i",
        str(video_b),
        "-lavfi",
        "[0:v][1:v]ssim=stats_file=-[out1];[0:v][1:v]psnr=stats_file=-[out2]",
        "-f",
        "null",
        "-",
    ]

    result = subprocess.run(  # nosec B603
        command,
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        timeout=300,
    )

    # 逐帧统计在 stdout（stats_file=-），汇总日志在 stderr
    stats_stdout = result.stdout or ""

    avg_ssim, min_ssim = _parse_ssim_stats(stats_stdout)
    avg_psnr, min_psnr = _parse_psnr_stats(stats_stdout)
    frame_count = _count_frames(result.stderr or "")

    passed = (
        resolution_match
        and duration_diff <= duration_tolerance
        and avg_ssim >= ssim_threshold
        and avg_psnr >= psnr_threshold
    )

    return VideoDiffResult(
        video_a=str(video_a),
        video_b=str(video_b),
        width=info_a["width"],
        height=info_a["height"],
        duration_a=round(info_a["duration"], 3),
        duration_b=round(info_b["duration"], 3),
        avg_ssim=round(avg_ssim, 6),
        min_ssim=round(min_ssim, 6),
        avg_psnr=round(avg_psnr, 3),
        min_psnr=round(min_psnr, 3),
        frame_count=frame_count,
        duration_diff=round(duration_diff, 3),
        resolution_match=resolution_match,
        passed=passed,
    )


def _parse_ssim_stats(stats_output: str) -> tuple[float, float]:
    """从 SSIM stats_file 输出中解析逐帧 SSIM.

    FFmpeg ssim 滤镜 stats_file 输出格式（每行一帧）:
        n:1 Y:0.987654 U:0.991234 V:0.990000 All:0.989000 (19.585642)
        n:2 Y:0.986543 U:0.990123 V:0.988888 All:0.987654 (19.123456)
        ...

    Returns:
        (avg_ssim, min_ssim)
    """
    ssim_values: list[float] = []

    for line in stats_output.split("\n"):
        # 匹配 stats_file 格式：n:数字 ... All:数字
        if not line.startswith("n:"):
            continue
        match = re.search(r"All:(\d+\.\d+)", line)
        if match:
            ssim_values.append(float(match.group(1)))

    if not ssim_values:
        return 0.0, 0.0

    avg_ssim = sum(ssim_values) / len(ssim_values)
    min_ssim = min(ssim_values)
    return avg_ssim, min_ssim


def _parse_psnr_stats(stats_output: str) -> tuple[float, float]:
    """从 PSNR stats_file 输出中解析逐帧 PSNR.

    FFmpeg psnr 滤镜 stats_file 输出格式（每行一帧）:
        n:1 mse_avg:100.23 mse_y:150.12 mse_u:50.34 mse_v:80.56 psnr_avg:28.12 psnr_y:26.34 psnr_u:31.12 psnr_v:29.08
        n:2 ...

    Returns:
        (avg_psnr, min_psnr)  —  avg_psnr 是逐帧 psnr_avg 的均值，min_psnr 是逐帧最小值
    """
    psnr_values: list[float] = []

    for line in stats_output.split("\n"):
        if not line.startswith("n:"):
            continue
        match = re.search(r"psnr_avg:(\d+\.\d+)", line)
        if match:
            psnr_values.append(float(match.group(1)))

    if not psnr_values:
        return 0.0, 0.0

    avg_psnr = sum(psnr_values) / len(psnr_values)
    min_psnr = min(psnr_values)
    return avg_psnr, min_psnr


def _count_frames(stderr: str) -> int:
    """从 FFmpeg 输出中统计帧数."""
    match = re.search(r"frame=\s*(\d+)", stderr)
    return int(match.group(1)) if match else 0


def save_diff_frame(
    video_a: str | Path,
    video_b: str | Path,
    output_path: str | Path,
    *,
    timestamp: float = 1.0,
) -> Path:
    """生成差异帧可视化图（红绿色差）.

    使用 blend 滤镜生成差异可视化图，差异越大越亮。

    Args:
        video_a: 视频A
        video_b: 视频B
        output_path: 输出图片路径
        timestamp: 截取的时间点（秒）

    Returns:
        输出图片路径
    """
    command = [
        FFMPEG_BIN,
        "-y",
        "-ss",
        str(timestamp),
        "-i",
        str(video_a),
        "-ss",
        str(timestamp),
        "-i",
        str(video_b),
        "-lavfi",
        "[0:v][1:v]blend=all_mode=difference,eq=contrast=5:brightness=0.5[diff]",
        "-map",
        "[diff]",
        "-vframes",
        "1",
        str(output_path),
    ]

    subprocess.run(command, check=True, capture_output=True, timeout=60)  # nosec B603
    return Path(output_path)
