"""
FikoYT MVD — LTX Video yardımcı sabitler ve fonksiyonlar.

Bu modül LTX Video tabanlı node'lar için ortak sabitleri, çözünürlük/frame
preset'lerini, ffmpeg/audio/video süre ölçme yardımcılarını ve dosya isim
güvenlik fonksiyonlarını içerir.
"""

from __future__ import annotations

import os
import re
import shutil
import subprocess
from typing import List, Optional, Tuple

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


# ---------------------------------------------------------------------------
# Sabitler
# ---------------------------------------------------------------------------

# LTX standart preset'ler: width x height -> (w, h, default_frames)
LTX_RES_PRESETS = {
    "576x320": (576, 320, 121),
    "768x512": (768, 512, 121),
    "1216x704": (1216, 704, 121),
    "1024x576_long": (1024, 576, 257),
}

LTX_FPS_DEFAULT = 24

# LTX modeli sadece (8k+1) frame sayılarında verimli çalışır
LTX_FRAME_PRESETS = [49, 65, 97, 121, 161, 201, 257]

MODEL_DOWNLOAD_HINTS = {
    "ltx-video-2b-v0.9.5.safetensors": (
        "https://huggingface.co/Lightricks/LTX-Video/blob/main/"
        "ltx-video-2b-v0.9.5.safetensors"
    ),
}


# ---------------------------------------------------------------------------
# Model dosyası bulma
# ---------------------------------------------------------------------------

def _get_models_root() -> str:
    """ComfyUI checkpoints klasörünün ilk yolunu döndürür."""
    if folder_paths is None:
        return ""
    try:
        paths = folder_paths.get_folder_paths("checkpoints")
        if paths:
            return paths[0]
    except Exception:
        pass
    return ""


def _list_ltx_checkpoints() -> List[str]:
    """models/checkpoints ve models/diffusion_models altında LTX dosyalarını listele."""
    if folder_paths is None:
        return []

    bulunanlar: List[str] = []
    klasor_anahtarlari = ("checkpoints", "diffusion_models")

    for anahtar in klasor_anahtarlari:
        try:
            yollar = folder_paths.get_folder_paths(anahtar)
        except Exception:
            yollar = []

        for kok in yollar or []:
            if not kok or not os.path.isdir(kok):
                continue
            try:
                for ad in os.listdir(kok):
                    if not ad.lower().endswith(".safetensors"):
                        continue
                    if ad.lower().startswith("ltx"):
                        if ad not in bulunanlar:
                            bulunanlar.append(ad)
            except Exception:
                continue

    return bulunanlar


# ---------------------------------------------------------------------------
# Frame / süre hesabı
# ---------------------------------------------------------------------------

