#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
╔══════════════════════════════════════════════════════════════════════════╗
║  CBZ ➜ MOBI AUTO CONVERTER — WATCHER DAEMON                              ║
║  Surveille un dossier et convertit automatiquement les .cbz en .mobi     ║
║  via Calibre (ebook-convert).                                            ║
╚══════════════════════════════════════════════════════════════════════════╝
"""

import argparse
import os
import queue
import shutil
import subprocess
import sys
import threading
import time
from pathlib import Path

try:
    from watchdog.observers import Observer
    from watchdog.events import FileSystemEventHandler
except ImportError:
    sys.stderr.write(
        "\n[!!] Module 'watchdog' introuvable.\n"
        "     >>> pip install watchdog\n\n"
    )
    sys.exit(1)


# ═══════════════════════════════════════════════════════════════════════════
#  TERMINAL / STYLE RÉTRO-HACKER
# ═══════════════════════════════════════════════════════════════════════════

if os.name == "nt":
    os.system("")

class C:
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    GREEN = "\033[92m"
    CYAN = "\033[96m"
    YELLOW = "\033[93m"
    RED = "\033[91m"
    MAGENTA = "\033[95m"
    GREY = "\033[90m"

BANNER = rf"""{C.CYAN}{C.BOLD}
   ▄████▄   ▄▄▄▄    ▒███████▒     ▄▄▄▄███▄▄▄▄       ▒█████    ▄▄▄▄    ██▓
  ▒██▀ ▀█  ▓█████▄  ▒ ▒ ▒ ▄▀░   ▓██▒▄██▀▀▀██▄▒██▒  ▒██▒  ██▒▓█████▄ ▓██▒
  ▒▓█    ▄ ▒██▒ ▄██ ░ ▒ ▄▀▒░    ▓██▒██▒  ▒██▒▒██░  ▒██░  ██▒▒██▒ ▄██▒██▒
  ▒▓▓▄ ▄██▒▒██░█▀     ▄▀▒   ░   ▓██▓██▒  ▒██▒▒██░  ▒██   ██░▒██░█▀  ░██░
  ▒ ▓███▀ ░░▓█  ▀█▓ ▒███████▒   ▒██▒▓█  ▓██▒░██████░ ████▓▒░░▓█  ▀█▓░██░
  ░ ░▒ ▒  ░░▒▓███▀▒ ░▒▒ ▓░▒░▒   ░▒ ░▒▓▒▒░▒░ ░░ ▒░▓  ░ ▒░▒░▒░ ░▒▓███▀▒░▓
    ░  ▒   ▒░▒   ░  ░░▒ ▒ ░ ▒     ░ ▒░▒░ ░  ░ ░ ▒  ░ ░ ▒ ▒░ ▒░▒   ░  ▒ ░
  ░         ░    ░  ░ ░ ░ ░ ░     ░ ░ ░ ░ ░    ░ ░  ░ ░ ░ ▒   ░    ░  ▒ ░
  ░ ░       ░           ░ ░     ░   ░ ░              ░ ░   ░       ░
  ░              ░     ░
{C.RESET}{C.MAGENTA}{C.BOLD}
        >>  [ C B Z ]  ═══════▶  [ M O B I ]   A U T O - C O N V E R T E R  <<
{C.RESET}{C.GREY}        ── watcher daemon // calibre backend // rétro-hacker edition ──{C.RESET}
"""

DONE_BADGE = f"""{C.GREEN}
        ┌─────────────────────────────┐
        │   ✔  C O N V E R S I O N    │
        │        T E R M I N É E      │
        └─────────────────────────────┘{C.RESET}
