"""
FikoYT MVD — LTX Video Post-Processing Node'ları.

İçerik:
  - FikoYT_MVD_VideoCombiner: Birden fazla segmenti birleştir + opsiyonel master ses
  - FikoYT_MVD_VideoUpscaler: Video upscale (ffmpeg lanczos/spline veya Real-ESRGAN)
  - FikoYT_MVD_VideoBatchManager: Segment manifest JSON üretici
"""

from __future__ import annotations

import glob
import json
import os
import shutil
import subprocess
import tempfile
import urllib.parse
from typing import List, Tuple

try:
    import folder_paths  # ComfyUI
except Exception:  # pragma: no cover
    folder_paths = None  # type: ignore

from .helpers import _find_ffmpeg, _run_ffmpeg, _video_duration_sec, _safe_filename


# ---------------------------------------------------------------------------
# Ortak yardımcılar
# ---------------------------------------------------------------------------

# Yaygın video uzantıları (video file picker COMBO için)
_VIDEO_EXTS = (".mp4", ".webm", ".mov", ".mkv", ".avi", ".m4v", ".flv")


def _list_video_files() -> list:
    """ComfyUI input/ ve output/ klasörlerini tarayıp video dosyalarını döndür.
    LyricBurner / ColorGrader / AspectRatioBatch COMBO dropdown'ları için."""
    files = []
    if folder_paths is None:
        return ["(input/output klasoru bulunamadi)"]
    # Önce input/ — kullanıcı kendi videosunu buraya koyabilir
    try:
        in_dir = folder_paths.get_input_directory()
        if in_dir and os.path.isdir(in_dir):
            for f in sorted(os.listdir(in_dir)):
                if f.startswith("."): continue
                if f.lower().endswith(_VIDEO_EXTS):
                    files.append(f)
    except Exception: pass
    # Sonra output/ kökü — recursive değil (sadece kök seviyesi)
    try:
        out_dir = folder_paths.get_output_directory()
        if out_dir and os.path.isdir(out_dir):
            for f in sorted(os.listdir(out_dir)):
                if f.startswith("."): continue
                if f.lower().endswith(_VIDEO_EXTS):
                    files.append(f"[output] {f}")
            # output/ alt-klasörlerini de tek seviye tara (örn fikoyt_mvd/)
            for sub in sorted(os.listdir(out_dir)):
                sub_path = os.path.join(out_dir, sub)
                if not os.path.isdir(sub_path) or sub.startswith("."):
                    continue
                # 2 seviye recursive — projects altında çok dosya var
                for root, dirs, sub_files in os.walk(sub_path):
                    rel = os.path.relpath(root, out_dir).replace("\\", "/")
                    depth = rel.count("/")
                    if depth > 2:
                        dirs[:] = []  # daha derine inme
                        continue
                    for fn in sub_files:
                        if fn.startswith("."): continue
                        if fn.lower().endswith(_VIDEO_EXTS):
                            files.append(f"[output] {rel}/{fn}")
    except Exception: pass
    if not files:
        files = ["(input/ veya output/ klasorune .mp4 koy)"]
    return files


def _resolve_video_path(video_file: str, custom_path: str = "") -> str:
    """COMBO secimi (video_file) + opsiyonel custom_path'i tam dosya yoluna cevir.
    custom_path doluysa o tercih edilir.
    'video_file' formatlari:
      - 'sarki.mp4'         → input/sarki.mp4
      - '[output] x.mp4'    → output/x.mp4
      - '[output] sub/x.mp4' → output/sub/x.mp4
    """
    custom = (custom_path or "").strip()
    if custom:
        # Absolute path mi?
        if os.path.isabs(custom) and os.path.isfile(custom):
            return custom
        # Relative — input/ altında kabul et
        if folder_paths is not None:
            cand = os.path.join(folder_paths.get_input_directory(), custom)
            if os.path.isfile(cand):
                return cand
        # Yoksa olduğu gibi döndür (hata downstream'de)
        return custom

    vf = (video_file or "").strip()
    if not vf or vf.startswith("("):
        return ""

    if folder_paths is None:
        return vf

    if vf.startswith("[output] "):
        rel = vf[9:]  # "[output] " prefix kaldır
        return os.path.join(folder_paths.get_output_directory(), rel.replace("/", os.sep))
    else:
        return os.path.join(folder_paths.get_input_directory(), vf)


def _output_dir(project_dir: str) -> str:
    """project_dir verilmişse onu, yoksa ComfyUI output klasörünü döndür."""
    if project_dir and project_dir.strip():
        os.makedirs(project_dir, exist_ok=True)
        return project_dir
    if folder_paths is not None:
        try:
            kok = folder_paths.get_output_directory()
            if kok:
                os.makedirs(kok, exist_ok=True)
                return kok
        except Exception:
            pass
    fallback = os.path.join(os.getcwd(), "output")
    os.makedirs(fallback, exist_ok=True)
    return fallback


def _comfy_view_url(file_path: str) -> str:
    """ComfyUI /view URL formatı üretir."""
    if not file_path:
        return ""
    ad = os.path.basename(file_path)
    try:
        param = urllib.parse.urlencode({"filename": ad, "type": "output"})
        return f"/view?{param}"
    except Exception:
        return f"/view?filename={ad}&type=output"


def _parse_resolution(res: str) -> Tuple[int, int]:
    """'1280x720' -> (1280, 720). Hatada (0, 0)."""
    try:
        w, h = res.lower().split("x")
        return (int(w.strip()), int(h.strip()))
    except Exception:
        return (0, 0)


# ===========================================================================
# 1) FikoYT_MVD_VideoCombiner
# ===========================================================================

