#!/usr/bin/env python3
"""Проверка результата монтажа: длительность (допуск 1 кадр), разрешение, дорожки, титр."""
from __future__ import annotations

import json
import subprocess
import sys
from fractions import Fraction
from pathlib import Path

from PIL import Image

ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "маршруты" / "медиа" / "учебный-ролик.mp4"
OUT = ROOT / "результаты" / "учебный-ролик-титр-640x360.mp4"
TITLE_PNG = ROOT / "сборка" / "титр-учебный-пример.png"
FRAMES_DIR = ROOT / "сборка" / "рендер-видео"
RESULT = ROOT / "сборка" / "видео-проверка.json"

EXPECT_START, EXPECT_END = 1.0, 5.0
WIDTH, HEIGHT, FPS = 640, 360, 25
EXPECT_DURATION = EXPECT_END - EXPECT_START          # 4.0 с
EXPECT_FRAMES = int(round(EXPECT_DURATION * FPS))    # 100 кадров
FRAME_TOL = 1.0 / FPS                                # допуск одного кадра, 0.04 с
SAMPLE_FRAMES = [0, 49, 99]                          # начало, середина, конец
GLYPH_MIN_LUMA = 180
BACKGROUND_MAX_LUMA = 170
GLYPH_COVERAGE_MIN = 0.90


def probe(path: Path) -> dict:
    out = subprocess.run(
        ["ffprobe", "-v", "error", "-print_format", "json", "-show_format", "-show_streams", str(path)],
        capture_output=True, text=True, check=True)
    return json.loads(out.stdout)


def luma(px) -> float:
    r, g, b = px[:3]
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def glyph_mask() -> tuple[set, int]:
    img = Image.open(TITLE_PNG).convert("RGBA")
    alpha = img.getchannel("A").load()
    band_y0 = HEIGHT - 56  # плашка титра, см. видео-монтаж.py
    return ({(x, y) for y in range(band_y0, HEIGHT) for x in range(WIDTH) if alpha[x, y] >= 200},
            band_y0)


def extract_frames(out_dir: Path) -> dict[int, Path]:
    out_dir.mkdir(parents=True, exist_ok=True)
    paths: dict[int, Path] = {}
    for n in SAMPLE_FRAMES:
        p = out_dir / f"кадр-{n:03d}.png"
        subprocess.run(["ffmpeg", "-y", "-v", "error", "-i", str(OUT),
                        "-vf", f"select='eq(n\\,{n})'", "-fps_mode", "passthrough", str(p)],
                       check=True)
        paths[n] = p
    return paths


def src_frame(n: int, out_dir: Path) -> Path:
    p = out_dir / f"исходник-{n:03d}.png"
    subprocess.run(["ffmpeg", "-y", "-v", "error", "-i", str(SRC),
                    "-vf", f"select='eq(n\\,{n})'", "-fps_mode", "passthrough", str(p)],
                   check=True)
    return p


def region_diff(a: Path, b: Path, y_to: int) -> dict:
    """Различие двух кадров выше плашки титра: средняя |dL| и доля пикселей |dL| > 20."""
    ia = Image.open(a).convert("RGB").crop((0, 0, WIDTH, y_to))
    ib = Image.open(b).convert("RGB").crop((0, 0, WIDTH, y_to))
    pa, pb = ia.load(), ib.load()
    total, changed, count = 0.0, 0, 0
    for y in range(y_to):
        for x in range(WIDTH):
            d = abs(luma(pa[x, y]) - luma(pb[x, y]))
            total += d
            if d > 20:
                changed += 1
            count += 1
    return {"средняя_разница": round(total / count, 3),
            "доля_пикселей_разницы_более_20": round(changed / count, 4)}