"""

def ts() -> str:
    return time.strftime("%H:%M:%S")

def log(tag: str, color: str, msg: str) -> None:
    print(f"{C.GREY}[{ts()}]{C.RESET} {color}{tag}{C.RESET} {msg}")

def log_scan(msg): log("[SCAN]", C.CYAN, msg)
def log_sync(msg): log("[SYNC]", C.YELLOW, msg)
def log_conv(msg): log("[CONV]", C.MAGENTA, msg)
def log_ok(msg):   log("[OK]  ", C.GREEN, msg)
def log_fail(msg): log("[FAIL]", C.RED, msg)


# ═══════════════════════════════════════════════════════════════════════════
#  RÉSOLUTION DE L'EXÉCUTABLE CALIBRE
# ═══════════════════════════════════════════════════════════════════════════

def resolve_ebook_convert() -> str:
    """Cherche ebook-convert dans le PATH, sinon fallback macOS ou Windows."""
    found = shutil.which("ebook-convert")
    if found:
        return found

    # Fallback macOS direct
    mac_path = Path("/Applications/calibre.app/Contents/MacOS/ebook-convert")
    if mac_path.exists():
        return str(mac_path)

    # Fallback Windows
    if os.name == "nt":
        fallback = Path(r"C:\Program Files\Calibre2\ebook-convert.exe")
        if fallback.exists():
            return str(fallback)

    log_fail("Exécutable 'ebook-convert' introuvable.")
    log_fail("Installe Calibre (https://calibre-ebook.com) dans /Applications.")
    sys.exit(1)


# ═══════════════════════════════════════════════════════════════════════════
#  STABILITÉ DU FICHIER
# ═══════════════════════════════════════════════════════════════════════════

def wait_until_stable(path: Path, stable_seconds: float = 1.5,
                       poll_interval: float = 0.3, timeout: float = 120.0) -> bool:
    start = time.time()
    last_size = -1
    stable_since = None

    while True:
        if not path.exists():
            return False

        try:
            size = path.stat().st_size
        except OSError:
            return False

        if size == last_size:
            if stable_since is None:
                stable_since = time.time()
            elif time.time() - stable_since >= stable_seconds:
                return True
        else:
            stable_since = None
            last_size = size

        if time.time() - start > timeout:
            return False

        time.sleep(poll_interval)


# ═══════════════════════════════════════════════════════════════════════════
#  CONVERSION
# ═══════════════════════════════════════════════════════════════════════════

def convert_file(cbz_path: Path, out_dir: Path, ebook_convert: str, stable_seconds: float) -> None:
    name = cbz_path.name
    try:
        log_sync(f"vérification de la stabilité du transfert « {name} »...")
        if not wait_until_stable(cbz_path, stable_seconds=stable_seconds):
            log_fail(f"« {name} » instable ou disparu — abandon.")
            return

        if not cbz_path.exists():
            log_fail(f"« {name} » n'existe plus — abandon.")
            return

        mobi_path = out_dir / (cbz_path.stem + ".mobi")
        log_conv(f"conversion en cours « {name} » ➜ « {mobi_path.name} »...")

        t0 = time.time()
        result = subprocess.run(
            [ebook_convert, str(cbz_path), str(mobi_path)],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        elapsed = time.time() - t0

        if result.returncode == 0:
            log_ok(f"« {mobi_path.name} » généré en {elapsed:.2f}s.")
            try:
                cbz_path.unlink()
                log_ok(f"original « {name} » supprimé.")
            except OSError as e:
                log_fail(f"suppression de « {name} » impossible : {e}")
            print(DONE_BADGE)
        else:
            log_fail(f"échec Calibre sur « {name} » (code {result.returncode}) après {elapsed:.2f}s.")
            stderr_tail = (result.stderr or "").strip().splitlines()[-5:]
            for line in stderr_tail:
                log_fail(f"    {line}")

    except Exception as e:
        log_fail(f"exception inattendue sur « {name} » : {e}")


# ═══════════════════════════════════════════════════════════════════════════
#  HANDLER WATCHDOG & WORKER
# ═══════════════════════════════════════════════════════════════════════════

class CBZHandler(FileSystemEventHandler):
    def __init__(self, work_queue: "queue.Queue[Path]"):
        super().__init__()
        self.work_queue = work_queue
        self._processing = set()
        self._lock = threading.Lock()

    def _handle(self, path_str: str) -> None:
        try:
            path = Path(path_str)
            if path.suffix.lower() != ".cbz":
                return
            
            key = str(path.resolve())
            with self._lock:
                if key in self._processing:
                    return
                self._processing.add(key)

            log_scan(f"nouveau fichier détecté « {path.name} ».")
            self.work_queue.put(path)
        except Exception as e:
            log_fail(f"exception handler : {e}")

    def release(self, path: Path) -> None:
        key = str(path.resolve())
        with self._lock:
            self._processing.discard(key)

    def on_created(self, event):
        if not event.is_directory:
            self._handle(event.src_path)

    def on_moved(self, event):
        if not event.is_directory:
            self._handle(event.dest_path)


def worker_loop(work_queue: "queue.Queue[Path]", out_dir: Path,
                ebook_convert: str, stable_seconds: float, handler: CBZHandler) -> None:
    while True:
        cbz_path = work_queue.get()
        if cbz_path is None:
            break
        try:
            convert_file(cbz_path, out_dir, ebook_convert, stable_seconds)
        except Exception as e:
            log_fail(f"exception worker : {e}")
        finally:
            handler.release(cbz_path)  # Libère le nom pour de futurs dépôts
            work_queue.task_done()


# ═══════════════════════════════════════════════════════════════════════════
#  MAIN
# ═══════════════════════════════════════════════════════════════════════════

def main() -> None:
    parser = argparse.ArgumentParser(description="CBZ -> MOBI auto converter")
    parser.add_argument("--in", dest="in_dir", default="Entree_CBZ")
    parser.add_argument("--out", dest="out_dir", default="Sortie_MOBI")
    parser.add_argument("--stable", dest="stable_seconds", type=float, default=1.5)
    args = parser.parse_args()

    print(BANNER)

    # 1. Ancrage strict par rapport à l'emplacement du script Python
    script_dir = Path(__file__).parent.resolve()
    in_dir = (script_dir / args.in_dir).resolve()
    out_dir = (script_dir / args.out_dir).resolve()
    
    in_dir.mkdir(parents=True, exist_ok=True)
    out_dir.mkdir(parents=True, exist_ok=True)

    ebook_convert = resolve_ebook_convert()

    log_scan(f"dossier surveillé : {in_dir}")
    log_scan(f"dossier de sortie : {out_dir}")
    log_scan(f"binaire Calibre   : {ebook_convert}")
    print(f"{C.GREY}{'─' * 78}{C.RESET}")

    work_queue: "queue.Queue[Path]" = queue.Queue()
    handler = CBZHandler(work_queue)

    worker = threading.Thread(
        target=worker_loop,
        args=(work_queue, out_dir, ebook_convert, args.stable_seconds, handler),
        daemon=True,
    )
    worker.start()

    # 2. Recherche récursive (rglob) au démarrage dans les sous-dossiers
    for existing in sorted(in_dir.rglob("*")):
        if existing.suffix.lower() == ".cbz":
            log_scan(f"fichier pré-existant détecté « {existing.name} ».")
            work_queue.put(existing)

    observer = Observer()
    # 3. Surveillance récursive (recursive=True) pour les nouveaux fichiers
    observer.schedule(handler, str(in_dir), recursive=True)
    observer.start()

    log_scan("veille active — dépose un .cbz dans le dossier pour lancer la conversion.")
    log_scan("[Ctrl+C] pour arrêter le daemon.")

    try:
        while True:
            time.sleep(0.5)
    except KeyboardInterrupt:
        print()
        log_scan("arrêt demandé — fermeture propre du daemon...")
    finally:
        observer.stop()
        observer.join()
        work_queue.put(None)
        worker.join(timeout=5)
        log_ok("daemon arrêté. À bientôt. o/")

if __name__ == "__main__":
    main()
