#!/usr/bin/env python3
import os
import shutil
import subprocess
from pathlib import Path
import inquirer
from typing import Optional, List
import re
import urllib.parse
import urllib.request
import urllib.error
import json
import ssl

# ---------- 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

# ---------- Réglages Retry / Telegram ----------
MAX_RETRIES = 10  # nombre de réessais APRÈS le premier échec (=> 1 + MAX_RETRIES tentatives max)

TELEGRAM_ENABLED = True          # passe à False pour désactiver tout envoi
TELEGRAM_BOT_TOKEN = os.environ.get(
    "TELEGRAM_BOT_TOKEN",
    "8284311026:AAEWrrzpJ71wKsGJiTwhi84gWJ7ej2KORsM"
)
TELEGRAM_CHAT_ID = os.environ.get(
    "TELEGRAM_CHAT_ID",
    "8418649348"  # ton chat privé avec le bot
)
TELEGRAM_PROGRESS_UPDATES = True # barre de progression sur un seul message

# ⚠️ CONTEXTE SSL SANS VÉRIFICATION (pour contourner CERTIFICATE_VERIFY_FAILED)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

# 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():
    """
    Vérifie que HandBrakeCLI est accessible (Windows + Linux).
    Sur Windows le binaire est souvent HandBrakeCLI.exe.
    """
    candidates = ["HandBrakeCLI", "HandBrakeCLI.exe"]
    found = any(shutil.which(c) is not None for c in candidates)
    if not found:
        raise SystemExit(
            "Erreur: HandBrakeCLI introuvable dans le PATH.\n"
            "- Vérifie que HandBrakeCLI (version CLI) est installé.\n"
            "- Ajoute son dossier au PATH, ou lance ce script depuis un terminal où 'HandBrakeCLI --version' fonctionne."
        )

# ---------- 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 classify_media(src: Path) -> str:
    """
    Devine si c'est une série (SxxExx) ou un film à partir du nom de fichier
    et renvoie une description lisible.
    """
    name = src.stem
    m = re.search(r"[Ss](\d{1,2})[ ._-]?[Ee](\d{1,2})", name)
    if m:
        s = int(m.group(1))
        e = int(m.group(2))
        return f"Série: {name} (S{s:02d}E{e:02d})"
    else:
        return f"Film: {name}"

# ---------- Telegram : helpers généraux ----------
def _telegram_base_url(method: str) -> str:
    return f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/{method}"

def send_telegram_message(message: str):
    """Envoi simple, sans récupérer l'ID."""
    if not TELEGRAM_ENABLED:
        return
    if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
        print("⚠️  Telegram non configuré (TOKEN/CHAT_ID manquant).")
        return

    data = urllib.parse.urlencode({
        "chat_id": TELEGRAM_CHAT_ID,
        "text": message,
    }).encode("utf-8")

    try:
        with urllib.request.urlopen(
            _telegram_base_url("sendMessage"),
            data=data,
            timeout=10,
            context=ssl_context
        ) as resp:
            body = resp.read()
            if resp.status != 200:
                print(f"⚠️  Échec envoi Telegram: HTTP {resp.status} — {body!r}")
    except urllib.error.HTTPError as e:
        try:
            detail = e.read().decode("utf-8", errors="ignore")
        except Exception:
            detail = "<no body>"
        print(f"⚠️  Erreur envoi Telegram: {e} — réponse: {detail}")
    except Exception as e:
        print(f"⚠️  Erreur envoi Telegram: {e}")

def send_telegram_message_with_result(message: str) -> Optional[int]:
    """
    Envoie un message et retourne son message_id (pour pouvoir l'éditer).
    Retourne None en cas de problème.
    """
    if not TELEGRAM_ENABLED:
        return None
    if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
        print("⚠️  Telegram non configuré (TOKEN/CHAT_ID manquant).")
        return None

    data = urllib.parse.urlencode({
        "chat_id": TELEGRAM_CHAT_ID,
        "text": message,
    }).encode("utf-8")

    try:
        with urllib.request.urlopen(
            _telegram_base_url("sendMessage"),
            data=data,
            timeout=10,
            context=ssl_context
        ) as resp:
            body = resp.read()
            payload = json.loads(body.decode("utf-8"))
            if not payload.get("ok"):
                print(f"⚠️  Réponse Telegram non OK: {payload}")
                return None
            return payload["result"]["message_id"]
    except urllib.error.HTTPError as e:
        try:
            detail = e.read().decode("utf-8", errors="ignore")
        except Exception:
            detail = "<no body>"
        print(f"⚠️  Erreur envoi Telegram (with_result): {e} — réponse: {detail}")
        return None
    except Exception as e:
        print(f"⚠️  Erreur envoi Telegram (with_result): {e}")
        return None

