"""
Visual domain — image generation node'ları + visual reference (IPAdapter/PuLID/
ControlNet) helpers, model availability detection, reference chain.

Mevcut Node sınıfları (_legacy.py'dan re-export):
  - FikoYT_MVD_SegmentImageGen
  - FikoYT_MVD_HiResFix
  - FikoYT_MVD_AlbumCoverGen
  - FikoYT_MVD_ImageAspectBatch

Yeni Node sınıfları (Sprint 2 — W13):
  - FikoYT_MVD_PoseExtractor — DWPose/OpenPose ile keypoint extract
  - FikoYT_MVD_ControlNetUnion — Tek slot'ta pose+canny+depth+softedge

Yeni Node sınıfları (Sprint 3 — W14):
  - FikoYT_MVD_RegionalMultiChar — 2/3/4 karakter regional prompting

Yeni Node sınıfları (Sprint 5 — W16):
  - FikoYT_MVD_LightningPreview — SDXL Lightning 4-step preview
"""
from ._legacy import (
    FikoYT_MVD_SegmentImageGen,
    FikoYT_MVD_HiResFix,
    FikoYT_MVD_AlbumCoverGen,
    FikoYT_MVD_ImageAspectBatch,
    _empty_image,
    _get_model_search_paths,
    _try_apply_flux_guidance,
    _try_apply_pulid,
    _load_input_image,
    _try_apply_ipadapter,
    _try_run_preprocessor,
    _try_apply_controlnet,
    _classify_ref_mode,
    _apply_references_chain,
    _get_missing_flux_models,
    _download_one_sync,
    _comfy_class_available,
    _find_controlnet_file,
    _visual_ref_capabilities,
    _detect_pipeline_nodes,
    _next_free_id,
    _inject_visual_references,
    CATEGORY,
)


# =============================================================================
# Sprint 2 (W13): ControlNet Union helper
# =============================================================================
def _try_apply_controlnet_union(positive_cond, negative_cond, ref_image,
                                modes=("pose",), weights=(0.6,), vae=None,
                                union_model_hint=None):
    """ControlNet Union — tek slot'ta birden fazla kontrolü birleştir.

    SDXL: xinsir/controlnet-union-sdxl-1.0
    Flux: Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro veya InstantX variant

    modes: tuple of "pose"|"canny"|"depth"|"softedge"
    weights: aynı uzunlukta tuple veya tek float (replicate edilir)

    Her mode için preprocessor sonucu hesaplanıp ControlNet Union'a sırayla
    uygulanır. Union model bulunamazsa standard ControlNet'e fallback.

    Dönüş: (new_positive, new_negative, status_str)
    """
    if ref_image is None:
        return positive_cond, negative_cond, "ControlNet Union: ref yok"
    if not modes:
        return positive_cond, negative_cond, "ControlNet Union: mod yok"

    # weights normalize
    if isinstance(weights, (int, float)):
        weights = [float(weights)] * len(modes)
    weights = list(weights)
    while len(weights) < len(modes):
        weights.append(weights[-1] if weights else 0.6)

    try:
        from nodes import NODE_CLASS_MAPPINGS as _NCM
        import folder_paths
        cn_loader = _NCM.get("ControlNetLoader")
        cn_apply = (_NCM.get("ControlNetApplyAdvanced")
                    or _NCM.get("ControlNetApplySD3")
                    or _NCM.get("ControlNetApply"))
        if not cn_loader or not cn_apply:
            return positive_cond, negative_cond, "ControlNet node'ları yok"

        # Union model bul
        try:
            cn_files = folder_paths.get_filename_list("controlnet")
        except Exception:
            cn_files = []
        union_file = None
        priority_terms = ("union",)
        flux_terms = ("flux", "instantx", "shakker", "xlabs")
        candidates = [f for f in cn_files
                      if any(t in f.lower() for t in priority_terms)]
        if union_model_hint:
            for f in candidates:
                if union_model_hint.lower() in f.lower():
                    union_file = f
                    break
        if not union_file:
            # Flux Union > SDXL Union > diğer Union
            flux_union = [f for f in candidates
                          if any(t in f.lower() for t in flux_terms)]
            union_file = flux_union[0] if flux_union else (candidates[0] if candidates else None)

        if not union_file:
            # Standart ControlNet fallback — mode başına ayrı CN
            status_parts = ["ControlNet Union modeli yok, standard ControlNet'e fallback"]
            cur_pos, cur_neg = positive_cond, negative_cond
            for m, w in zip(modes, weights):
                cur_pos, cur_neg, msg = _try_apply_controlnet(
                    cur_pos, cur_neg, ref_image,
                    f"controlnet_{m}" if m in ("canny", "depth", "normal") else m,
                    weight=w, vae=vae)
                status_parts.append(f"[{m}] {msg}")
            return cur_pos, cur_neg, " | ".join(status_parts)

        # Union model var — her mod için preprocess + apply
        try:
            cn_model = cn_loader().load_controlnet(union_file)[0]
        except Exception as e:
            return positive_cond, negative_cond, f"ControlNet Union load hata: {e}"

        cur_pos, cur_neg = positive_cond, negative_cond
        status_parts = []
        for m, w in zip(modes, weights):
            if w <= 0:
                continue
            prep_mode = m if m in ("openpose", "dwpose") else f"controlnet_{m}"
            ctrl_image, prep_msg = _try_run_preprocessor(ref_image, prep_mode)
            if ctrl_image is None:
                status_parts.append(f"[{m}] preprocess fail")
                continue
            try:
                result = cn_apply().apply_controlnet(
                    cur_pos, cur_neg, cn_model, ctrl_image,
                    strength=w, start_percent=0.0, end_percent=1.0, vae=vae
                )
            except TypeError:
                result = cn_apply().apply_controlnet(
                    cur_pos, cur_neg, cn_model, ctrl_image,
                    strength=w, start_percent=0.0, end_percent=1.0
                )
            cur_pos, cur_neg = result[0], result[1]
            status_parts.append(f"✓[{m},w={w}]")
        return cur_pos, cur_neg, (
            f"✓ ControlNet Union ({union_file}): " + " ".join(status_parts))
    except Exception as e:
        import traceback
        print(traceback.format_exc()[-500:])
        return positive_cond, negative_cond, f"ControlNet Union hata: {e}"