class FikoYT_MVD_VideoCombiner:
    """Birden fazla segment videosunu birleştir + opsiyonel master ses ekle."""

    CATEGORY = "FikoYT/MVD/Video"
    FUNCTION = "combine"
    RETURN_TYPES = ("STRING", "STRING", "STRING")
    RETURN_NAMES = ("output_path", "output_url", "report")
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        # ffmpeg xfade transition tipleri (V6)
        transitions = [
            "fade", "fadeblack", "fadewhite", "wipeleft", "wiperight",
            "wipeup", "wipedown", "slideleft", "slideright", "slideup", "slidedown",
            "circleopen", "circleclose", "vertopen", "vertclose",
            "horzopen", "horzclose", "diagtl", "diagtr", "diagbl", "diagbr",
            "hlslice", "hrslice", "vuslice", "vdslice",
            "dissolve", "pixelize", "radial", "smoothleft", "smoothright",
            "smoothup", "smoothdown", "zoomin", "fadegrays",
        ]
        return {
            "required": {
                "video_paths_json": ("STRING", {"multiline": True, "default": "[]"}),
                "output_name": ("STRING", {"default": "klip_final.mp4"}),
                "project_dir": ("STRING", {"default": ""}),
            },
            "optional": {
                "master_audio": ("STRING", {"default": ""}),
                "crossfade_ms": ("INT", {"default": 0, "min": 0, "max": 2000}),
                "transition_type": (transitions, {"default": "fade",
                    "tooltip": "Geçiş tipi (crossfade_ms>0 ise aktif). "
                               "hlslice/hrslice = whip-pan, zoomin = zoom transition, "
                               "fadeblack = sinema fade, dissolve = klasik geçiş"}),
                "output_fps": ("INT", {"default": 24, "min": 12, "max": 60}),
                "output_resolution": ("STRING", {"default": "auto"}),
                "bitrate_mbps": ("INT", {"default": 8, "min": 1, "max": 50}),
            },
        }

    # -----------------------------------------------------------------------
    def combine(
        self,
        video_paths_json: str,
        output_name: str,
        project_dir: str,
        master_audio: str = "",
        crossfade_ms: int = 0,
        transition_type: str = "fade",
        output_fps: int = 24,
        output_resolution: str = "auto",
        bitrate_mbps: int = 8,
    ):
        rapor: List[str] = []

        # ffmpeg kontrol
        if _find_ffmpeg() is None:
            mesaj = (
                "HATA: ffmpeg bulunamadı.\n"
                "Çözüm: ffmpeg.exe System PATH'e ekle veya FFMPEG_BIN env değişkeni ayarla.\n"
                "İndir: https://www.gyan.dev/ffmpeg/builds/"
            )
            return ("", "", mesaj)

        # JSON parse
        try:
            yollar = json.loads(video_paths_json or "[]")
            if not isinstance(yollar, list):
                raise ValueError("video_paths_json bir liste olmalı")
        except Exception as e:
            return ("", "", f"HATA: video_paths_json parse edilemedi: {e}")

        # Var olan dosyaları filtrele
        gecerli: List[str] = []
        for y in yollar:
            if isinstance(y, str) and os.path.isfile(y):
                gecerli.append(os.path.abspath(y))
            else:
                rapor.append(f"⚠ Atlandı (bulunamadı): {y}")

        if not gecerli:
            return ("", "", "HATA: Geçerli video dosyası yok\n" + "\n".join(rapor))

        rapor.append(f"✓ {len(gecerli)} segment birleştirilecek")

        # Çıktı yolu
        out_dir = _output_dir(project_dir)
        ad_temiz = output_name.strip() or "klip_final.mp4"
        if not ad_temiz.lower().endswith(".mp4"):
            ad_temiz += ".mp4"
        cikti = os.path.join(out_dir, ad_temiz)

        # Çözünürlük
        w, h = _parse_resolution(output_resolution) if output_resolution != "auto" else (0, 0)

        # Video filter zinciri
        vf_parcalar: List[str] = []
        if w > 0 and h > 0:
            vf_parcalar.append(f"scale={w}:{h}:flags=lanczos")
        vf_parcalar.append(f"fps={output_fps}")
        vf_str = ",".join(vf_parcalar)

        ortak_v = [
            "-c:v", "libx264", "-crf", "18", "-preset", "medium",
            "-pix_fmt", "yuv420p", "-b:v", f"{bitrate_mbps}M",
            "-vf", vf_str,
        ]

        # ---- Crossfade yolu (xfade filtre) ----
        crossfade_basarili = False
        ara_video = os.path.join(out_dir, f"_concat_{_safe_filename(ad_temiz, 30)}.mp4")

        if crossfade_ms > 0 and len(gecerli) >= 2:
            rapor.append(f"→ Transition deneniyor ({transition_type}, {crossfade_ms}ms)")
            try:
                kod, err = self._xfade_concat(
                    gecerli, ara_video, crossfade_ms, output_fps, bitrate_mbps, w, h,
                    transition=transition_type,
                )
                if kod == 0 and os.path.isfile(ara_video):
                    crossfade_basarili = True
                    rapor.append(f"✓ Transition başarılı ({transition_type})")
                else:
                    rapor.append(f"⚠ Transition başarısız (kod={kod}), düz concat'e geçiliyor")
            except Exception as e:
                rapor.append(f"⚠ Transition hata: {e}")

        # ---- Düz concat (fallback veya crossfade=0) ----
        if not crossfade_basarili:
            with tempfile.NamedTemporaryFile(
                "w", suffix=".txt", delete=False, encoding="utf-8"
            ) as f:
                liste_yolu = f.name
                for v in gecerli:
                    # ffmpeg concat list: tek tırnak içinde, içerideki ' kaçırılır
                    guvenli = v.replace("\\", "/").replace("'", "'\\''")
                    f.write(f"file '{guvenli}'\n")

            try:
                kod, err = _run_ffmpeg([
                    "-y", "-f", "concat", "-safe", "0",
                    "-i", liste_yolu,
                    *ortak_v,
                    "-an",
                    ara_video,
                ])
                if kod != 0:
                    rapor.append(f"HATA: concat başarısız\n{err[-500:]}")
                    return ("", "", "\n".join(rapor))
                rapor.append("✓ Concat tamamlandı")
            finally:
                try:
                    os.unlink(liste_yolu)
                except Exception:
                    pass

        # ---- Master audio mux ----
        if master_audio and os.path.isfile(master_audio):
            rapor.append("→ Master ses ekleniyor")
            kod, err = _run_ffmpeg([
                "-y",
                "-i", ara_video,
                "-i", master_audio,
                "-c:v", "copy",
                "-c:a", "aac", "-b:a", "192k",
                "-shortest",
                "-map", "0:v:0", "-map", "1:a:0",
                cikti,
            ])
            if kod != 0:
                rapor.append(f"⚠ Ses mux başarısız: {err[-300:]}")
                # Sessiz versiyonu kullan
                try:
                    shutil.move(ara_video, cikti)
                except Exception:
                    pass
            else:
                rapor.append("✓ Master ses eklendi")
                try:
                    os.unlink(ara_video)
                except Exception:
                    pass
        else:
            try:
                shutil.move(ara_video, cikti)
            except Exception as e:
                rapor.append(f"⚠ Dosya taşıma hatası: {e}")

        if not os.path.isfile(cikti):
            return ("", "", "HATA: Çıktı dosyası oluşmadı\n" + "\n".join(rapor))

        sure = _video_duration_sec(cikti)
        boyut_mb = os.path.getsize(cikti) / (1024 * 1024)
        rapor.append(f"✓ Çıktı: {cikti}")
        rapor.append(f"   Süre: {sure:.2f}s | Boyut: {boyut_mb:.2f} MB | FPS: {output_fps}")

        url = _comfy_view_url(cikti)
        return (cikti, url, "\n".join(rapor))

    # -----------------------------------------------------------------------
    def _xfade_concat(
        self,
        yollar: List[str],
        cikti: str,
        crossfade_ms: int,
        fps: int,
        bitrate_mbps: int,
        w: int,
        h: int,
        transition: str = "fade",
    ) -> Tuple[int, str]:
        """xfade filter ile transition'lı concat. Sadece 2+ video için.
        transition: ffmpeg xfade tip adı (fade, hlslice, dissolve, zoomin, vb.)"""
        n = len(yollar)
        d_sn = crossfade_ms / 1000.0

        # Her videonun süresini al
        sureler = [_video_duration_sec(p) for p in yollar]
        if any(s <= 0 for s in sureler):
            return (1, "süre alınamadı")

        # Input args
        args: List[str] = ["-y"]
        for p in yollar:
            args += ["-i", p]

        # Filter complex
        # Önce her input scale + fps
        olcek = ""
        if w > 0 and h > 0:
            olcek = f",scale={w}:{h}:flags=lanczos,setsar=1"

        filtre_parts: List[str] = []
        for i in range(n):
            filtre_parts.append(f"[{i}:v]fps={fps}{olcek}[v{i}]")

        # Zincirleme xfade
        # offset birikimli: sum(sureler[0..i]) - d_sn*(i+1)? Aslında xfade offset, A'nın başlangıcından
        # itibaren geçişin başlayacağı an. İlk geçişte offset = sure0 - d_sn
        prev = "v0"
        kum_offset = sureler[0]
        for i in range(1, n):
            offset = kum_offset - d_sn
            if offset < 0:
                offset = 0.0
            etiket = f"vx{i}"
            filtre_parts.append(
                f"[{prev}][v{i}]xfade=transition={transition}:duration={d_sn:.3f}:offset={offset:.3f}[{etiket}]"
            )
            prev = etiket
            kum_offset += sureler[i] - d_sn

        filtre = ";".join(filtre_parts)

        args += [
            "-filter_complex", filtre,
            "-map", f"[{prev}]",
            "-c:v", "libx264", "-crf", "18", "-preset", "medium",
            "-pix_fmt", "yuv420p", "-b:v", f"{bitrate_mbps}M",
            "-an",
            cikti,
        ]
        return _run_ffmpeg(args, timeout=1200)


