
#!/usr/bin/env python3
import os
import shutil
import subprocess
from pathlib import Path
import inquirer
from typing import Optional, List

# ---------- Réglages HandBrake ----------
HB_VIDEO_ENCODER = "nvenc_av1"
HB_QUALITY = "25"                # CRF ~ visuellement transparent
HB_PRESET = "slow"               # priorité qualité
HB_AUDIO_MODE = "copy"           # garde l'audio original
HB_LANGS = "fra,eng"             # langues audio/sous-titres à garder (priorité)
HB_CONTAINER_EXT = ".mkv"        # conteneur de sortie

# Extensions vidéo prises en charge
VIDEO_EXTS = {
    ".mp4", ".mkv", ".mov", ".avi", ".wmv", ".m4v",
    ".ts", ".m2ts", ".flv", ".webm", ".mpg", ".mpeg", ".vob"
}

# ---------- Outils requis ----------
def check_handbrake():
    if shutil.which("HandBrakeCLI") is None:
        raise SystemExit("Erreur: HandBrakeCLI introuvable dans le PATH. Installe-le puis réessaie.")

def check_ff_tools():
    ok = True
    if shutil.which("ffprobe") is None:
        print("⚠️  ffprobe introuvable — certaines vérifications seront SKIP.")
        ok = False
    if shutil.which("ffmpeg") is None:
        print("⚠️  ffmpeg introuvable — le decode test sera SKIP.")
        ok = False
    return ok

# ---------- Utilitaires ----------
def build_output_path(src: Path) -> Path:
    """Même dossier, même nom + _converted, extension forcée en .mkv"""
    stem = src.stem
    out_stem = stem if stem.endswith("_converted") else f"{stem}_converted"
    return src.with_name(out_stem + HB_CONTAINER_EXT)

def is_video_file(p: Path) -> bool:
    return p.is_file() and p.suffix.lower() in VIDEO_EXTS

def list_videos_in_dir(directory: Path, recursive: bool) -> list[Path]:
    if recursive:
        return [p for p in directory.rglob("*") if is_video_file(p)]
    else:
        return [p for p in directory.iterdir() if is_video_file(p)]

def find_original_for_converted(converted: Path) -> Optional[Path]:
    """Retrouve l'original d'un *_converted.ext* (même dossier, sans suffixe, extension connue)."""
    stem = converted.stem
    if not stem.endswith("_converted"):
        return None
    orig_stem = stem[:-10]  # retirer "_converted"
    parent = converted.parent
    for ext in VIDEO_EXTS:
        candidate = parent / f"{orig_stem}{ext}"
        if candidate.exists() and candidate.is_file():
            return candidate
    return None

# ---------- Navigateur de dossiers ----------
def list_subdirs(path: Path) -> List[str]:
    items = []
    try:
        for entry in sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
            if entry.is_dir():
                items.append(entry.name + "/")
    except PermissionError:
        print(f"⚠️  Permission refusée: {path}")
    return items

def browse_dir(start: str = "/") -> Path:
    """
    Navigateur interactif de dossiers:
    - Commence à '/'
    - Affiche sous-dossiers + '..' (remonter) + '✅ Valider ici'
    - Retourne le Path validé
    """
    current = Path(start).resolve()
    if not current.exists():
        current = Path("/")

    while True:
        choices = []
        header = f"[ {str(current)} ]"
        choices.append(header)

        if current != current.anchor:
            choices.append("⬆️  ..")

        subdirs = list_subdirs(current)
        if subdirs:
            choices.extend(subdirs)
        else:
            choices.append("(aucun sous-dossier)")

        choices.append("✅ Valider ici")

        ans = inquirer.prompt([inquirer.List("sel", message="Navigue puis valide", choices=choices)])
        if not ans:
            raise SystemExit("Annulé.")
        sel = ans["sel"]

        if sel == "✅ Valider ici":
            return current
        if sel == "⬆️  ..":
            parent = current.parent
            if parent != current:
                current = parent
            continue
        if sel == "(aucun sous-dossier)" or sel == header:
            continue

        next_dir = current / sel.rstrip("/")
        if next_dir.exists() and next_dir.is_dir():
            current = next_dir
        else:
            print(f"⚠️  Inaccessible: {next_dir}")

def prompt_path_dir() -> Path:
    return browse_dir("Z:\\")

def prompt_path_file() -> Path:
    d = browse_dir("Z:\\")
    files = [p for p in d.iterdir() if is_video_file(p)]
    if not files:
        raise SystemExit("Aucune vidéo dans ce dossier.")
    choices = [f.name for f in sorted(files, key=lambda p: p.name.lower())]
    ans = inquirer.prompt([inquirer.List("file", message=f"Fichier dans {d}", choices=choices)])
    if not ans:
        raise SystemExit("Annulé.")
    return d / ans["file"]