# =============================================================================
# Sprint 2 (W13): FikoYT_MVD_PoseExtractor node
# =============================================================================
class FikoYT_MVD_PoseExtractor:
    """Bir giriş resminden pose keypoint çıkar (DWPose öncelikli, fallback OpenPose).

    Çıktı: pose_image (skeleton overlay) + json string (keypoints) + status.
    comfyui_controlnet_aux kurulu değilse raw image + boş json + uyarı döner.
    """
    CATEGORY = CATEGORY
    FUNCTION = "extract"
    RETURN_TYPES = ("IMAGE", "STRING", "STRING")
    RETURN_NAMES = ("pose_image", "keypoints_json", "status")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "preferred": (["dwpose", "openpose"], {"default": "dwpose"}),
                "resolution": ("INT", {"default": 512, "min": 256, "max": 2048, "step": 64}),
            }
        }

    def extract(self, image, preferred="dwpose", resolution=512):
        try:
            from nodes import NODE_CLASS_MAPPINGS as _NCM
            order = ["DWPreprocessor", "OpenposePreprocessor"]
            if preferred == "openpose":
                order = ["OpenposePreprocessor", "DWPreprocessor"]

            pose_image = image
            keypoints = "{}"
            status = "Preprocessor yok"
            for node_name in order:
                cls = _NCM.get(node_name)
                if not cls:
                    continue
                try:
                    node = cls()
                    method = None
                    for m in ("execute", "estimate_pose", "process"):
                        if hasattr(node, m):
                            method = getattr(node, m)
                            break
                    if method is None:
                        continue
                    # DWPose: image=, detect_hand=, detect_body=, detect_face=, resolution=
                    try:
                        result = method(image=image, detect_hand="enable",
                                        detect_body="enable", detect_face="enable",
                                        resolution=resolution)
                    except TypeError:
                        result = method(image=image, resolution=resolution)
                    # Sprint 38: 3 farklı return format desteği
                    # 1. (tensor, json) — eski tuple
                    # 2. tensor — tek
                    # 3. {"ui": {...}, "result": (tensor, json)} — ComfyUI 0.3+ dict
                    import json
                    inner = result
                    if isinstance(result, dict):
                        # ComfyUI yeni format
                        if "result" in result:
                            inner = result["result"]
                        elif "ui" in result and "images" in result.get("ui", {}):
                            # Sadece ui dönmüş, tensor yok — orijinal image kullan
                            inner = (image,)
                        else:
                            inner = (image,)
                    if isinstance(inner, (tuple, list)) and len(inner) > 0:
                        pose_image = inner[0]
                        if len(inner) > 1:
                            try:
                                keypoints = json.dumps(inner[1]) if not isinstance(
                                    inner[1], str) else inner[1]
                            except Exception:
                                keypoints = "{}"
                    else:
                        pose_image = inner if inner is not None else image
                    # Sanity: pose_image bir torch tensor olmalı
                    if not hasattr(pose_image, "shape"):
                        print(f"[FikoYT PoseExtractor] {node_name} returned non-tensor: "
                              f"{type(pose_image).__name__}, falling back to input")
                        pose_image = image
                    status = f"✓ {node_name} ({resolution}px)"
                    break
                except Exception as e:
                    print(f"[FikoYT] PoseExtractor {node_name} hata: {e}")
                    continue
            return (pose_image, keypoints, status)
        except Exception as e:
            return (image, "{}", f"PoseExtractor hata: {e}")