# ===========================================================================
# 2) FikoYT_MVD_VideoUpscaler
# ===========================================================================

class FikoYT_MVD_VideoUpscaler:
    """Video upscale (ffmpeg lanczos/spline veya Real-ESRGAN harici)."""

    CATEGORY = "FikoYT/MVD/Video"
    FUNCTION = "upscale"
    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("output_path", "report")
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "video_path": ("STRING", {"default": ""}),
                "scale": (["2x", "3x", "4x", "custom"], {"default": "2x"}),
                "method": (
                    ["ffmpeg_lanczos", "ffmpeg_spline", "realesrgan_external"],
                    {"default": "ffmpeg_lanczos"},
                ),
            },
            "optional": {
                "custom_resolution": ("STRING", {"default": ""}),
                "output_suffix": ("STRING", {"default": "_upscaled"}),
                "fps_override": ("INT", {"default": 0, "min": 0, "max": 120}),
            },
        }

    # -----------------------------------------------------------------------
    def upscale(
        self,
        video_path: str,
        scale: str,
        method: str,
        custom_resolution: str = "",
        output_suffix: str = "_upscaled",
        fps_override: int = 0,
    ):
        rapor: List[str] = []

        if _find_ffmpeg() is None:
            return ("", "HATA: ffmpeg bulunamadı. PATH'e ffmpeg ekle.")

        if not video_path or not os.path.isfile(video_path):
            return ("", f"HATA: Video bulunamadı: {video_path}")

        # Kaynak çözünürlüğü al
        src_w, src_h = self._video_resolution(video_path)
        if src_w == 0 or src_h == 0:
            rapor.append("⚠ Kaynak çözünürlüğü okunamadı, 'iw/ih' kullanılacak")

        # Hedef çözünürlük
        ozel_w, ozel_h = (0, 0)
        if scale == "custom":
            ozel_w, ozel_h = _parse_resolution(custom_resolution)
            if ozel_w == 0 or ozel_h == 0:
                return ("", "HATA: custom seçildi ama custom_resolution geçersiz (örn 1920x1080)")
            scale_filtre_w, scale_filtre_h = ozel_w, ozel_h
        else:
            try:
                carpan = int(scale.replace("x", ""))
            except Exception:
                carpan = 2
            if src_w > 0:
                scale_filtre_w = src_w * carpan
                scale_filtre_h = src_h * carpan
            else:
                scale_filtre_w = f"iw*{carpan}"  # type: ignore
                scale_filtre_h = f"ih*{carpan}"  # type: ignore

        # Çıktı yolu
        kok, uzanti = os.path.splitext(video_path)
        suffix = output_suffix or "_upscaled"
        cikti = f"{kok}{suffix}.mp4"

        # Real-ESRGAN dene
        if method == "realesrgan_external":
            esrgan = shutil.which("realesrgan-ncnn-vulkan")
            if esrgan is None:
                rapor.append("⚠ realesrgan-ncnn-vulkan PATH'te yok, lanczos fallback")
                method = "ffmpeg_lanczos"
            else:
                rapor.append("→ Real-ESRGAN harici binary dene")
                kod, mesaj = self._realesrgan_run(esrgan, video_path, cikti, scale)
                if kod == 0 and os.path.isfile(cikti):
                    rapor.append(f"✓ Real-ESRGAN tamamlandı → {cikti}")
                    return (cikti, "\n".join(rapor))
                rapor.append(f"⚠ Real-ESRGAN başarısız: {mesaj}")
                rapor.append("→ ffmpeg lanczos fallback")
                method = "ffmpeg_lanczos"

        # ffmpeg scale
        flag = "lanczos" if method == "ffmpeg_lanczos" else "spline"
        vf = f"scale={scale_filtre_w}:{scale_filtre_h}:flags={flag}"

        args = ["-y", "-i", video_path, "-vf", vf,
                "-c:v", "libx264", "-crf", "17", "-preset", "medium",
                "-pix_fmt", "yuv420p"]
        if fps_override and fps_override > 0:
            args += ["-r", str(fps_override)]
        args += ["-c:a", "copy", cikti]

        kod, err = _run_ffmpeg(args, timeout=1800)
        if kod != 0:
            return ("", f"HATA: ffmpeg upscale başarısız\n{err[-500:]}")

        rapor.append(f"✓ {src_w}x{src_h} → {scale_filtre_w}x{scale_filtre_h} ({flag})")
        rapor.append(f"   Çıktı: {cikti}")
        return (cikti, "\n".join(rapor))

    # -----------------------------------------------------------------------
    def _video_resolution(self, path: str) -> Tuple[int, int]:
        """ffprobe ile width/height al."""
        from .helpers import _find_ffprobe  # local import (dosya başında değil)
        probe = _find_ffprobe()
        if probe is None:
            return (0, 0)
        try:
            sonuc = subprocess.run(
                [
                    probe, "-v", "error",
                    "-select_streams", "v:0",
                    "-show_entries", "stream=width,height",
                    "-of", "csv=p=0:s=x",
                    path,
                ],
                capture_output=True, text=True, timeout=30, check=False,
            )
            metin = (sonuc.stdout or "").strip()
            if metin and "x" in metin:
                w, h = metin.split("x")
                return (int(w), int(h))
        except Exception:
            pass
        return (0, 0)

    def _realesrgan_run(
        self, binary: str, giris: str, cikis: str, scale: str
    ) -> Tuple[int, str]:
        """realesrgan-ncnn-vulkan video desteklemez (frame frame), basit deneme."""
        # Bu binary aslında image-only; gerçek video için frame ayırma+birleştirme gerekir.
        # Burada basit subprocess çağrısı yapıyoruz; başarısız olursa caller fallback'e geçer.
        try:
            carpan = scale.replace("x", "") if scale != "custom" else "2"
            sonuc = subprocess.run(
                [binary, "-i", giris, "-o", cikis, "-s", carpan],
                capture_output=True, text=True, timeout=1800, check=False,
            )
            return (sonuc.returncode, sonuc.stderr or "")
        except Exception as e:
            return (1, str(e))


# ===========================================================================
# 3) FikoYT_MVD_VideoBatchManager
# ===========================================================================