def main() -> int:
    checks: list[dict] = []

    def check(name: str, ok: bool, fact, expect) -> None:
        checks.append({"проверка": name, "итог": "ок" if ok else "ошибка",
                       "факт": fact, "ожидание": expect})

    src_data, out_data = probe(SRC), probe(OUT)
    v = next(s for s in out_data["streams"] if s["codec_type"] == "video")
    audios = [s for s in out_data["streams"] if s["codec_type"] == "audio"]

    duration = float(out_data["format"]["duration"])
    check("длительность, с", abs(duration - EXPECT_DURATION) <= FRAME_TOL,
          round(duration, 4), f"{EXPECT_DURATION} ± {FRAME_TOL} (1 кадр при {FPS} к/с)")
    check("число кадров", int(v["nb_frames"]) == EXPECT_FRAMES, int(v["nb_frames"]), EXPECT_FRAMES)
    check("разрешение", (int(v["width"]), int(v["height"])) == (WIDTH, HEIGHT),
          f'{v["width"]}x{v["height"]}', f"{WIDTH}x{HEIGHT}")
    check("частота кадров", Fraction(v["avg_frame_rate"]) == Fraction(FPS, 1),
          v["avg_frame_rate"], f"{FPS}/1")
    check("видеодорожек", len([s for s in out_data["streams"] if s["codec_type"] == "video"]) == 1,
          len([s for s in out_data["streams"] if s["codec_type"] == "video"]), 1)
    check("аудиодорожек (в исходнике звука нет)", len(audios) == 0, len(audios), 0)
    check("контейнер/кодек", out_data["format"]["format_name"].startswith("mov")
          and v["codec_name"] == "h264", f'{out_data["format"]["format_name"]}/{v["codec_name"]}',
          "mp4/h264")
    src_streams = src_data["streams"]
    check("исходник не изменён (6 с, 150 кадров, без звука)",
          abs(float(src_data["format"]["duration"]) - 6.0) <= FRAME_TOL
          and int(next(s for s in src_streams if s["codec_type"] == "video")["nb_frames"]) == 150
          and not [s for s in src_streams if s["codec_type"] == "audio"],
          f'{src_data["format"]["duration"]} с, '
          f'{next(s for s in src_streams if s["codec_type"] == "video")["nb_frames"]} кадров, '
          f'аудио: {len([s for s in src_streams if s["codec_type"] == "audio"])}',
          "6.0 ± 0.04 с, 150 кадров, аудио: 0")

    mask, band_y0 = glyph_mask()
    frames = extract_frames(FRAMES_DIR)
    title_details = []
    visible = True
    for n, path in frames.items():
        img = Image.open(path).convert("RGB")
        px = img.load()
        glyph_lumas = [luma(px[p]) for p in mask]
        glyph_mean = sum(glyph_lumas) / len(glyph_lumas)
        covered = sum(1 for v_ in glyph_lumas if v_ >= GLYPH_MIN_LUMA) / len(glyph_lumas)
        outside = [luma(px[(x, y)]) for y in range(band_y0, HEIGHT) for x in range(WIDTH)
                   if (x, y) not in mask]
        band_max = max(outside)
        ok = covered >= GLYPH_COVERAGE_MIN and glyph_mean >= GLYPH_MIN_LUMA and band_max <= BACKGROUND_MAX_LUMA
        visible = visible and ok
        title_details.append({
            "кадр": n,
            "средняя_яркость_глифов": round(glyph_mean, 1),
            "доля_ярких_пикселей_глифа": round(covered, 3),
            "макс_яркость_фона_плашки": round(band_max, 1),
            "итог": "ок" if ok else "ошибка",
        })
    check("титр виден в кадре (начало/середина/конец)", visible,
          title_details, f"глифы ≥ {GLYPH_MIN_LUMA} при покрытии ≥ {GLYPH_COVERAGE_MIN}, "
                         f"фон плашки ≤ {BACKGROUND_MAX_LUMA}")
    check("исходный файл на месте", SRC.is_file(), str(SRC.relative_to(ROOT)), "файл существует")

    # Интервал: первый кадр результата совпадает с кадром 25 исходника (t = 1.0 с),
    # последний - с кадром 124 (t = 4.96 с), и оба отличаются от кадров вне интервала.
    band_top = HEIGHT - 56
    first = src_frame(25, FRAMES_DIR)
    last = src_frame(124, FRAMES_DIR)
    outside = src_frame(0, FRAMES_DIR)
    match_first = region_diff(first, frames[0], band_top)
    match_last = region_diff(last, frames[99], band_top)
    mismatch = region_diff(outside, frames[0], band_top)
    ok_interval = (max(match_first["доля_пикселей_разницы_более_20"],
                       match_last["доля_пикселей_разницы_более_20"]) < 0.005
                   and mismatch["доля_пикселей_разницы_более_20"] > 0.02)
    check("интервал 1-5 с (сверка кадров вне зоны титра)", ok_interval,
          {"совпадение_с_кадром_25_в_начале": match_first,
           "совпадение_с_кадром_124_в_конце": match_last,
           "отличие_от_кадра_0": mismatch},
          "доля изменённых пикселей: совпадение < 0.005, отличие > 0.02")

    RESULT.write_text(json.dumps({"проверки": checks}, ensure_ascii=False, indent=2) + "\n",
                      encoding="utf-8")
    failed = [c for c in checks if c["итог"] != "ок"]
    for c in checks:
        print(f'[{"OK " if c["итог"] == "ок" else "FAIL"}] {c["проверка"]}: {c["факт"]}')
    for d in title_details:
        print(f'    кадр {d["кадр"]}: глифы {d["средняя_яркость_глифов"]}, '
              f'покрытие {d["доля_ярких_пикселей_глифа"]}, фон ≤ {d["макс_яркость_фона_плашки"]}')
    print(f"\nвсего проверок: {len(checks)}, неудачных: {len(failed)}")
    print(f"протокол: {RESULT}")
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main())