# =============================================================================
# Sprint 2 (W13): FikoYT_MVD_ControlNetUnion node
# =============================================================================
class FikoYT_MVD_ControlNetUnion:
    """ControlNet Union node wrapper — tek slot'ta pose+canny+depth+softedge.

    Dahili olarak _try_apply_controlnet_union helper'ını çağırır. Workflow JSON'da
    standalone kullanılabilir, MVD render pipeline'ı da bu node'u çağırabilir.
    """
    CATEGORY = CATEGORY
    FUNCTION = "apply"
    RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "STRING")
    RETURN_NAMES = ("positive", "negative", "status")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "positive": ("CONDITIONING",),
                "negative": ("CONDITIONING",),
                "control_image": ("IMAGE",),
                "enable_pose":      ("BOOLEAN", {"default": True}),
                "enable_canny":     ("BOOLEAN", {"default": False}),
                "enable_depth":     ("BOOLEAN", {"default": False}),
                "enable_softedge":  ("BOOLEAN", {"default": False}),
                "weight_pose":      ("FLOAT", {"default": 0.6, "min": 0.0, "max": 2.0, "step": 0.05}),
                "weight_canny":     ("FLOAT", {"default": 0.4, "min": 0.0, "max": 2.0, "step": 0.05}),
                "weight_depth":     ("FLOAT", {"default": 0.4, "min": 0.0, "max": 2.0, "step": 0.05}),
                "weight_softedge":  ("FLOAT", {"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.05}),
            },
            "optional": {
                "vae": ("VAE",),
                "union_model_hint": ("STRING", {"default": "", "multiline": False}),
            }
        }

    def apply(self, positive, negative, control_image,
              enable_pose=True, enable_canny=False, enable_depth=False, enable_softedge=False,
              weight_pose=0.6, weight_canny=0.4, weight_depth=0.4, weight_softedge=0.3,
              vae=None, union_model_hint=""):
        modes, weights = [], []
        if enable_pose:
            modes.append("dwpose"); weights.append(weight_pose)
        if enable_canny:
            modes.append("canny"); weights.append(weight_canny)
        if enable_depth:
            modes.append("depth"); weights.append(weight_depth)
        if enable_softedge:
            modes.append("softedge"); weights.append(weight_softedge)
        if not modes:
            return (positive, negative, "ControlNet Union: hiçbir mod aktif değil")
        new_pos, new_neg, status = _try_apply_controlnet_union(
            positive, negative, control_image,
            modes=modes, weights=weights, vae=vae,
            union_model_hint=union_model_hint or None
        )
        return (new_pos, new_neg, status)


# =============================================================================
# Sprint 3 (W14): FikoYT_MVD_RegionalMultiChar — 2/3/4 karakter regional
# =============================================================================
class FikoYT_MVD_RegionalMultiChar:
    """Multi-character regional prompting wrapper.

    Tek code path'te 2/3/4 karakter destekler. characters_json bir JSON string
    bekler: [{cid, lora_strength, mask_polygon: [[x,y],...], prompt_override?,
    identity_method?}, ...]. base_prompt arka plan + ortam için.

    Backend implementasyonu Sprint 3'te tam doluyor (workflow JSON template'leri
    flux_regional_3char.json/4char.json). Bu node şu an helper stub'ı.
    """
    CATEGORY = CATEGORY
    FUNCTION = "build"
    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("plan_json", "status")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "characters_json": ("STRING", {
                    "default": "[]", "multiline": True,
                    "placeholder": '[{"cid":"singer","lora_strength":0.8,"mask_polygon":[[0,0],[256,0],[256,512],[0,512]]}]'
                }),
                "base_prompt": ("STRING", {"default": "cinematic film still", "multiline": True}),
                "width": ("INT", {"default": 1024, "min": 256, "max": 4096, "step": 64}),
                "height": ("INT", {"default": 1024, "min": 256, "max": 4096, "step": 64}),
            }
        }

    def build(self, characters_json, base_prompt, width, height):
        import json as _json
        try:
            chars = _json.loads(characters_json or "[]")
        except Exception as e:
            return ("{}", f"characters_json parse hata: {e}")
        if not isinstance(chars, list):
            return ("{}", "characters_json bir liste olmalı")
        n = len(chars)
        if n < 2 or n > 4:
            return ("{}", f"2-4 karakter destekleniyor, {n} verildi")
        plan = {
            "char_count": n,
            "characters": chars,
            "base_prompt": base_prompt,
            "width": width, "height": height,
            "workflow_template": f"flux_regional_{n}char.json",
        }
        return (_json.dumps(plan, ensure_ascii=False, indent=2),
                f"✓ Regional plan: {n} karakter, template={plan['workflow_template']}")


# =============================================================================
# Sprint 5 (W16): FikoYT_MVD_LightningPreview — SDXL Lightning 4-step
# =============================================================================
class FikoYT_MVD_LightningPreview:
    """SDXL Lightning 4-step preview render — hızlı iterasyon.

    Lightning LoRA modelle birleştirilir (sampler="euler", scheduler="sgm_uniform",
    steps=4, cfg=1.0). Flux modellerinde no-op (uyarı döner).

    Workflow JSON adapter olarak da kullanılabilir.
    """
    CATEGORY = CATEGORY
    FUNCTION = "preview"
    RETURN_TYPES = ("MODEL", "STRING")
    RETURN_NAMES = ("model", "status")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "model": ("MODEL",),
                "lightning_lora": ("STRING", {
                    "default": "sdxl_lightning_4step_lora.safetensors",
                    "multiline": False,
                    "placeholder": "models/loras/ altındaki Lightning LoRA dosya adı"
                }),
                "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05}),
            }
        }

    def preview(self, model, lightning_lora, strength=1.0):
        try:
            from nodes import NODE_CLASS_MAPPINGS as _NCM
            cls = _NCM.get("LoraLoaderModelOnly") or _NCM.get("LoraLoader")
            if not cls:
                return (model, "Lightning: LoraLoader node yok")
            try:
                # LoraLoaderModelOnly: (model, lora_name, strength_model)
                patched = cls().load_lora_model_only(model, lightning_lora, strength)[0]
            except Exception:
                # LoraLoader: (model, clip, lora_name, strength_model, strength_clip)
                # clip yoksa skip
                return (model, "Lightning: LoraLoaderModelOnly yok, manuel kur")
            return (patched, f"✓ Lightning LoRA uygulandı ({lightning_lora}, s={strength}) — "
                            f"4 step euler/sgm_uniform önerilir")
        except Exception as e:
            return (model, f"Lightning hata: {e}")


# =============================================================================
# Sprint 6 (Production Pipeline): Profesyonel post-grade + Master render + Aspect preset
# =============================================================================