class FikoYT_MVD_VideoBatchManager:
    """Segment metadata + tüm video/audio path'lerini topla, manifest JSON üret."""

    CATEGORY = "FikoYT/MVD/Video"
    FUNCTION = "manage"
    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("manifest_json", "report")
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "project_dir": ("STRING", {"default": ""}),
            },
            "optional": {
                "include_audio": ("BOOLEAN", {"default": True}),
                "include_videos": ("BOOLEAN", {"default": True}),
                "pattern": ("STRING", {"default": "seg_*.mp4"}),
            },
        }

    # -----------------------------------------------------------------------
    def manage(
        self,
        project_dir: str,
        include_audio: bool = True,
        include_videos: bool = True,
        pattern: str = "seg_*.mp4",
    ):
        rapor: List[str] = []

        if not project_dir or not os.path.isdir(project_dir):
            manifest = {"project_dir": project_dir, "segments": [], "total_videos": 0, "total_duration": 0.0}
            return (json.dumps(manifest, ensure_ascii=False), f"HATA: project_dir geçersiz: {project_dir}")

        video_klasor = os.path.join(project_dir, "videos")
        audio_klasor = os.path.join(project_dir, "audio")

        # Video glob
        video_yollari: List[str] = []
        if include_videos and os.path.isdir(video_klasor):
            try:
                video_yollari = sorted(glob.glob(os.path.join(video_klasor, pattern)))
            except Exception as e:
                rapor.append(f"⚠ Video glob hatası: {e}")

        # Audio glob (mp3/wav/m4a)
        audio_haritasi = {}
        if include_audio and os.path.isdir(audio_klasor):
            for uzanti in ("mp3", "wav", "m4a"):
                a_pattern = pattern.replace(".mp4", f".{uzanti}")
                try:
                    for ap in glob.glob(os.path.join(audio_klasor, a_pattern)):
                        kok_ad = os.path.splitext(os.path.basename(ap))[0]
                        audio_haritasi[kok_ad] = ap
                except Exception:
                    pass

        # Segment listesi
        segmentler = []
        toplam_sure = 0.0
        for i, vp in enumerate(video_yollari):
            ad_kok = os.path.splitext(os.path.basename(vp))[0]
            # Tip çıkarımı: seg_<idx>_<tip>.mp4 desenini varsay
            tip = "segment"
            parts = ad_kok.split("_")
            if len(parts) >= 3:
                tip = "_".join(parts[2:])
            elif len(parts) == 2:
                tip = parts[1]

            sure = _video_duration_sec(vp)
            toplam_sure += sure

            seg = {
                "index": i,
                "type": tip,
                "video_path": vp,
                "audio_path": audio_haritasi.get(ad_kok, ""),
                "duration": round(sure, 3),
            }
            segmentler.append(seg)

        manifest = {
            "project_dir": project_dir,
            "segments": segmentler,
            "total_videos": len(segmentler),
            "total_duration": round(toplam_sure, 3),
        }

        rapor.append(f"✓ {len(segmentler)} segment bulundu")
        rapor.append(f"   Toplam süre: {toplam_sure:.2f}s")
        if include_audio:
            eslesen = sum(1 for s in segmentler if s["audio_path"])
            rapor.append(f"   Ses eşleşen: {eslesen}/{len(segmentler)}")

        return (json.dumps(manifest, ensure_ascii=False, indent=2), "\n".join(rapor))


# ===========================================================================
# 4) FikoYT_MVD_LyricBurner — Karaoke style lyric/subtitle burn-in (V1)
# ===========================================================================

# ASS color format: &HBBGGRR& (alfa+blue+green+red)
def _hex_to_ass_color(hex_str: str, default: str = "&H00FFFFFF") -> str:
    """#RRGGBB → ASS &HAABBGGRR (alfa 00 = solid)."""
    h = (hex_str or "").strip().lstrip("#")
    if len(h) != 6:
        return default
    try:
        r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
        return f"&H00{b:02X}{g:02X}{r:02X}"
    except Exception:
        return default