# ---------- Vérifications (utilisées uniquement en mode Vérifier) ----------
def ffprobe_ok(path: Path) -> Optional[bool]:
    if shutil.which("ffprobe") is None:
        return None
    result = subprocess.run(["ffprobe", "-v", "error", str(path)],
                            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    return result.returncode == 0

def decode_test_ok(path: Path) -> Optional[bool]:
    if shutil.which("ffmpeg") is None:
        return None
    proc = subprocess.run(["ffmpeg", "-v", "error", "-xerror", "-i", str(path), "-f", "null", "-"],
                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
    if proc.returncode != 0:
        return False
    if proc.stderr.strip():
        return False
    return True

def get_duration_seconds(path: Path) -> Optional[float]:
    if shutil.which("ffprobe") is None:
        return None
    try:
        proc = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
                               "-of", "default=noprint_wrappers=1:nokey=1", str(path)],
                              stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
        if proc.returncode != 0:
            return None
        return float(proc.stdout.strip())
    except Exception:
        return None

def duration_similar(src: Path, dst: Path, tol_seconds: float = 5.0, tol_ratio: float = 0.03) -> Optional[bool]:
    d_src = get_duration_seconds(src)
    d_dst = get_duration_seconds(dst)
    if d_src is None or d_dst is None:
        return None
    diff = abs(d_src - d_dst)
    threshold = max(tol_seconds, tol_ratio * d_src)
    return diff <= threshold

def print_check(label: str, val: Optional[bool]):
    if val is True:
        print(f"   ✅ {label}")
    elif val is False:
        print(f"   ❌ {label}")
    else:
        print(f"   ⏭️  {label} (non vérifié)")

def verify_converted(converted: Path, original: Optional[Path] = None) -> bool:
    print(f"[CHK] {converted.name}")
    all_ok = True

    v1 = ffprobe_ok(converted)
    print_check("ffprobe (lisible / non corrompu)", v1)
    if v1 is False:
        all_ok = False

    v2 = decode_test_ok(converted)
    print_check("Decode test (ffmpeg -> null)", v2)
    if v2 is False:
        all_ok = False

    if original is None:
        original = find_original_for_converted(converted)
    v3 = duration_similar(original, converted) if original else None
    if original:
        print_check(f"Durée similaire à l'original ({original.name})", v3)
        if v3 is False:
            all_ok = False
    else:
        print("   ⏭️  Durée similaire (original introuvable)")

    if original is None:
        d = get_duration_seconds(converted)
        v4 = (d is not None and d > 0.5) if d is not None else None
        print_check("Durée non nulle", v4)
        if v4 is False:
            all_ok = False

    return all_ok

# ---------- Transcodage ----------
def transcode_file(src: Path, overwrite: bool = False) -> tuple[int, Path]:
    out_path = build_output_path(src)

    if out_path.exists() and not overwrite:
        print(f"[SKIP] Sortie existe déjà: {out_path}")
        return 0, out_path

    cmd = [
        "HandBrakeCLI",
        "-i", str(src),
        "-o", str(out_path),
        "-e", HB_VIDEO_ENCODER,
        "-q", HB_QUALITY,
        "--encoder-preset", HB_PRESET,
        "-E", HB_AUDIO_MODE,
        "--all-audio",
        "--all-subtitles",
        # "--audio-lang-list", HB_LANGS,
        # "--subtitle-lang-list", HB_LANGS,
        "--subtitle-burned=none",
        "--optimize",
    ]

    print(f"[RUN] {src.name} -> {out_path.name}")
    try:
        result = subprocess.run(cmd, check=False)
        if result.returncode == 0:
            print(f"[OK ] {out_path}")
        else:
            print(f"[ERR] Code {result.returncode} pour {src}")
        return result.returncode, out_path
    except FileNotFoundError:
        print("Erreur: HandBrakeCLI introuvable.")
        return 127, out_path

# ---------- Prompts (modes) ----------
def ask_overwrite() -> bool:
    q = [inquirer.List("overwrite",
                       message="Un fichier _converted existe déjà — que faire ?",
                       choices=["Ne pas écraser (skip)", "Écraser"],
                       default="Ne pas écraser (skip)")]
    ans = inquirer.prompt(q)
    return ans and ans["overwrite"] == "Écraser"

def select_mode() -> str:
    q = [inquirer.List(
        "mode",
        message="Que veux-tu faire ?",
        choices=[
            "Un seul fichier",
            "Tout le contenu d’un dossier",
            "Un dossier + tous les sous-dossiers",
            "Vérifier tous les *_converted* d’un dossier (récursif)",
            "Supprimer les originaux s'ils ont un *_converted* (récursif)",
        ],
    )]
    ans = inquirer.prompt(q)
    if not ans:
        raise SystemExit("Annulé.")
    return ans["mode"]

# ---------- Suppression (séparée) des originaux ----------
def delete_sources_if_converted_exists():
    d = prompt_path_dir()
    originals = [p for p in d.rglob("*") if is_video_file(p) and not p.stem.endswith("_converted")]

    if not originals:
        print("Aucun fichier source trouvé.")
        return

    deletable, missing = [], []

    print(f"\nAnalyse de {len(originals)} fichiers source… (vérif existence du converted)")
    for idx, src in enumerate(originals, 1):
        converted = build_output_path(src)
        print(f"[{idx}/{len(originals)}] {src.name}")
        if converted.exists() and converted.is_file():
            deletable.append((src, converted))
            print("   ✅ Converted correspondant trouvé")
        else:
            missing.append(src)
            print("   ⏭️  Pas de converted -> SKIP")

    print("\n--- Aperçu (dry-run) ---")
    print(f"Originaux total                   : {len(originals)}")
    print(f"Supprimables (existe converted)   : {len(deletable)}")
    print(f"Sans converted                    : {len(missing)}")

    if not deletable:
        print("\nRien à supprimer en toute sécurité.")
        return

    q = [inquirer.Confirm("proceed",
                          message=f"Supprimer définitivement {len(deletable)} fichier(s) source ?",
                          default=False)]
    ans = inquirer.prompt(q)
    if not ans or not ans["proceed"]:
        print("Annulé.")
        return

    deleted = failed = 0
    for src, _converted in deletable:
        try:
            os.remove(src)
            deleted += 1
            print(f"🗑️  Supprimé: {src}")
        except Exception as e:
            failed += 1
            print(f"❌ Échec suppression {src}: {e}")

    print("\n--- Résumé suppression ---")
    print(f"Supprimés: {deleted} | Échecs: {failed} | Conservés: {len(originals) - deleted}")

# ---------- Programme principal ----------
def main():
    check_handbrake()

    mode = select_mode()

    if mode == "Vérifier tous les *_converted* d’un dossier (récursif)":
        check_ff_tools()  # <= uniquement en mode vérification
        d = prompt_path_dir()
        conv_files = [p for p in d.rglob("*") if p.is_file() and p.stem.endswith("_converted")]
        if not conv_files:
            print("Aucun fichier *_converted* trouvé (récursif).")
            return
        print(f"\nFichiers à vérifier: {len(conv_files)}")
        bad = 0
        for idx, cf in enumerate(conv_files, 1):
            print(f"[{idx}/{len(conv_files)}] {cf}")
            ok = verify_converted(cf)
            if not ok:
                bad += 1
        print("\n--- Résumé vérification ---")
        print(f"Total: {len(conv_files)} | OK: {len(conv_files)-bad} | KO: {bad}")
        return

    if mode == "Supprimer les originaux s'ils ont un *_converted* (récursif)":
        # pas de check_ff_tools ici (pas nécessaire)
        delete_sources_if_converted_exists()
        return

    # --- Modes de conversion (AUCUNE vérification ici) ---
    overwrite = ask_overwrite()

    if mode == "Un seul fichier":
        files = [prompt_path_file()]

    elif mode == "Tout le contenu d’un dossier":
        d = prompt_path_dir()
        files = list_videos_in_dir(d, recursive=False)
        if not files:
            print("Aucun fichier vidéo trouvé dans ce dossier.")
            return

    elif mode == "Un dossier + tous les sous-dossiers":
        d = prompt_path_dir()
        files = list_videos_in_dir(d, recursive=True)
        if not files:
            print("Aucun fichier vidéo trouvé dans ce dossier (récursif).")
            return

    else:
        raise SystemExit("Mode inconnu.")

    print(f"\nFichiers à traiter: {len(files)}")
    errors = 0

    for idx, f in enumerate(files, 1):
        print(f"[{idx}/{len(files)}] {f}")
        rc, _out = transcode_file(f, overwrite=overwrite)
        if rc != 0:
            errors += 1

    print("\n--- Résumé ---")
    print(f"Total: {len(files)} | Succès transcodage: {len(files)-errors} | Échecs transcodage: {errors}")

if __name__ == "__main__":
    main()