def _estimate_frames_for_duration(seconds: float, fps: int = LTX_FPS_DEFAULT) -> int:
    """Süreyi LTX-uyumlu (8k+1) frame sayısına çevir.

    LTX 2.3 `length` INT step=8, +1 baş kare offset.
    Örnek: 5s @ 24fps -> 121; 10s @ 24fps -> 241; 30s @ 24fps -> 721; 120s @ 24fps -> 2881.
    Min 49, üst sınır 2881 (~120s @ 24fps).
    """
    try:
        sec = float(seconds)
        fps_i = int(fps) if fps else LTX_FPS_DEFAULT
    except Exception:
        return 97

    if sec <= 0 or fps_i <= 0:
        return 97

    # +1 baş kare ile hedef raw frame
    raw = int(round(sec * fps_i)) + 1
    step = 8
    MAX_F = 2881  # ~2 dakika @ 24fps
    if raw < 49:
        return 49
    if raw > MAX_F:
        return MAX_F
    # Yukarı yuvarlama tercih edilir — video audio'dan kısa olmasın
    base = ((raw - 1 + step - 1) // step) * step
    frames = base + 1
    if frames < 49:
        frames = 49
    if frames > MAX_F:
        frames = MAX_F
    return frames


# ---------------------------------------------------------------------------
# ffmpeg / ffprobe
# ---------------------------------------------------------------------------

def _find_ffmpeg() -> Optional[str]:
    """PATH'te ffmpeg ara. Bulunamazsa None döndür."""
    ortam = os.environ.get("FFMPEG_BIN")
    if ortam and os.path.isfile(ortam):
        return ortam

    yol = shutil.which("ffmpeg")
    if yol:
        return yol

    # Windows portable bundled konum dene
    olasi = [
        os.path.join(os.environ.get("ProgramFiles", ""), "ffmpeg", "bin", "ffmpeg.exe"),
        r"C:\ffmpeg\bin\ffmpeg.exe",
    ]
    for p in olasi:
        if p and os.path.isfile(p):
            return p
    return None


def _find_ffprobe() -> Optional[str]:
    ortam = os.environ.get("FFPROBE_BIN")
    if ortam and os.path.isfile(ortam):
        return ortam
    yol = shutil.which("ffprobe")
    if yol:
        return yol
    ff = _find_ffmpeg()
    if ff:
        aday = os.path.join(os.path.dirname(ff), "ffprobe.exe" if os.name == "nt" else "ffprobe")
        if os.path.isfile(aday):
            return aday
    return None


def _run_ffmpeg(args: list, timeout: int = 600) -> Tuple[int, str]:
    """ffmpeg subprocess sarmalayıcı. (returncode, stderr_text) döndürür."""
    ff = _find_ffmpeg()
    if ff is None:
        return (127, "ffmpeg bulunamadı")
    try:
        sonuc = subprocess.run(
            [ff, *args],
            capture_output=True,
            text=True,
            timeout=timeout,
            check=False,
        )
        stderr = sonuc.stderr or ""
        return (sonuc.returncode, stderr)
    except subprocess.TimeoutExpired:
        return (124, "ffmpeg zaman aşımı")
    except Exception as e:
        return (1, f"ffmpeg hata: {e}")


def _audio_duration_sec(path: str) -> float:
    """Ses dosyası süresini saniye olarak döndür. Hatada 0."""
    if not path or not os.path.isfile(path):
        return 0.0

    probe = _find_ffprobe()
    if probe:
        try:
            sonuc = subprocess.run(
                [
                    probe, "-v", "error", "-show_entries", "format=duration",
                    "-of", "default=noprint_wrappers=1:nokey=1", path,
                ],
                capture_output=True, text=True, timeout=30, check=False,
            )
            metin = (sonuc.stdout or "").strip()
            if metin:
                return float(metin)
        except Exception:
            pass

    # soundfile fallback
    try:
        import soundfile as sf  # type: ignore
        with sf.SoundFile(path) as f:
            return float(len(f)) / float(f.samplerate or 1)
    except Exception:
        pass

    # librosa fallback
    try:
        import librosa  # type: ignore
        return float(librosa.get_duration(path=path))
    except Exception:
        pass

    return 0.0


def _video_duration_sec(path: str) -> float:
    """Video süresi saniye. Hatada 0."""
    if not path or not os.path.isfile(path):
        return 0.0
    probe = _find_ffprobe()
    if probe is None:
        return 0.0
    try:
        sonuc = subprocess.run(
            [
                probe, "-v", "error", "-show_entries", "format=duration",
                "-of", "default=noprint_wrappers=1:nokey=1", path,
            ],
            capture_output=True, text=True, timeout=30, check=False,
        )
        metin = (sonuc.stdout or "").strip()
        if metin:
            return float(metin)
    except Exception:
        return 0.0
    return 0.0


# ---------------------------------------------------------------------------
# Dosya adı güvenliği
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Image ↔ Video çözünürlük eşleme (LTX en-boy oranı koruması)
# ---------------------------------------------------------------------------

def _resolution_match(image_w, image_h, target_long_edge: int = 768,
                      min_dim: int = 64, multiple: int = 32) -> Tuple[int, int]:
    """Image en-boy oranını koruyarak LTX-uyumlu video çözünürlüğü döndür.

    target_long_edge: uzun kenar (LTX 2.3 sweet spot = 768)
    multiple: LTX zorunlu kat (32)
    min_dim: en az 64 px

    LTX video kare/oran image ile aynı olmazsa, model resmi merkezi crop+resize
    yapar — bu yüzlerin/karakterin değişmesine sebep olur. Bu fonksiyon
    image'in oranını koruyarak uygun video boyutu üretir.
    """
    try:
        iw = int(image_w)
        ih = int(image_h)
    except Exception:
        return 768, 512
    if iw <= 0 or ih <= 0:
        return 768, 512
    aspect = iw / ih
    if aspect >= 1.0:
        # Landscape — uzun kenar genişlik
        vw = int(target_long_edge)
        vh = max(min_dim, int(round(target_long_edge / aspect / multiple)) * multiple)
    else:
        # Portrait — uzun kenar yükseklik
        vh = int(target_long_edge)
        vw = max(min_dim, int(round(target_long_edge * aspect / multiple)) * multiple)
    # multiple güvencesi (long edge de katı olmalı)
    vw = max(min_dim, (vw // multiple) * multiple) if vw % multiple else vw
    vh = max(min_dim, (vh // multiple) * multiple) if vh % multiple else vh
    return vw, vh


def _image_dimensions_from_url_or_path(image_url_or_path: str):
    """Image dosyasından (path veya /view?filename URL) (width, height) çıkar.

    Birden fazla path varyantı dener; başarısızsa None döner ve sebebi loglar.
    """
    import urllib.parse
    try:
        from PIL import Image
    except Exception:
        print("[FikoYT LTX] _image_dimensions: PIL/Pillow yok → image boyutu okunamaz")
        return None
    s = (image_url_or_path or "").strip()
    if not s:
        return None

    # URL parse (/view?filename=...&subfolder=...&type=output|input|temp)
    if "filename=" in s or s.startswith("/view"):
        try:
            qs = urllib.parse.urlparse(s).query or s.split("?", 1)[-1]
            params = urllib.parse.parse_qs(qs)
            filename = (params.get("filename") or [""])[0]
            subfolder_raw = (params.get("subfolder") or [""])[0]
            subfolder_clean = subfolder_raw.replace("\\", "/").replace("%5C", "/").strip("/")
            type_ = (params.get("type") or ["input"])[0]
            if folder_paths is None or not filename:
                print(f"[FikoYT LTX] _image_dimensions: folder_paths yok veya filename boş (s={s[:120]})")
                return None
            if type_ == "output":
                base = folder_paths.get_output_directory()
            elif type_ == "temp":
                base = folder_paths.get_temp_directory()
            else:
                base = folder_paths.get_input_directory()
            # Birkaç path varyantı dene (clean, raw, düz)
            candidates = []
            if subfolder_clean:
                candidates.append(os.path.join(base, subfolder_clean.replace("/", os.sep), filename))
            if subfolder_raw and subfolder_raw != subfolder_clean:
                candidates.append(os.path.join(
                    base, subfolder_raw.replace("/", os.sep).replace("\\", os.sep), filename
                ))
            candidates.append(os.path.join(base, filename))
            for cand in candidates:
                if os.path.isfile(cand):
                    try:
                        with Image.open(cand) as img:
                            return img.size  # (w, h)
                    except Exception as e:
                        print(f"[FikoYT LTX] _image_dimensions: PIL open hata ({cand}): {e}")
                        return None
            print(f"[FikoYT LTX] _image_dimensions: dosya bulunamadı "
                  f"(filename={filename}, sub={subfolder_raw}, type={type_})")
            return None
        except Exception as e:
            print(f"[FikoYT LTX] _image_dimensions URL parse hata: {e} (s={s[:120]})")
            return None

    # Absolute path
    if os.path.isabs(s) and os.path.isfile(s):
        try:
            with Image.open(s) as img:
                return img.size
        except Exception as e:
            print(f"[FikoYT LTX] _image_dimensions: PIL open hata (abs={s}): {e}")
            return None

    # Relative path — input klasöründe dene
    if folder_paths is not None:
        try:
            cand = os.path.join(folder_paths.get_input_directory(), s)
            if os.path.isfile(cand):
                with Image.open(cand) as img:
                    return img.size
        except Exception:
            pass

    print(f"[FikoYT LTX] _image_dimensions: hiçbir varyantta dosya bulunamadı (s={s[:120]})")
    return None


_SAFE_RE = re.compile(r"[^A-Za-z0-9_\-]+")


def _safe_filename(s: str, max_len: int = 60) -> str:
    """Slug benzeri güvenli dosya adı. Alnum + _ - dışındakileri _ yapar."""
    if not s:
        return "dosya"
    temiz = _SAFE_RE.sub("_", str(s).strip())
    temiz = temiz.strip("_-") or "dosya"
    if len(temiz) > max_len:
        temiz = temiz[:max_len].rstrip("_-") or "dosya"
    return temiz