def _seconds_to_ass_time(s: float) -> str:
    """Saniye → ASS H:MM:SS.cc formatı (centisecond precision)."""
    if s < 0:
        s = 0
    h = int(s // 3600)
    m = int((s % 3600) // 60)
    sec = s - h * 3600 - m * 60
    return f"{h}:{m:02d}:{sec:05.2f}"


def _build_ass_subtitle(events: List[dict], style: dict, video_w: int = 1920,
                        video_h: int = 1080) -> str:
    """Lyric event listesinden ASS subtitle dosyası string'i üret.

    events: [{start, end, text}, ...]
    style: {font_family, font_size, primary_color, outline_color, outline_width,
            position ('top'/'middle'/'bottom'), bg_opacity (0-100)}
    """
    font_family = style.get("font_family") or "Arial"
    font_size = int(style.get("font_size") or 48)
    primary_c = _hex_to_ass_color(style.get("font_color") or "#FFFFFF", "&H00FFFFFF")
    outline_c = _hex_to_ass_color(style.get("outline_color") or "#000000", "&H00000000")
    outline_w = int(style.get("outline_width") or 2)
    position = (style.get("position") or "bottom").lower()
    bg_opacity = int(style.get("bg_opacity") or 0)  # 0=şeffaf

    # Alignment: ASS numpad layout — 2=alt orta, 5=orta orta, 8=üst orta
    align = {"top": 8, "middle": 5, "bottom": 2}.get(position, 2)
    # Margins
    margin_v = 60 if position == "bottom" else (60 if position == "top" else 0)

    # Back color (subtitle box bg) — opacity 0-100 → ASS alpha (FF=şeffaf, 00=opak)
    bg_alpha = max(0, min(255, int(255 - bg_opacity * 2.55)))
    back_c = f"&H{bg_alpha:02X}000000"
    border_style = 1 if bg_opacity == 0 else 3  # 1=outline, 3=opaque box

    ass = []
    ass.append("[Script Info]")
    ass.append("Title: FikoYT MVD Lyrics")
    ass.append("ScriptType: v4.00+")
    ass.append(f"PlayResX: {video_w}")
    ass.append(f"PlayResY: {video_h}")
    ass.append("WrapStyle: 0")
    ass.append("ScaledBorderAndShadow: yes")
    ass.append("")
    ass.append("[V4+ Styles]")
    ass.append("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, "
               "OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, "
               "ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, "
               "Alignment, MarginL, MarginR, MarginV, Encoding")
    ass.append(f"Style: Default,{font_family},{font_size},{primary_c},&H000000FF,"
               f"{outline_c},{back_c},-1,0,0,0,100,100,0,0,{border_style},{outline_w},"
               f"1,{align},20,20,{margin_v},1")
    ass.append("")
    ass.append("[Events]")
    ass.append("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, "
               "Effect, Text")
    for ev in events:
        try:
            start = float(ev.get("start", 0))
            end = float(ev.get("end", start + 2))
            text = (ev.get("text") or "").strip()
            if not text:
                continue
            # ASS text içinde \N satır sonu; backslash zaten geçer
            text = text.replace("\n", "\\N").replace("{", "(").replace("}", ")")
            ass.append(f"Dialogue: 0,{_seconds_to_ass_time(start)},"
                       f"{_seconds_to_ass_time(end)},Default,,0,0,0,,{text}")
        except Exception:
            continue
    return "\n".join(ass) + "\n"


class FikoYT_MVD_LyricBurner:
    """Video üstüne lyric/subtitle yak (karaoke-style). Whisper word-level
    timestamps veya manuel JSON [{start, end, text}, ...] kabul eder."""

    CATEGORY = "FikoYT/MVD/Video"
    FUNCTION = "burn"
    RETURN_TYPES = ("STRING", "STRING", "STRING")
    RETURN_NAMES = ("output_path", "output_url", "report")
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        video_files = _list_video_files()
        return {
            "required": {
                "video_file": (video_files, {
                    "tooltip": "input/ veya output/ klasorundeki video. Manuel path icin "
                               "asagi 'custom_video_path' alanini kullan."
                }),
                "lyrics_json": ("STRING", {"multiline": True, "default": "[]",
                    "tooltip": "Liste: [{\"start\":0.0,\"end\":2.5,\"text\":\"...\"}]"}),
            },
            "optional": {
                "custom_video_path": ("STRING", {"default": "", "multiline": False,
                    "placeholder": "Manuel tam yol (ornek: D:\\videolar\\klip.mp4) — varsa dropdown'i override eder"
                }),
                "position": (["bottom", "middle", "top"], {"default": "bottom"}),
                "font_family": ("STRING", {"default": "Arial"}),
                "font_size": ("INT", {"default": 48, "min": 12, "max": 120}),
                "font_color": ("STRING", {"default": "#FFFFFF"}),
                "outline_color": ("STRING", {"default": "#000000"}),
                "outline_width": ("INT", {"default": 2, "min": 0, "max": 8}),
                "bg_opacity": ("INT", {"default": 0, "min": 0, "max": 100,
                    "tooltip": "Subtitle arka plan opaklığı 0 (şeffaf) - 100 (opak siyah kutu)"}),
                "output_suffix": ("STRING", {"default": "_lyrics"}),
                "subtitle_format": (["ass", "srt"], {"default": "ass",
                    "tooltip": "ASS daha gelişmiş (karaoke için), SRT basit"}),
            },
        }

    def burn(self, video_file: str, lyrics_json: str,
             custom_video_path: str = "",
             position: str = "bottom", font_family: str = "Arial",
             font_size: int = 48, font_color: str = "#FFFFFF",
             outline_color: str = "#000000", outline_width: int = 2,
             bg_opacity: int = 0, output_suffix: str = "_lyrics",
             subtitle_format: str = "ass"):
        rapor: List[str] = []

        video_path = _resolve_video_path(video_file, custom_video_path)

        if _find_ffmpeg() is None:
            return ("", "", "HATA: ffmpeg bulunamadı.")
        if not video_path or not os.path.isfile(video_path):
            return ("", "", f"HATA: Video yok: {video_path}\n"
                            f"  COMBO: {video_file!r}\n"
                            f"  custom_video_path: {custom_video_path!r}")

        try:
            events = json.loads(lyrics_json or "[]")
            if not isinstance(events, list):
                raise ValueError("lyrics_json bir liste olmalı")
        except Exception as e:
            return ("", "", f"HATA: lyrics_json parse: {e}")

        if not events:
            return ("", "", "HATA: lyrics_json boş")

        rapor.append(f"✓ {len(events)} lyric event")

        # Video çözünürlüğünü al (ASS PlayResX/Y için)
        from .helpers import _find_ffprobe
        probe = _find_ffprobe()
        video_w, video_h = 1920, 1080
        if probe:
            try:
                pr = subprocess.run(
                    [probe, "-v", "error", "-select_streams", "v:0",
                     "-show_entries", "stream=width,height",
                     "-of", "csv=p=0:s=x", video_path],
                    capture_output=True, text=True, timeout=15)
                out = (pr.stdout or "").strip()
                if "x" in out:
                    w, h = out.split("x")
                    video_w, video_h = int(w), int(h)
            except Exception: pass

        # Subtitle dosyası üret
        kok, _ext = os.path.splitext(video_path)
        sub_ext = ".ass" if subtitle_format == "ass" else ".srt"
        sub_path = f"{kok}{output_suffix}{sub_ext}"
        cikti = f"{kok}{output_suffix}.mp4"

        style = {
            "font_family": font_family, "font_size": font_size,
            "font_color": font_color, "outline_color": outline_color,
            "outline_width": outline_width, "position": position,
            "bg_opacity": bg_opacity,
        }

        try:
            if subtitle_format == "ass":
                sub_content = _build_ass_subtitle(events, style, video_w, video_h)
            else:
                # SRT basit format
                lines = []
                for i, ev in enumerate(events, 1):
                    try:
                        start = float(ev.get("start", 0))
                        end = float(ev.get("end", start + 2))
                        text = (ev.get("text") or "").strip()
                        if not text: continue
                        def _srt_time(s):
                            h = int(s // 3600); m = int((s % 3600) // 60)
                            sec = s - h*3600 - m*60
                            return f"{h:02d}:{m:02d}:{sec:06.3f}".replace(".", ",")
                        lines.append(f"{i}\n{_srt_time(start)} --> {_srt_time(end)}\n{text}\n")
                    except Exception: continue
                sub_content = "\n".join(lines)
            with open(sub_path, "w", encoding="utf-8") as f:
                f.write(sub_content)
            rapor.append(f"✓ Subtitle dosyası: {os.path.basename(sub_path)}")
        except Exception as e:
            return ("", "", f"HATA: subtitle yazımı: {e}")

        # ffmpeg filter — ASS için ass filtresi, SRT için subtitles filtresi
        # Windows path için backslash escape gerek: subtitles='C\:/path/file.ass'
        sub_for_filter = sub_path.replace("\\", "/").replace(":", "\\:")
        if subtitle_format == "ass":
            vf = f"ass='{sub_for_filter}'"
        else:
            vf = f"subtitles='{sub_for_filter}'"

        args = ["-y", "-i", video_path, "-vf", vf,
                "-c:v", "libx264", "-crf", "18", "-preset", "medium",
                "-pix_fmt", "yuv420p", "-c:a", "copy", cikti]

        kod, err = _run_ffmpeg(args, timeout=900)
        if kod != 0:
            return ("", "", f"HATA: ffmpeg lyric burn: {err[-400:]}")

        rapor.append(f"✓ Lyric yakıldı: {cikti}")
        url = _comfy_view_url(cikti)
        return (cikti, url, "\n".join(rapor))


# ===========================================================================
# 5a) FikoYT_MVD_BeatSnapTrimmer — video sürelerini beat grid'e snap eder (V3)
# ===========================================================================
class FikoYT_MVD_BeatSnapTrimmer:
    """Bir veya daha çok video dosyasının sürelerini, sağlanan beat_times'a göre
    en yakın beat'e snap eder (sadece kırpar — frame eklemez).

    Kullanım: BeatAnalyzer'dan beat_times_json al → bu node videoları kırp →
    VideoCombiner'a sırayla bağla → cut'lar müziğin beat'ine oturur.
    """
    CATEGORY = "FikoYT/MVD/Video"
    FUNCTION = "snap"
    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("trimmed_paths_json", "report")
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "video_paths_json": ("STRING", {"multiline": True, "default": "[]",
                    "tooltip": "Video yolları listesi"}),
                "beat_times_json": ("STRING", {"multiline": True, "default": "[]",
                    "tooltip": "BeatAnalyzer'dan beat_times_json"}),
            },
            "optional": {
                "snap_target": (["nearest", "downbeat_only", "every_2_beats",
                                  "every_4_beats"], {"default": "nearest"}),
                "max_trim_sec": ("FLOAT", {"default": 0.5, "min": 0.05, "max": 3.0,
                                            "step": 0.05,
                    "tooltip": "Max bu kadar saniye kırpılır. Daha fazlasını gerektiren "
                               "snap noktası reddedilir."}),
                "min_video_duration_sec": ("FLOAT", {"default": 0.5, "min": 0.1,
                                                     "max": 30.0, "step": 0.1,
                    "tooltip": "Bu süreden kısa video'lar kırpılmaz"}),
                "output_suffix": ("STRING", {"default": "_beatsnap"}),
                "concat_start_offset_sec": ("FLOAT", {"default": 0.0, "min": 0.0,
                                                       "max": 60.0, "step": 0.1,
                    "tooltip": "İlk segmentin başlangıcı müzikte hangi saniyede olacak? "
                               "(genelde 0)"}),
            },
        }

    def snap(self, video_paths_json: str, beat_times_json: str,
             snap_target: str = "nearest", max_trim_sec: float = 0.5,
             min_video_duration_sec: float = 0.5,
             output_suffix: str = "_beatsnap",
             concat_start_offset_sec: float = 0.0):
        rapor = []
        if _find_ffmpeg() is None:
            return ("[]", "HATA: ffmpeg yok")

        try:
            video_paths = json.loads(video_paths_json or "[]")
            if not isinstance(video_paths, list):
                raise ValueError("liste değil")
        except Exception as e:
            return ("[]", f"HATA: video_paths_json: {e}")

        try:
            beats = json.loads(beat_times_json or "[]")
            if not isinstance(beats, list) or not beats:
                raise ValueError("boş veya liste değil")
            beats = sorted(float(b) for b in beats)
        except Exception as e:
            return ("[]", f"HATA: beat_times_json: {e}")

        # Snap target'a göre alt liste
        if snap_target == "downbeat_only":
            target_beats = beats[::4]
        elif snap_target == "every_2_beats":
            target_beats = beats[::2]
        elif snap_target == "every_4_beats":
            target_beats = beats[::4]
        else:
            target_beats = beats

        rapor.append(f"✓ {len(target_beats)} snap point ({snap_target})")
        rapor.append(f"✓ {len(video_paths)} video işlenecek")

        outputs = []
        cum_time = float(concat_start_offset_sec)

        for idx, vp in enumerate(video_paths):
            if not isinstance(vp, str) or not os.path.isfile(vp):
                rapor.append(f"⚠ Atlandı: {vp}")
                continue
            dur = _video_duration_sec(vp)
            if dur <= 0:
                rapor.append(f"⚠ Süre alınamadı: {os.path.basename(vp)}")
                outputs.append(vp)  # orijinali geri ver
                continue
            if dur < min_video_duration_sec:
                rapor.append(f"⚠ {os.path.basename(vp)} çok kısa ({dur:.2f}s), kırpılmadı")
                outputs.append(vp)
                cum_time += dur
                continue

            # Bu videonun ideal bitiş zamanı: cum_time + dur
            ideal_end = cum_time + dur
            # En yakın beat
            if not target_beats:
                outputs.append(vp); cum_time += dur; continue
            nearest = min(target_beats, key=lambda b: abs(b - ideal_end))
            new_dur = nearest - cum_time
            if new_dur < min_video_duration_sec:
                # Bir sonraki beat'i dene
                later = [b for b in target_beats if b > cum_time + min_video_duration_sec]
                if later:
                    nearest = later[0]
                    new_dur = nearest - cum_time
                else:
                    outputs.append(vp); cum_time += dur; continue
            trim_amount = dur - new_dur
            if abs(trim_amount) > max_trim_sec:
                # Snap noktası çok uzak — orijinali kullan
                rapor.append(f"  [{idx + 1}] snap çok uzak ({trim_amount:.2f}s), "
                             f"orijinal kullanıldı")
                outputs.append(vp); cum_time += dur; continue

            # Kırp (stream copy hızlı — ama keyframe'e snap edebilir)
            kok, _ext = os.path.splitext(vp)
            cikti = f"{kok}{output_suffix}.mp4"
            # -ss 0 -t new_dur ile kırp; re-encode (keyframe snap için)
            args = ["-y", "-i", vp, "-t", f"{new_dur:.3f}",
                    "-c:v", "libx264", "-crf", "18", "-preset", "fast",
                    "-pix_fmt", "yuv420p", "-c:a", "copy", cikti]
            try:
                kod, err = _run_ffmpeg(args, timeout=300)
                if kod == 0 and os.path.isfile(cikti):
                    outputs.append(cikti)
                    rapor.append(f"  [{idx + 1}] {dur:.2f}s → {new_dur:.2f}s "
                                 f"(snap @ {nearest:.2f}s, trim={trim_amount:.2f}s)")
                    cum_time += new_dur
                else:
                    rapor.append(f"  [{idx + 1}] kırpma başarısız, orijinal: {err[-150:]}")
                    outputs.append(vp); cum_time += dur
            except Exception as e:
                rapor.append(f"  [{idx + 1}] exception: {e}")
                outputs.append(vp); cum_time += dur

        rapor.insert(0, f"✓ Snap tamamlandı, son cum_time={cum_time:.2f}s")
        return (json.dumps(outputs, ensure_ascii=False), "\n".join(rapor))


# ===========================================================================
# 5b) FikoYT_MVD_ColorGrader — Color grading / LUT preset'leri (V4)
# ===========================================================================
# Built-in ffmpeg color filter preset'leri (.cube dosyası gerektirmez).
# Her preset bir ffmpeg filter zinciri olarak tanımlanır — taşınabilir.
_COLOR_PRESETS = {
    "none": {
        "label": "Yok (orijinal)",
        "filter": "",
    },
    "cinematic_teal_orange": {
        "label": "🎬 Cinematic Teal-Orange (Hollywood look)",
        # Shadows → teal/cyan, midtones nötr, highlights → orange/warm
        # colorbalance: -1..1 (rs/gs/bs shadows, rm/gm/bm midtones, rh/gh/bh highlights)
        "filter": "colorbalance=rs=-0.2:gs=0.1:bs=0.3:"
                  "rm=0.05:gm=0:bm=-0.05:"
                  "rh=0.25:gh=0.05:bh=-0.2,"
                  "eq=contrast=1.08:saturation=1.05",
    },
    "vintage_film": {
        "label": "📽 Vintage Film (warm sepia faded)",
        "filter": "curves=preset=vintage,"
                  "colorbalance=rm=0.1:gm=0.05:bm=-0.1,"
                  "eq=contrast=0.92:saturation=0.7:brightness=0.02",
    },
    "noir_high_contrast": {
        "label": "🎞 Noir B&W (high contrast)",
        "filter": "hue=s=0,eq=contrast=1.35:brightness=-0.02",
    },
    "neon_cyberpunk": {
        "label": "🌃 Neon Cyberpunk (magenta+cyan)",
        "filter": "colorbalance=rs=0.15:gs=-0.05:bs=0.25:"
                  "rm=0:gm=-0.1:bm=0.15:"
                  "rh=0.2:gh=-0.1:bh=0.3,"
                  "eq=saturation=1.4:contrast=1.15",
    },
    "warm_sunset": {
        "label": "🌅 Warm Sunset (orange/golden)",
        "filter": "colorbalance=rm=0.2:gm=0.1:bm=-0.15,"
                  "eq=saturation=1.15:contrast=1.05:gamma_r=1.05",
    },
    "cool_blue": {
        "label": "❄️ Cool Blue (icy)",
        "filter": "colorbalance=rm=-0.15:gm=-0.05:bm=0.2,"
                  "eq=saturation=0.95:contrast=1.05:gamma_b=1.08",
    },
    "bleach_bypass": {
        "label": "💀 Bleach Bypass (desaturated high contrast)",
        "filter": "eq=saturation=0.4:contrast=1.4:brightness=0.02",
    },
    "dreamy_pastel": {
        "label": "🌸 Dreamy Pastel (soft low saturation)",
        "filter": "curves=preset=lighter,"
                  "eq=saturation=0.75:contrast=0.92:brightness=0.05,"
                  "gblur=sigma=0.4",
    },
    "vibrant_pop": {
        "label": "🌈 Vibrant Pop (saturated boost)",
        "filter": "eq=saturation=1.4:contrast=1.12:brightness=0.02",
    },
    "fade_film": {
        "label": "🎬 Faded Film (low contrast retro)",
        "filter": "curves=preset=color_negative,"
                  "eq=saturation=0.85:contrast=0.88:brightness=0.04",
    },
    "moody_dark": {
        "label": "🌑 Moody Dark (crushed shadows)",
        "filter": "curves=preset=darker,"
                  "eq=saturation=0.9:contrast=1.25:brightness=-0.05",
    },
    "music_video_punchy": {
        "label": "🎵 Music Video Punchy (bright + saturated)",
        "filter": "eq=saturation=1.25:contrast=1.18:brightness=0.04:gamma=0.95,"
                  "unsharp=5:5:0.5",
    },
}


class FikoYT_MVD_ColorGrader:
    """Video post-process color grading. Built-in preset'ler veya .cube LUT dosyası.

    Preset'ler ffmpeg native filter'larla yazılı (taşınabilir, ek dosya gerekmez).
    Custom .cube LUT için lut_file path ver.
    """
    CATEGORY = "FikoYT/MVD/Video"
    FUNCTION = "grade"
    RETURN_TYPES = ("STRING", "STRING", "STRING")
    RETURN_NAMES = ("output_path", "output_url", "report")
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        presets = list(_COLOR_PRESETS.keys())
        video_files = _list_video_files()
        return {
            "required": {
                "video_file": (video_files, {
                    "tooltip": "input/ veya output/ klasorundeki video. Manuel path icin "
                               "asagi 'custom_video_path' alanini kullan."
                }),
                "preset": (presets, {"default": "cinematic_teal_orange"}),
            },
            "optional": {
                "custom_video_path": ("STRING", {"default": "", "multiline": False,
                    "placeholder": "Manuel tam yol — varsa dropdown override edilir"}),
                "lut_file": ("STRING", {"default": "",
                    "tooltip": "Opsiyonel .cube LUT dosyası tam yolu (preset üstüne uygulanır). "
                               "Boş ise sadece preset."}),
                "lut_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0,
                                            "step": 0.05}),
                "extra_brightness": ("FLOAT", {"default": 0.0, "min": -0.3, "max": 0.3,
                                                "step": 0.01}),
                "extra_contrast": ("FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0,
                                              "step": 0.05}),
                "extra_saturation": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0,
                                                "step": 0.05}),
                "film_grain": ("INT", {"default": 0, "min": 0, "max": 50,
                    "tooltip": "Film grain miktarı (0=kapalı, 10-20 hafif, 30+ ağır)"}),
                "vignette": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05,
                    "tooltip": "Vignette koyuluğu (0=kapalı, 0.3-0.5 ideal)"}),
                "output_suffix": ("STRING", {"default": "_graded"}),
            },
        }

    def grade(self, video_file: str, preset: str,
              custom_video_path: str = "",
              lut_file: str = "", lut_strength: float = 1.0,
              extra_brightness: float = 0.0, extra_contrast: float = 1.0,
              extra_saturation: float = 1.0, film_grain: int = 0,
              vignette: float = 0.0, output_suffix: str = "_graded"):
        rapor: List[str] = []
        video_path = _resolve_video_path(video_file, custom_video_path)
        if _find_ffmpeg() is None:
            return ("", "", "HATA: ffmpeg bulunamadı.")
        if not video_path or not os.path.isfile(video_path):
            return ("", "", f"HATA: Video yok: {video_path}\n"
                            f"  COMBO: {video_file!r}\n"
                            f"  custom_video_path: {custom_video_path!r}")

        # Filter zinciri kur
        filters = []
        preset_cfg = _COLOR_PRESETS.get(preset, _COLOR_PRESETS["none"])
        if preset_cfg["filter"]:
            filters.append(preset_cfg["filter"])
            rapor.append(f"✓ Preset: {preset_cfg['label']}")

        # Custom LUT (preset üstüne)
        if lut_file and lut_file.strip() and os.path.isfile(lut_file.strip()):
            lf = lut_file.strip().replace("\\", "/").replace(":", "\\:")
            if lut_strength >= 0.99:
                filters.append(f"lut3d='{lf}'")
            else:
                # mix orijinal + LUT'lu — ekstra split+overlay maliyetli, basit yaklaşım
                filters.append(f"lut3d='{lf}'")
                rapor.append(f"⚠ lut_strength<1 tam destek için split+blend gerek (basit mod)")
            rapor.append(f"✓ Custom LUT: {os.path.basename(lut_file)}")

        # Ekstra düzeltmeler
        eq_parts = []
        if abs(extra_brightness) > 0.001:
            eq_parts.append(f"brightness={extra_brightness:.3f}")
        if abs(extra_contrast - 1.0) > 0.001:
            eq_parts.append(f"contrast={extra_contrast:.3f}")
        if abs(extra_saturation - 1.0) > 0.001:
            eq_parts.append(f"saturation={extra_saturation:.3f}")
        if eq_parts:
            filters.append("eq=" + ":".join(eq_parts))
            rapor.append(f"✓ Ekstra: {', '.join(eq_parts)}")

        # Film grain
        if film_grain > 0:
            # noise filter: alls=N:allf=t (temporal)
            filters.append(f"noise=alls={film_grain}:allf=t")
            rapor.append(f"✓ Film grain: {film_grain}")

        # Vignette
        if vignette > 0.001:
            # vignette angle PI/5 default; opacity custom via mode
            filters.append(f"vignette=angle=PI/4")
            rapor.append(f"✓ Vignette: {vignette}")

        if not filters:
            return ("", "", "HATA: Hiç filter seçilmedi (preset=none ve ekstra yok)")

        vf = ",".join(filters)

        # Çıktı yolu
        kok, _ext = os.path.splitext(video_path)
        cikti = f"{kok}{output_suffix}.mp4"

        args = ["-y", "-i", video_path, "-vf", vf,
                "-c:v", "libx264", "-crf", "18", "-preset", "medium",
                "-pix_fmt", "yuv420p", "-c:a", "copy", cikti]

        kod, err = _run_ffmpeg(args, timeout=900)
        if kod != 0:
            return ("", "", f"HATA: ffmpeg color grade: {err[-400:]}")

        rapor.append(f"✓ Çıktı: {cikti}")
        url = _comfy_view_url(cikti)
        return (cikti, url, "\n".join(rapor))