# Müzik klibi için endüstri-standardı aspect ratio preset'leri
# Her preset → (width, height, label, platform)
MUSIC_VIDEO_ASPECT_PRESETS = {
    "tiktok_reels":  (1080, 1920, "TikTok / Reels / Shorts (9:16)", "tiktok"),
    "instagram":     (1080, 1080, "Instagram Feed (1:1)", "instagram"),
    "instagram_vertical": (1080, 1350, "Instagram Vertical (4:5)", "instagram"),
    "youtube":       (1920, 1080, "YouTube / Vimeo (16:9)", "youtube"),
    "youtube_4k":    (3840, 2160, "YouTube 4K (16:9)", "youtube"),
    "cinematic":     (2048, 858,  "Sinematik (2.39:1 letterbox)", "film"),
    "ultrawide":     (2560, 1080, "Ultrawide (21:9)", "monitor"),
}


def _apply_lut_3d(image_tensor, lut_name="cinematic", strength=1.0):
    """Basit 3D LUT yaklaşımı: birkaç preset için kanal-tabanlı ton eğrisi.

    Profesyonel kullanım: .cube LUT dosyalarını models/luts/ altına koyup
    color-matcher lib ile aralarda interpolation yapılır. Burada hafif/preset.

    lut_name: "cinematic" | "vintage" | "vibrant" | "teal_orange" | "noir" | "warm" | "cool"
    """
    try:
        import torch
        import numpy as np
    except ImportError:
        return image_tensor, "torch/numpy yok"

    if strength <= 0:
        return image_tensor, "LUT: strength=0, atlandı"

    out = image_tensor.clone()
    # LUT preset'leri — (R_curve, G_curve, B_curve) shadow/midtone/highlight şiddetleri
    presets = {
        # cinematic: kontrast + sıcak shadow + soğuk highlight
        "cinematic":    {"shadow": (0.95, 0.97, 1.08), "midtone": (1.02, 1.0, 1.0), "highlight": (1.05, 1.02, 0.92), "saturation": 0.92, "contrast": 1.08},
        # vintage: düşük kontrast, sarı/yeşil tonlama, lifted blacks
        "vintage":      {"shadow": (1.08, 1.02, 0.88), "midtone": (1.05, 1.02, 0.95), "highlight": (1.02, 1.0, 0.85), "saturation": 0.78, "contrast": 0.92, "lift": 0.05},
        # vibrant: doygunluk + canlılık (pop music)
        "vibrant":      {"shadow": (1.0, 1.0, 1.0), "midtone": (1.0, 1.0, 1.0), "highlight": (1.0, 1.0, 1.0), "saturation": 1.3, "contrast": 1.12},
        # teal & orange: Hollywood blockbuster
        "teal_orange":  {"shadow": (0.92, 1.05, 1.18), "midtone": (1.02, 1.0, 0.95), "highlight": (1.15, 1.05, 0.85), "saturation": 1.05, "contrast": 1.1},
        # noir: yüksek kontrast siyah-beyaz tilt
        "noir":         {"shadow": (1.0, 1.0, 1.0), "midtone": (1.0, 1.0, 1.0), "highlight": (1.0, 1.0, 1.0), "saturation": 0.15, "contrast": 1.25},
        # warm: sıcak ton (akustik/duygusal)
        "warm":         {"shadow": (1.08, 1.02, 0.92), "midtone": (1.05, 1.0, 0.95), "highlight": (1.08, 1.02, 0.92), "saturation": 1.05, "contrast": 1.02},
        # cool: soğuk ton (elektronik/melankolik)
        "cool":         {"shadow": (0.92, 0.98, 1.08), "midtone": (0.95, 1.0, 1.05), "highlight": (0.92, 0.98, 1.08), "saturation": 1.0, "contrast": 1.05},
    }
    p = presets.get(lut_name, presets["cinematic"])

    for b in range(out.shape[0]):
        img = out[b]  # [H,W,3]
        # Luminance hesapla (mask için)
        lum = 0.299 * img[..., 0] + 0.587 * img[..., 1] + 0.114 * img[..., 2]
        shadow_m = torch.clamp(1.0 - lum * 2.5, 0, 1)
        highlight_m = torch.clamp((lum - 0.5) * 2.0, 0, 1)
        midtone_m = 1.0 - shadow_m - highlight_m
        midtone_m = torch.clamp(midtone_m, 0, 1)

        for c in range(3):
            channel = img[..., c]
            adj = (channel * (shadow_m * p["shadow"][c] +
                              midtone_m * p["midtone"][c] +
                              highlight_m * p["highlight"][c]))
            # Lift (vintage için raised blacks)
            lift = p.get("lift", 0.0)
            if lift > 0:
                adj = adj * (1 - lift) + lift
            # Contrast (0.5 etrafında ölçekle)
            contrast = p.get("contrast", 1.0)
            if contrast != 1.0:
                adj = (adj - 0.5) * contrast + 0.5
            img[..., c] = adj

        # Saturation (HSV-benzeri lineer interpolation luma'ya)
        sat = p.get("saturation", 1.0)
        if sat != 1.0:
            lum = 0.299 * img[..., 0] + 0.587 * img[..., 1] + 0.114 * img[..., 2]
            for c in range(3):
                img[..., c] = lum + (img[..., c] - lum) * sat

        # Strength blend (original ile karıştır)
        if strength < 1.0:
            img = image_tensor[b] * (1 - strength) + img * strength
        out[b] = torch.clamp(img, 0, 1)

    return out, f"✓ LUT '{lut_name}' uygulandı (s={strength})"