def edit_telegram_message(message_id: int, text: str):
    """Édite un message existant (pour la barre de progression)."""
    if not TELEGRAM_ENABLED:
        return
    if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
        return
    if message_id is None:
        return

    data = urllib.parse.urlencode({
        "chat_id": TELEGRAM_CHAT_ID,
        "message_id": message_id,
        "text": text,
    }).encode("utf-8")

    try:
        with urllib.request.urlopen(
            _telegram_base_url("editMessageText"),
            data=data,
            timeout=10,
            context=ssl_context
        ) as resp:
            body = resp.read()
            if resp.status != 200:
                print(f"⚠️  Échec edit Telegram: HTTP {resp.status} — {body!r}")
    except urllib.error.HTTPError as e:
        try:
            detail = e.read().decode("utf-8", errors="ignore")
        except Exception:
            detail = "<no body>"
        print(f"⚠️  Erreur edit Telegram: {e} — réponse: {detail}")
    except Exception as e:
        print(f"⚠️  Erreur edit Telegram: {e}")

# ---------- Telegram : barre de progression ----------
def build_progress_bar(done: int, total: int, length: int = 20) -> tuple[float, str]:
    if total <= 0:
        return 0.0, "░" * length
    ratio = done / total
    ratio = max(0.0, min(1.0, ratio))
    filled = int(round(ratio * length))
    bar = "█" * filled + "░" * (length - filled)
    percent = ratio * 100.0
    return percent, bar

def send_progress_start(total_files: int) -> Optional[int]:
    if not (TELEGRAM_ENABLED and TELEGRAM_PROGRESS_UPDATES):
        return None
    msg = (
        "Transcodage en cours\n"
        f"0.00% (0/{total_files} fichiers)\n"
        "[░░░░░░░░░░░░░░░░░░░░]"
    )
    return send_telegram_message_with_result(msg)

def update_progress_message(message_id: int,
                            done: int,
                            total: int,
                            success_no_retry: int,
                            success_with_retry: int,
                            errors: int):
    if not (TELEGRAM_ENABLED and TELEGRAM_PROGRESS_UPDATES):
        return
    percent, bar = build_progress_bar(done, total)
    text = (
        "Transcodage en cours\n"
        f"{percent:.2f}% ({done}/{total} fichiers)\n"
        f"[{bar}]\n\n"
        f"✓ Sans retry : {success_no_retry}\n"
        f"✓ Avec retry : {success_with_retry}\n"
        f"✗ Échecs définitifs : {errors}"
    )
    edit_telegram_message(message_id, text)

# ---------- 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 à 'start'
    - 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:
    # Sous Windows, ton NAS ou répertoire est sur Z:
    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"]

# ---------- Transcodage ----------
def transcode_file(src: Path, overwrite: bool = False) -> tuple[int, Path, Optional[str]]:
    """
    Lance HandBrakeCLI pour un fichier.
    Retourne (returncode, chemin_sortie, message_erreur_court_ou_None).
    """
    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, None

    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:
        # On capture stderr pour récupérer une raison courte en cas d'échec
        # Important: encoding + errors="replace" pour éviter UnicodeDecodeError sous Windows
        result = subprocess.run(
            cmd,
            check=False,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            text=True,
            encoding="utf-8",
            errors="replace",
        )
        error_msg = None
        if result.returncode == 0:
            print(f"[OK ] {out_path}")
        else:
            stderr_text = result.stderr or ""
            # Récupère la dernière ligne non vide de stderr comme "raison courte"
            stderr_lines = [l.strip() for l in stderr_text.splitlines() if l.strip()]
            if stderr_lines:
                error_msg = stderr_lines[-1]
            else:
                error_msg = f"HandBrakeCLI exit code {result.returncode}"
            print(f"[ERR] Code {result.returncode} pour {src} — {error_msg}")
        return result.returncode, out_path, error_msg
    except FileNotFoundError:
        msg = "Erreur: HandBrakeCLI introuvable."
        print(msg)
        return 127, out_path, msg

# ---------- Prompts (modes & options) ----------
def ask_transcode_options() -> tuple[bool, bool]:
    """
    Demande EN MÊME TEMPS :
      - que faire si le fichier _converted existe (overwrite ou skip)
      - supprimer ou non le fichier source après succès
    Retourne (overwrite_bool, delete_source_on_success_bool)
    """
    questions = [
        inquirer.List(
            "overwrite",
            message="Un fichier _converted existe déjà — que faire ?",
            choices=["Ne pas écraser (skip)", "Écraser"],
            default="Ne pas écraser (skip)",
        ),
        inquirer.Confirm(
            "del_src",
            message="Supprimer le fichier source après transcodage réussi ?",
            default=False,
        ),
    ]
    ans = inquirer.prompt(questions)
    if not ans:
        raise SystemExit("Annulé.")
    overwrite = ans["overwrite"] == "Écraser"
    delete_source_on_success = bool(ans["del_src"])
    return overwrite, delete_source_on_success