# ===========================================================================
# 6) FikoYT_MVD_AspectRatioBatch — Bir kaynaktan 3-4 platform formatı (V2)
# ===========================================================================

# Yaygın platform aspect ratio'ları + çözünürlük preset'leri
_ASPECT_PRESETS = {
    "16:9":  {"w": 1920, "h": 1080, "label": "YouTube/Landscape"},
    "9:16":  {"w": 1080, "h": 1920, "label": "Stories/Reels/Shorts"},
    "1:1":   {"w": 1080, "h": 1080, "label": "Instagram Post"},
    "4:5":   {"w": 1080, "h": 1350, "label": "Instagram Portrait"},
    "21:9":  {"w": 2560, "h": 1080, "label": "Cinemascope"},
    "4:3":   {"w": 1440, "h": 1080, "label": "Classic TV"},
}


class FikoYT_MVD_AspectRatioBatch:
    """Tek bir video kaynağından farklı aspect ratio'larda batch çıktı.
    Her hedef için: crop-center + scale (smart fill, deformasyon yok).
    """

    CATEGORY = "FikoYT/MVD/Video"
    FUNCTION = "batch"
    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("outputs_json", "report")
    OUTPUT_NODE = True

    @classmethod
    def INPUT_TYPES(cls):
        ratios = list(_ASPECT_PRESETS.keys())
        video_files = _list_video_files()
        return {
            "required": {
                "video_file": (video_files, {
                    "tooltip": "input/ veya output/ klasorundeki video. Manuel path icin "
                               "asagi 'custom_video_path' alanini kullan."
                }),
                "ratios_csv": ("STRING", {"default": "16:9,9:16,1:1",
                    "tooltip": "Virgülle ayrılmış: 16:9,9:16,1:1,4:5,21:9,4:3"}),
            },
            "optional": {
                "custom_video_path": ("STRING", {"default": "", "multiline": False,
                    "placeholder": "Manuel tam yol — varsa dropdown override edilir"}),
                "fit_mode": (["crop", "pad"], {"default": "crop",
                    "tooltip": "crop: merkez kırp (kenarlar kaybolur). "
                               "pad: kenarlara siyah bant (içerik tam görünür)."}),
                "output_suffix_template": ("STRING", {"default": "_{ratio}",
                    "tooltip": "{ratio} → 16x9, 9x16 vb. ile değiştirilir"}),
                "bitrate_mbps": ("INT", {"default": 8, "min": 1, "max": 50}),
                "available_ratios_info": (ratios, {"default": ratios[0]}),
            },
        }

    def batch(self, video_file: str, ratios_csv: str,
              custom_video_path: str = "",
              fit_mode: str = "crop",
              output_suffix_template: str = "_{ratio}",
              bitrate_mbps: int = 8,
              available_ratios_info: str = "16:9"):
        rapor: List[str] = []
        outputs: List[dict] = []

        video_path = _resolve_video_path(video_file, custom_video_path)

        if _find_ffmpeg() is None:
            return ("[]", "HATA: ffmpeg bulunamadı.")
        if not video_path or not os.path.isfile(video_path):
            return ("[]", f"HATA: Video yok: {video_path}\n"
                          f"  COMBO: {video_file!r}\n"
                          f"  custom_video_path: {custom_video_path!r}")

        # Ratio listesi parse
        wanted = [r.strip() for r in (ratios_csv or "").split(",") if r.strip()]
        if not wanted:
            return ("[]", "HATA: ratios_csv boş")

        kok, _ext = os.path.splitext(video_path)

        for ratio in wanted:
            if ratio not in _ASPECT_PRESETS:
                rapor.append(f"⚠ Bilinmeyen ratio: {ratio}, atlandı")
                continue
            preset = _ASPECT_PRESETS[ratio]
            tw, th = preset["w"], preset["h"]
            slug = ratio.replace(":", "x")
            suffix = output_suffix_template.replace("{ratio}", slug)
            cikti = f"{kok}{suffix}.mp4"

            if fit_mode == "crop":
                # Önce iç-içe scale, sonra crop-center
                # Hedef ratio'ya uygun olacak şekilde büyüt, sonra fazlasını kırp
                vf = (f"scale={tw}:{th}:force_original_aspect_ratio=increase,"
                      f"crop={tw}:{th}")
            else:  # pad
                vf = (f"scale={tw}:{th}:force_original_aspect_ratio=decrease,"
                      f"pad={tw}:{th}:(ow-iw)/2:(oh-ih)/2:black")

            args = ["-y", "-i", video_path, "-vf", vf,
                    "-c:v", "libx264", "-crf", "18", "-preset", "medium",
                    "-b:v", f"{bitrate_mbps}M", "-pix_fmt", "yuv420p",
                    "-c:a", "copy", cikti]
            try:
                kod, err = _run_ffmpeg(args, timeout=600)
                if kod == 0 and os.path.isfile(cikti):
                    outputs.append({
                        "ratio": ratio, "path": cikti, "url": _comfy_view_url(cikti),
                        "width": tw, "height": th, "label": preset["label"],
                    })
                    rapor.append(f"✓ {ratio} ({preset['label']}) → {os.path.basename(cikti)}")
                else:
                    rapor.append(f"✗ {ratio} başarısız: {err[-200:] if err else 'unknown'}")
            except Exception as e:
                rapor.append(f"✗ {ratio} exception: {e}")

        if not outputs:
            return ("[]", "HATA: hiçbir varyant üretilemedi\n" + "\n".join(rapor))

        rapor.insert(0, f"✓ {len(outputs)}/{len(wanted)} aspect ratio başarıyla üretildi")
        return (json.dumps(outputs, ensure_ascii=False, indent=2), "\n".join(rapor))