def _apply_vignette(image_tensor, intensity=0.3, falloff=0.6):
    """Köşelerde karartma — sinematik odak."""
    try:
        import torch
    except ImportError:
        return image_tensor, "torch yok"
    if intensity <= 0:
        return image_tensor, "Vignette: kapalı"
    out = image_tensor.clone()
    _, h, w, _ = out.shape
    yy, xx = torch.meshgrid(torch.linspace(-1, 1, h), torch.linspace(-1, 1, w), indexing="ij")
    dist = torch.sqrt(xx ** 2 + yy ** 2)
    mask = 1.0 - torch.clamp((dist - falloff) / (1.4 - falloff), 0, 1) * intensity
    mask = mask.unsqueeze(-1)  # [H,W,1]
    for b in range(out.shape[0]):
        out[b] = out[b] * mask
    return torch.clamp(out, 0, 1), f"✓ Vignette (i={intensity}, f={falloff})"


def _apply_chromatic_aberration(image_tensor, intensity=2):
    """RGB kanal kaydırma — analog film hissi."""
    try:
        import torch
    except ImportError:
        return image_tensor, "torch yok"
    if intensity <= 0:
        return image_tensor, "CA: kapalı"
    out = image_tensor.clone()
    sh = int(round(intensity))
    if sh <= 0:
        return out, "CA: shift=0"
    for b in range(out.shape[0]):
        # Red kanalı sağa, Blue kanalı sola kaydır
        r = out[b, :, :, 0]
        bch = out[b, :, :, 2]
        r_shifted = torch.roll(r, shifts=sh, dims=1)
        b_shifted = torch.roll(bch, shifts=-sh, dims=1)
        out[b, :, :, 0] = r_shifted
        out[b, :, :, 2] = b_shifted
    return torch.clamp(out, 0, 1), f"✓ Chromatic aberration (shift={sh}px)"


def _apply_letterbox(image_tensor, target_ratio=2.39):
    """Üst+alt siyah bar ekle — sinematik aspect."""
    try:
        import torch
    except ImportError:
        return image_tensor, "torch yok"
    out = image_tensor.clone()
    _, h, w, _ = out.shape
    current_ratio = w / h
    if current_ratio >= target_ratio:
        return out, f"Letterbox gerek yok (ratio={current_ratio:.2f})"
    # Hedef yüksekliği hesapla
    new_h = int(round(w / target_ratio))
    bar_h = (h - new_h) // 2
    if bar_h <= 0:
        return out, "Letterbox: bar yok"
    for b in range(out.shape[0]):
        out[b, :bar_h, :, :] = 0.0
        out[b, h - bar_h:, :, :] = 0.0
    return out, f"✓ Letterbox {target_ratio:.2f}:1 (bar={bar_h}px)"


class FikoYT_MVD_ProfessionalGrade:
    """Sprint 6: Profesyonel müzik klibi post-prodüksiyon paketi.

    CinematicPost'u (film_grain + sharpen + color_match) tamamlar:
      - 7 LUT preset (cinematic, vintage, vibrant, teal_orange, noir, warm, cool)
      - Vignette (intensity + falloff)
      - Chromatic aberration (analog film)
      - Letterbox (sinematik aspect, 2.39/2.35)

    Tüm efektler sırayla uygulanır: LUT → CA → Vignette → Letterbox.
    Pipeline'da CinematicPost'tan SONRA bağlanmalı (grain/sharpen → grade).
    """
    CATEGORY = CATEGORY
    FUNCTION = "grade"
    RETURN_TYPES = ("IMAGE", "STRING")
    RETURN_NAMES = ("image", "report")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "lut_preset": (["none", "cinematic", "vintage", "vibrant",
                                "teal_orange", "noir", "warm", "cool"],
                               {"default": "cinematic"}),
                "lut_strength": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.05}),
                "vignette_intensity": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.05,
                                                  "tooltip": "0=off, 0.2-0.4 hafif, 0.6+ ağır"}),
                "vignette_falloff": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.05}),
                "chromatic_aberration": ("INT", {"default": 0, "min": 0, "max": 8, "step": 1,
                                                  "tooltip": "RGB kanal kaydırma piksel — 1-2 hafif analog, 5+ heavy distortion"}),
                "letterbox_ratio": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 3.0, "step": 0.01,
                                              "tooltip": "0=off, 2.35 klasik sinematik, 2.39 modern, 2.76 IMAX"}),
            }
        }

    def grade(self, image, lut_preset, lut_strength, vignette_intensity,
              vignette_falloff, chromatic_aberration, letterbox_ratio):
        notes = []
        out = image
        if lut_preset != "none":
            out, msg = _apply_lut_3d(out, lut_preset, lut_strength)
            notes.append(msg)
        if chromatic_aberration > 0:
            out, msg = _apply_chromatic_aberration(out, chromatic_aberration)
            notes.append(msg)
        if vignette_intensity > 0:
            out, msg = _apply_vignette(out, vignette_intensity, vignette_falloff)
            notes.append(msg)
        if letterbox_ratio > 0:
            out, msg = _apply_letterbox(out, letterbox_ratio)
            notes.append(msg)
        return (out, "\n".join(notes) if notes else "ProfessionalGrade: tüm efektler kapalı")