def select_mode() -> str:
    q = [inquirer.List(
        "mode",
        message="Que veux-tu faire ?",
        choices=[
            "Convert : Fichier",
            "Convert : Répertoire",
            "Convert : Répertoire Récursif",
            "Delete : Delete Origine File",
        ],
    )]
    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 == "Delete : Delete Origine File":
        delete_sources_if_converted_exists()
        return

    # --- Modes de conversion ---
    overwrite, delete_source_on_success = ask_transcode_options()

    if mode == "Convert : Fichier":
        files = [prompt_path_file()]

    elif mode == "Convert : Répertoire":
        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 == "Convert : Répertoire Récursif":
        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.")

    total_files = len(files)
    print(f"\nFichiers à traiter: {total_files}")

    # message Telegram de progression
    progress_msg_id: Optional[int] = send_progress_start(total_files)

    # Stats globales
    errors = 0
    success_no_retry = 0
    success_with_retry = 0
    total_retries = 0
    deleted_sources = 0
    failed_files: list[tuple[str, str]] = []  # (media_label, reason)

    for idx, f in enumerate(files, 1):
        print(f"[{idx}/{total_files}] {f}")

        last_error_msg: Optional[str] = None
        rc = 0
        attempts = 0  # nombre de tentatives pour CE fichier

        # 1 tentative initiale + MAX_RETRIES réessais
        for attempt in range(MAX_RETRIES + 1):
            attempts = attempt + 1
            if attempt > 0:
                print(f"   ↻ Retentative {attempt}/{MAX_RETRIES}")
            # À partir de la 2e tentative, on force overwrite=True pour éviter le SKIP sur un fichier partiel
            overwrite_flag = overwrite or (attempt > 0)
            rc, _out, err_msg = transcode_file(f, overwrite=overwrite_flag)
            last_error_msg = err_msg
            if rc == 0:
                break

        retries_for_file = max(0, attempts - 1)
        total_retries += retries_for_file

        if rc == 0:
            if retries_for_file == 0:
                success_no_retry += 1
            else:
                success_with_retry += 1
                print(f"   ✅ Succès après {retries_for_file} retry(s)")

            if delete_source_on_success:
                try:
                    os.remove(f)
                    deleted_sources += 1
                    print(f"   🗑️  Source supprimée: {f}")
                except Exception as e:
                    print(f"   ⚠️  Échec suppression source {f}: {e}")
        else:
            errors += 1
            short_reason = last_error_msg or f"HandBrakeCLI exit code {rc}"
            media_label = classify_media(f)
            failed_files.append((media_label, short_reason))

            # Notif fichier en échec après tous les essais
            msg = (
                "Échec transcodage après plusieurs essais\n"
                f"{media_label}\n"
                f"Tentatives: {attempts} (retries: {retries_for_file})\n"
                f"Raison: {short_reason}"
            )
            print(msg)  # log console
            send_telegram_message(msg)

        # Mise à jour de la barre de progression (après ce fichier)
        done = idx
        update_progress_message(
            progress_msg_id,
            done,
            total_files,
            success_no_retry,
            success_with_retry,
            errors,
        )

    # --- Résumé console ---
    print("\n--- Résumé ---")
    print(f"Total fichiers           : {total_files}")
    print(f"Succès sans retry        : {success_no_retry}")
    print(f"Succès avec retry        : {success_with_retry}")
    print(f"Échecs définitifs        : {errors}")
    print(f"Total retries effectués  : {total_retries}")
    print(f"Sources supprimées       : {deleted_sources}")

    if failed_files:
        print("\nFichiers en échec définitif :")
        for label, reason in failed_files:
            print(f" - {label} — {reason}")

    # --- Résumé Telegram ---
    summary_msg = (
        "Résumé transcodage\n"
        f"Total fichiers : {total_files}\n"
        f"Succès sans retry : {success_no_retry}\n"
        f"Succès avec retry : {success_with_retry}\n"
        f"Échecs définitifs : {errors}\n"
        f"Total retries effectués : {total_retries}\n"
        f"Sources supprimées : {deleted_sources}"
    )

    if failed_files:
        summary_msg += "\n\nFichiers en échec définitif :\n"
        for label, reason in failed_files:
            summary_msg += f"• {label} — {reason}\n"

    send_telegram_message(summary_msg)

    # Dernière mise à jour de la barre pour la marquer comme finie
    update_progress_message(
        progress_msg_id,
        total_files,
        total_files,
        success_no_retry,
        success_with_retry,
        errors,
    )

if __name__ == "__main__":
    main()