# ---------------------------------------------------------------------------
# Node kayıtları
# ---------------------------------------------------------------------------

NODE_CLASS_MAPPINGS = {
    "FikoYT_MVD_VideoCombiner": FikoYT_MVD_VideoCombiner,
    "FikoYT_MVD_VideoUpscaler": FikoYT_MVD_VideoUpscaler,
    "FikoYT_MVD_VideoBatchManager": FikoYT_MVD_VideoBatchManager,
    "FikoYT_MVD_LyricBurner": FikoYT_MVD_LyricBurner,
    "FikoYT_MVD_AspectRatioBatch": FikoYT_MVD_AspectRatioBatch,
    "FikoYT_MVD_ColorGrader": FikoYT_MVD_ColorGrader,
    "FikoYT_MVD_BeatSnapTrimmer": FikoYT_MVD_BeatSnapTrimmer,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "FikoYT_MVD_VideoCombiner": "🎞 FikoYT | Video Combiner",
    "FikoYT_MVD_VideoUpscaler": "📈 FikoYT | Video Upscaler",
    "FikoYT_MVD_VideoBatchManager": "📂 FikoYT | Video Batch Manager",
    "FikoYT_MVD_LyricBurner": "📝 FikoYT | Lyric Burner (Karaoke)",
    "FikoYT_MVD_AspectRatioBatch": "📐 FikoYT | Aspect Ratio Batch (Multi-Platform)",
    "FikoYT_MVD_ColorGrader": "🎨 FikoYT | Color Grader (LUT + Presets)",
    "FikoYT_MVD_BeatSnapTrimmer": "🥁 FikoYT | Beat Snap Trimmer (Müziğe Cut Snap)",
}