class FikoYT_MVD_AspectPreset:
    """Müzik klibi platform preset'leri — TikTok/YouTube/Insta için tek-tıkla boyutlandırma.

    Çıktı: width, height, label, ratio_str. Wizard veya EmptyLatentImage'a
    bağlanır. Strategy: contain (siyah padding) | cover (crop) | stretch.
    """
    CATEGORY = CATEGORY
    FUNCTION = "resolve"
    RETURN_TYPES = ("INT", "INT", "STRING", "STRING")
    RETURN_NAMES = ("width", "height", "label", "ratio")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "preset": (list(MUSIC_VIDEO_ASPECT_PRESETS.keys()),
                           {"default": "youtube"}),
            }
        }

    def resolve(self, preset):
        w, h, label, _platform = MUSIC_VIDEO_ASPECT_PRESETS.get(
            preset, MUSIC_VIDEO_ASPECT_PRESETS["youtube"])
        from math import gcd
        g = gcd(w, h)
        ratio_str = f"{w // g}:{h // g}"
        return (w, h, label, ratio_str)


class FikoYT_MVD_MasterRender:
    """FFmpeg ile tüm segment video'larını + audio'yu birleştir → master MP4.

    Input: segment video path listesi (JSON string veya project dizini)
    Output: master video path + report.

    Aspect ratio normalize (target_w/h ile pad veya crop), audio track ekle,
    chapter markers (segment başlangıçları), fade-in/out, watermark.
    """
    CATEGORY = CATEGORY
    FUNCTION = "render"
    RETURN_TYPES = ("STRING", "STRING")
    RETURN_NAMES = ("output_path", "report")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "project_dir": ("STRING", {"default": "", "multiline": False,
                    "placeholder": "ComfyUI/output/fikoyt_mvd/<ProjeName>_<ts>"}),
                "audio_path": ("STRING", {"default": "", "multiline": False,
                    "placeholder": "Master ses dosyası (mp3/wav)"}),
                "video_subdir": ("STRING", {"default": "videos", "multiline": False}),
                "output_filename": ("STRING", {"default": "master.mp4", "multiline": False}),
                "target_width": ("INT", {"default": 1920, "min": 256, "max": 7680, "step": 8}),
                "target_height": ("INT", {"default": 1080, "min": 256, "max": 4320, "step": 8}),
                "crf": ("INT", {"default": 18, "min": 0, "max": 51, "step": 1,
                                "tooltip": "0=lossless, 18=görsel-lossless, 23=default, 28=küçük"}),
                "preset": (["ultrafast", "superfast", "veryfast", "faster", "fast",
                            "medium", "slow", "slower", "veryslow"], {"default": "medium"}),
                "fade_in": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.1}),
                "fade_out": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.1}),
            }
        }

    def render(self, project_dir, audio_path, video_subdir, output_filename,
               target_width, target_height, crf, preset, fade_in, fade_out):
        from ._legacy import _ffmpeg_bin
        import os
        import subprocess
        import tempfile

        ffmpeg = _ffmpeg_bin()
        if not ffmpeg:
            return ("", "❌ FFmpeg bulunamadı (PATH veya imageio_ffmpeg)")
        if not os.path.isdir(project_dir):
            return ("", f"❌ project_dir yok: {project_dir}")

        vdir = os.path.join(project_dir, video_subdir)
        if not os.path.isdir(vdir):
            return ("", f"❌ video dir yok: {vdir}")

        # Segment video'larını topla (sorted)
        segs = sorted([os.path.join(vdir, f) for f in os.listdir(vdir)
                       if f.lower().endswith((".mp4", ".mov", ".mkv", ".webm"))])
        if not segs:
            return ("", f"❌ {vdir} altında video yok")

        out_path = os.path.join(project_dir, output_filename)

        # FFmpeg concat list dosyası
        with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False,
                                          encoding="utf-8") as f:
            for v in segs:
                # Escape single quotes for FFmpeg concat
                v_esc = v.replace("\\", "/").replace("'", r"'\''")
                f.write(f"file '{v_esc}'\n")
            list_file = f.name

        notes = [f"✓ {len(segs)} segment video bulundu"]
        try:
            # 1) Önce segment'leri concat et (yeniden encode etmeden)
            tmp_concat = os.path.join(project_dir, ".tmp_concat.mp4")
            concat_cmd = [
                ffmpeg, "-y", "-f", "concat", "-safe", "0", "-i", list_file,
                "-c", "copy", tmp_concat
            ]
            r = subprocess.run(concat_cmd, capture_output=True, text=True)
            if r.returncode != 0:
                # Re-encode fallback (segment'ler farklı codec'lerde olabilir)
                notes.append("⚠ copy concat fail, re-encode'e geçildi")
                concat_cmd = [
                    ffmpeg, "-y", "-f", "concat", "-safe", "0", "-i", list_file,
                    "-vf", f"scale={target_width}:{target_height}:force_original_aspect_ratio=decrease,"
                           f"pad={target_width}:{target_height}:(ow-iw)/2:(oh-ih)/2:black",
                    "-c:v", "libx264", "-preset", preset, "-crf", str(crf),
                    "-pix_fmt", "yuv420p",
                    tmp_concat
                ]
                r = subprocess.run(concat_cmd, capture_output=True, text=True)
                if r.returncode != 0:
                    return ("", f"❌ concat fail: {r.stderr[-500:]}")

            # 2) Audio + final pass (scale + pad + fade)
            video_filter = (
                f"scale={target_width}:{target_height}:force_original_aspect_ratio=decrease,"
                f"pad={target_width}:{target_height}:(ow-iw)/2:(oh-ih)/2:black"
            )
            if fade_in > 0:
                video_filter += f",fade=t=in:st=0:d={fade_in}"
            if fade_out > 0:
                # Toplam süreyi öğrenmek için ffprobe
                try:
                    probe = subprocess.run(
                        [ffmpeg.replace("ffmpeg", "ffprobe"), "-v", "error",
                         "-show_entries", "format=duration", "-of",
                         "default=noprint_wrappers=1:nokey=1", tmp_concat],
                        capture_output=True, text=True)
                    duration = float(probe.stdout.strip())
                    video_filter += f",fade=t=out:st={duration - fade_out}:d={fade_out}"
                except Exception:
                    pass

            final_cmd = [ffmpeg, "-y", "-i", tmp_concat]
            audio_args = []
            if audio_path and os.path.isfile(audio_path):
                final_cmd.extend(["-i", audio_path])
                audio_args = ["-c:a", "aac", "-b:a", "192k", "-shortest", "-map", "0:v:0", "-map", "1:a:0"]
                notes.append(f"✓ Audio eklendi: {os.path.basename(audio_path)}")
            else:
                audio_args = ["-an"]
                notes.append("⚠ Audio yok (audio_path boş veya yok)")

            final_cmd.extend([
                "-vf", video_filter,
                "-c:v", "libx264", "-preset", preset, "-crf", str(crf),
                "-pix_fmt", "yuv420p",
            ] + audio_args + [out_path])

            r = subprocess.run(final_cmd, capture_output=True, text=True)
            if r.returncode != 0:
                return ("", f"❌ final render fail: {r.stderr[-500:]}")

            # Cleanup
            try:
                os.remove(tmp_concat)
                os.remove(list_file)
            except Exception:
                pass

            file_mb = os.path.getsize(out_path) / 1_048_576
            notes.append(f"✓ Master render: {output_filename} ({file_mb:.1f} MB)")
            return (out_path, "\n".join(notes))
        except Exception as e:
            return ("", f"❌ MasterRender hata: {e}")


# =============================================================================
# NODE_CLASS_MAPPINGS — Sprint 2/3/5/6 yeni node'ları registry'ye ekle
# =============================================================================
# =============================================================================
# Sprint 16 (Flux Kontext): Multi-image karakter sürekliliği
# =============================================================================
class FikoYT_MVD_FluxKontextChain:
    """🔗 Flux Kontext: multi-image referans + text edit ile karakter sürekliliği.

    BFL Flux.1 Kontext Dev (12B DiT). Önceki segment görselini "kontext" olarak
    alır, yeni prompt ile aynı karakter/sahne/stil tutarlılığını korur.

    Use case: Sahne 1 render edildi → Sahne 2'yi render ederken Sahne 1
    görselini referans olarak ver → karakter, kıyafet, stil tutarlı kalır.

    Node feature-detect: 'ReferenceLatent' veya 'FluxKontextImageRescale' varsa
    Kontext zinciri kurulur. Yoksa graceful uyarı.
    """
    CATEGORY = CATEGORY
    FUNCTION = "chain"
    RETURN_TYPES = ("IMAGE", "STRING")
    RETURN_NAMES = ("image", "status")

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "kontext_image": ("IMAGE", {
                    "tooltip": "Önceki segment görseli (karakter sürekliliği için referans)"
                }),
                "model": ("MODEL",),
                "clip": ("CLIP",),
                "vae": ("VAE",),
                "edit_prompt": ("STRING", {
                    "default": "the same character, now in a different scene, "
                               "maintaining the same identity, outfit, and style",
                    "multiline": True,
                    "tooltip": "Edit komutu — karakter aynı kalsın, sahne değişsin"
                }),
                "negative_prompt": ("STRING", {
                    "default": "blurry, deformed, different person, watermark",
                    "multiline": True,
                }),
                "width": ("INT", {"default": 1024, "min": 256, "max": 4096, "step": 64}),
                "height": ("INT", {"default": 1024, "min": 256, "max": 4096, "step": 64}),
                "steps": ("INT", {"default": 22, "min": 4, "max": 50, "step": 1}),
                "cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.5}),
                "flux_guidance": ("FLOAT", {"default": 2.5, "min": 0.0, "max": 10.0, "step": 0.1,
                    "tooltip": "Kontext default 2.5 (Flux Dev 3.5'den düşük)"}),
                "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}),
            },
            "optional": {
                "second_reference": ("IMAGE", {
                    "tooltip": "İsteğe bağlı 2. referans (örn: hedef poz/kıyafet)"
                }),
            }
        }

    def chain(self, kontext_image, model, clip, vae, edit_prompt, negative_prompt,
              width, height, steps, cfg, flux_guidance, seed, second_reference=None):
        try:
            from nodes import (NODE_CLASS_MAPPINGS as _NCM, CLIPTextEncode,
                               EmptyLatentImage, KSampler, VAEDecode)
            import random
        except ImportError as e:
            return (_empty_image(), f"❌ import: {e}")

        # Kontext-specific node'ları feature-detect
        ref_latent_cls = _NCM.get("ReferenceLatent")  # ComfyUI Kontext native
        kontext_rescale_cls = _NCM.get("FluxKontextImageRescale")
        if not ref_latent_cls and not kontext_rescale_cls:
            return (_empty_image(),
                    "❌ Flux Kontext node'ları yok. ComfyUI 0.3.30+ veya "
                    "ComfyUI-FluxKontextCreator yükle. Workflow template'i: "
                    "workflow/flux_kontext_segment.json")

        if seed == 0:
            seed = random.randint(1, 0xFFFFFFFF)

        try:
            # Text encode
            cte = CLIPTextEncode()
            pos = cte.encode(clip, edit_prompt)[0]
            neg = cte.encode(clip, negative_prompt)[0]

            # Flux guidance
            fg_cls = _NCM.get("FluxGuidance")
            if fg_cls:
                pos = fg_cls().append(pos, flux_guidance)[0]

            # Kontext image rescale (önerilen)
            ctx_img = kontext_image
            if kontext_rescale_cls:
                try:
                    ctx_img = kontext_rescale_cls().rescale(kontext_image)[0]
                except Exception:
                    pass

            # VAE encode kontext image → latent
            vae_encode_cls = _NCM.get("VAEEncode")
            if not vae_encode_cls:
                return (_empty_image(), "❌ VAEEncode node yok")
            ctx_latent = vae_encode_cls().encode(vae, ctx_img)[0]

            # ReferenceLatent ile positive cond'a kontext bağla
            if ref_latent_cls:
                pos = ref_latent_cls().append(pos, ctx_latent)[0]

            # 2. referans (opsiyonel) — ikinci kontext
            if second_reference is not None and ref_latent_cls:
                ctx2 = second_reference
                if kontext_rescale_cls:
                    try:
                        ctx2 = kontext_rescale_cls().rescale(second_reference)[0]
                    except Exception:
                        pass
                ctx2_latent = vae_encode_cls().encode(vae, ctx2)[0]
                pos = ref_latent_cls().append(pos, ctx2_latent)[0]

            # Empty latent for new generation
            eli = EmptyLatentImage()
            latent = eli.generate(width, height, 1)[0]

            # Sample
            ks = KSampler()
            samples = ks.sample(model, seed, steps, cfg, "euler", "beta",
                                 pos, neg, latent, 1.0)[0]
            vd = VAEDecode()
            out = vd.decode(vae, samples)[0]

            return (out, f"✓ Flux Kontext zinciri uygulandı "
                          f"({width}×{height}, steps={steps}, g={flux_guidance})")
        except Exception as e:
            import traceback; traceback.print_exc()
            return (_empty_image(), f"❌ Kontext chain hata: {e}")


NODE_CLASS_MAPPINGS_VISUAL_EXTRA = {
    "FikoYT_MVD_PoseExtractor":      FikoYT_MVD_PoseExtractor,
    "FikoYT_MVD_ControlNetUnion":    FikoYT_MVD_ControlNetUnion,
    "FikoYT_MVD_RegionalMultiChar":  FikoYT_MVD_RegionalMultiChar,
    "FikoYT_MVD_LightningPreview":   FikoYT_MVD_LightningPreview,
    "FikoYT_MVD_ProfessionalGrade":  FikoYT_MVD_ProfessionalGrade,
    "FikoYT_MVD_AspectPreset":       FikoYT_MVD_AspectPreset,
    "FikoYT_MVD_MasterRender":       FikoYT_MVD_MasterRender,
    "FikoYT_MVD_FluxKontextChain":   FikoYT_MVD_FluxKontextChain,
}

NODE_DISPLAY_NAME_MAPPINGS_VISUAL_EXTRA = {
    "FikoYT_MVD_PoseExtractor":      "🦴 MVD · Pose Extractor (DWPose)",
    "FikoYT_MVD_ControlNetUnion":    "🎛 MVD · ControlNet Union",
    "FikoYT_MVD_RegionalMultiChar":  "🧑‍🤝‍🧑 MVD · Regional Multi-Char",
    "FikoYT_MVD_LightningPreview":   "⚡ MVD · Lightning Preview (4-step)",
    "FikoYT_MVD_ProfessionalGrade":  "🎨 MVD · Professional Grade (LUT/Vignette/CA/Letterbox)",
    "FikoYT_MVD_AspectPreset":       "📐 MVD · Aspect Preset (TikTok/YT/Insta)",
    "FikoYT_MVD_MasterRender":       "🎬 MVD · Master Render (FFmpeg + audio)",
    "FikoYT_MVD_FluxKontextChain":   "🔗 MVD · Flux Kontext Chain (segment sürekliliği)",
}


__all__ = [
    "FikoYT_MVD_SegmentImageGen",
    "FikoYT_MVD_HiResFix",
    "FikoYT_MVD_AlbumCoverGen",
    "FikoYT_MVD_ImageAspectBatch",
    "FikoYT_MVD_PoseExtractor",
    "FikoYT_MVD_ControlNetUnion",
    "FikoYT_MVD_RegionalMultiChar",
    "FikoYT_MVD_LightningPreview",
    "FikoYT_MVD_ProfessionalGrade",
    "FikoYT_MVD_AspectPreset",
    "FikoYT_MVD_MasterRender",
    "MUSIC_VIDEO_ASPECT_PRESETS",
    "_empty_image",
    "_get_model_search_paths",
    "_try_apply_flux_guidance",
    "_try_apply_pulid",
    "_load_input_image",
    "_try_apply_ipadapter",
    "_try_run_preprocessor",
    "_try_apply_controlnet",
    "_try_apply_controlnet_union",
    "_classify_ref_mode",
    "_apply_references_chain",
    "_get_missing_flux_models",
    "_download_one_sync",
    "_comfy_class_available",
    "_find_controlnet_file",
    "_visual_ref_capabilities",
    "_detect_pipeline_nodes",
    "_next_free_id",
    "_inject_visual_references",
    "NODE_CLASS_MAPPINGS_VISUAL_EXTRA",
    "NODE_DISPLAY_NAME_MAPPINGS_VISUAL_EXTRA",
]
