import os
import sys
import shutil
import subprocess
import threading
import traceback
from pathlib import Path
from typing import Optional, List, Tuple

import tkinter as tk
from tkinter import ttk, filedialog, messagebox

# Drag & Drop (tkinterdnd2)
try:
    from tkinterdnd2 import DND_FILES, TkinterDnD
except Exception:
    TkinterDnD = None
    DND_FILES = None

SUPPORTED_EXTS = {".epub"}


def is_windows() -> bool:
    return sys.platform.startswith("win")


def unique_path(path: Path) -> Path:
    if not path.exists():
        return path
    stem, suffix, parent = path.stem, path.suffix, path.parent
    i = 1
    while True:
        c = parent / f"{stem}_{i}{suffix}"
        if not c.exists():
            return c
        i += 1


def parse_drop_files(data: str) -> List[Path]:
    """
    tkinterdnd2 returns a string that may include { } around paths with spaces.
    Example: '{C:/My Files/a.epub} {C:/b.epub}'
    """
    items, buf, in_brace = [], "", False
    for ch in data:
        if ch == "{":
            in_brace = True
            buf = ""
        elif ch == "}":
            in_brace = False
            if buf.strip():
                items.append(buf.strip())
            buf = ""
        elif ch == " " and not in_brace:
            if buf.strip():
                items.append(buf.strip())
            buf = ""
        else:
            buf += ch
    if buf.strip():
        items.append(buf.strip())
    return [Path(x) for x in items]


def find_ebook_convert_candidates() -> List[Path]:
    """
    Locate Calibre's ebook-convert executable on Windows even if not in PATH.
    """
    cands: List[Path] = []

    # PATH
    for exe in ("ebook-convert", "ebook-convert.exe"):
        p = shutil.which(exe)
        if p:
            cands.append(Path(p))

    # Common Windows install location
    if is_windows():
        common = [
            r"C:\Program Files\Calibre2\ebook-convert.exe",
            r"C:\Program Files (x86)\Calibre2\ebook-convert.exe",
        ]
        cands.extend(Path(x) for x in common)

    # Filter existing + dedupe
    out: List[Path] = []
    seen = set()
    for p in cands:
        try:
            pp = p.resolve()
        except Exception:
            pp = p
        if pp.exists() and pp.is_file():
            key = str(pp).lower()
            if key not in seen:
                seen.add(key)
                out.append(pp)
    return out


class CalibreBackend:
    name = "calibre"

    def __init__(self, ebook_convert_path: Optional[str] = None):
        self._ebook_convert: Optional[Path] = None
        self._details = ""

        if ebook_convert_path:
            p = Path(ebook_convert_path).expanduser()
            if p.exists() and p.is_file():
                self._ebook_convert = p
                self._details = f"Utilisation du chemin sélectionné : {p}"
                return
            self._details = f"Chemin fourni invalide : {p}"

        cands = find_ebook_convert_candidates()
        if cands:
            self._ebook_convert = cands[0]
            self._details = f"Détecté : {self._ebook_convert}"
        else:
            self._details = "Aucun 'ebook-convert' détecté (PATH + emplacements standards)."

    def set_ebook_convert(self, pth: str) -> None:
        p = Path(pth).expanduser()
        if p.exists() and p.is_file():
            self._ebook_convert = p
            self._details = f"Utilisation du chemin sélectionné : {p}"
        else:
            self._ebook_convert = None
            self._details = f"Chemin invalide : {p}"

    def available(self) -> Tuple[bool, str]:
        if self._ebook_convert is None:
            return False, self._details
        return True, self._details

    def convert_to_pdf(
        self,
        src_epub: Path,
        out_dir: Path,
        page_size: str = "a4",
        margin_left: int = 36,
        margin_right: int = 36,
        margin_top: int = 36,
        margin_bottom: int = 36,
        base_font_size: int = 12,
    ) -> Path:
        """
        Convert EPUB to PDF using Calibre ebook-convert.
        page_size: e.g. a4, letter
        margins in pt (36 pt = 0.5 inch approx)
        """
        if self._ebook_convert is None:
            raise RuntimeError("Calibre indisponible : 'ebook-convert' introuvable (ou non configuré).")

        out_dir.mkdir(parents=True, exist_ok=True)
        src_epub = src_epub.resolve()
        target = unique_path(out_dir / (src_epub.stem + ".pdf"))

        cmd = [
            str(self._ebook_convert),
            str(src_epub),
            str(target),
            "--paper-size", page_size,
            "--margin-left", str(margin_left),
            "--margin-right", str(margin_right),
            "--margin-top", str(margin_top),
            "--margin-bottom", str(margin_bottom),
            "--base-font-size", str(base_font_size),
        ]

        result = subprocess.run(cmd, capture_output=True, text=True)

        if result.returncode != 0:
            raise RuntimeError(
                "Calibre (ebook-convert) a échoué.\n"
                f"Code retour: {result.returncode}\n"
                f"Commande: {' '.join(cmd)}\n"
                f"STDOUT: {result.stdout}\n"
                f"STDERR: {result.stderr}"
            )

        if not target.exists():
            raise RuntimeError("ebook-convert a terminé sans produire le PDF (fichier introuvable).")

        return target


class App:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("EPUB → PDF — Drag & Drop (Calibre)")

        self.files: List[Path] = []
        self.is_converting = False

        # UI vars
        self.output_mode = tk.StringVar(value="per_file_convert")  # per_file_convert / single_folder
        self.single_dir = tk.StringVar(value="")
        self.ebook_convert_path = tk.StringVar(value="")

        # PDF settings
        self.page_size = tk.StringVar(value="a4")
        self.base_font_size = tk.StringVar(value="12")

        self.backend = CalibreBackend()

        self._build_ui()
        self._setup_dnd()

        self._log("Prêt. Déposez des EPUB, ou utilisez 'Ajouter…'.")
        ok, info = self.backend.available()
        self._log(f"Calibre : {'OK' if ok else 'NON'} — {info}")

    def _build_ui(self):
        frm = ttk.Frame(self.root, padding=12)
        frm.pack(fill="both", expand=True)

        drop = ttk.LabelFrame(frm, text="Zone de dépôt", padding=10)
        drop.pack(fill="x")

        self.drop_label = ttk.Label(drop, text="Glissez-déposez ici vos fichiers .epub", anchor="center")
        self.drop_label.pack(fill="x")

        mid = ttk.Frame(frm)
        mid.pack(fill="both", expand=True, pady=(10, 10))

        left = ttk.Frame(mid)
        left.pack(side="left", fill="both", expand=True)

        ttk.Label(left, text="EPUB à convertir :").pack(anchor="w")
        self.listbox = tk.Listbox(left, height=10)
        self.listbox.pack(fill="both", expand=True, pady=(4, 0))

        btns = ttk.Frame(mid)
        btns.pack(side="right", fill="y", padx=(10, 0))
        ttk.Button(btns, text="Ajouter…", command=self.add_files).pack(fill="x")
        ttk.Button(btns, text="Retirer sélection", command=self.remove_selected).pack(fill="x", pady=6)
        ttk.Button(btns, text="Vider la liste", command=self.clear_list).pack(fill="x")

        out = ttk.LabelFrame(frm, text="Sortie PDF", padding=10)
        out.pack(fill="x")

        ttk.Radiobutton(
            out,
            text="Créer un sous-dossier /convert à côté de chaque EPUB",
            variable=self.output_mode, value="per_file_convert",
            command=self._refresh_output_state
        ).pack(anchor="w")

        row2 = ttk.Frame(out)
        row2.pack(fill="x", pady=(6, 0))
        ttk.Radiobutton(
            row2,
            text="Créer tous les PDF dans un dossier unique :",
            variable=self.output_mode, value="single_folder",
            command=self._refresh_output_state
        ).pack(side="left")

        self.dir_entry = ttk.Entry(row2, textvariable=self.single_dir)
        self.dir_entry.pack(side="left", fill="x", expand=True, padx=6)
        ttk.Button(row2, text="Choisir…", command=self.choose_output_dir).pack(side="left")

        # Calibre path
        calib = ttk.LabelFrame(frm, text="Calibre — chemin vers ebook-convert.exe (optionnel)", padding=10)
        calib.pack(fill="x", pady=(10, 0))
        rowc = ttk.Frame(calib)
        rowc.pack(fill="x")
        ttk.Entry(rowc, textvariable=self.ebook_convert_path).pack(side="left", fill="x", expand=True)
        ttk.Button(rowc, text="Parcourir…", command=self.choose_ebook_convert).pack(side="left", padx=(8, 0))
        ttk.Button(rowc, text="Appliquer", command=self.apply_ebook_convert).pack(side="left", padx=(8, 0))

        # Settings
        st = ttk.LabelFrame(frm, text="Réglages PDF (simples)", padding=10)
        st.pack(fill="x", pady=(10, 0))

        r1 = ttk.Frame(st); r1.pack(fill="x")
        ttk.Label(r1, text="Format papier (a4/letter) :").pack(side="left")
        ttk.Entry(r1, width=10, textvariable=self.page_size).pack(side="left", padx=8)

        r2 = ttk.Frame(st); r2.pack(fill="x", pady=(6, 0))
        ttk.Label(r2, text="Taille de police de base :").pack(side="left")
        ttk.Entry(r2, width=10, textvariable=self.base_font_size).pack(side="left", padx=8)

        actions = ttk.Frame(frm)
        actions.pack(fill="x", pady=(10, 0))

        self.btn_convert = ttk.Button(actions, text="Convertir", command=self.start_convert)
        self.btn_convert.pack(side="left")

        ttk.Button(actions, text="Diagnostics", command=self.run_diagnostics).pack(side="left", padx=8)

        self.progress = ttk.Label(actions, text="")
        self.progress.pack(side="left", padx=10)

        logf = ttk.LabelFrame(frm, text="Journal", padding=10)
        logf.pack(fill="both", expand=True, pady=(10, 0))

        self.log = tk.Text(logf, height=12, wrap="word")
        self.log.pack(fill="both", expand=True)

        self._refresh_output_state()

    def _setup_dnd(self):
        if TkinterDnD is None:
            self._log("Drag & Drop indisponible : installez tkinterdnd2 (pip install tkinterdnd2).")
            return
        self.drop_label.drop_target_register(DND_FILES)
        self.drop_label.dnd_bind("<<Drop>>", self._on_drop)

    def _on_drop(self, event):
        paths = parse_drop_files(event.data)
        self._add_paths(paths)

    def _add_paths(self, paths: List[Path]):
        added = 0
        for p in paths:
            if p.is_dir():
                for f in p.rglob("*.epub"):
                    if f.is_file():
                        added += self._add_one(f)
            else:
                added += self._add_one(p)
        self._log(f"{added} fichier(s) ajouté(s)." if added else "Aucun EPUB valide ajouté.")

    def _add_one(self, p: Path) -> int:
        if not p.exists() or not p.is_file():
            return 0
        if p.suffix.lower() not in SUPPORTED_EXTS:
            return 0
        p = p.resolve()
        if p in self.files:
            return 0
        self.files.append(p)
        self.listbox.insert(tk.END, str(p))
        return 1

    def add_files(self):
        fpaths = filedialog.askopenfilenames(title="Choisir des EPUB", filetypes=[("EPUB", "*.epub")])
        self._add_paths([Path(x) for x in fpaths])

    def remove_selected(self):
        sel = list(self.listbox.curselection())
        for idx in reversed(sel):
            self.listbox.delete(idx)
            del self.files[idx]
        if sel:
            self._log("Sélection retirée.")

    def clear_list(self):
        self.listbox.delete(0, tk.END)
        self.files.clear()
        self._log("Liste vidée.")

    def choose_output_dir(self):
        d = filedialog.askdirectory(title="Choisir le dossier de sortie")
        if d:
            self.single_dir.set(d)
            self.output_mode.set("single_folder")
            self._refresh_output_state()

    def _refresh_output_state(self):
        self.dir_entry.configure(state="normal" if self.output_mode.get() == "single_folder" else "disabled")

    def choose_ebook_convert(self):
        initial = r"C:\Program Files\Calibre2" if is_windows() else ""
        f = filedialog.askopenfilename(
            title="Choisir ebook-convert.exe",
            initialdir=initial,
            filetypes=[("Calibre ebook-convert", "ebook-convert.exe" if is_windows() else "ebook-convert"),
                       ("Tous fichiers", "*.*")]
        )
        if f:
            self.ebook_convert_path.set(f)

    def apply_ebook_convert(self):
        p = self.ebook_convert_path.get().strip()
        if not p:
            self.backend = CalibreBackend(None)
            ok, info = self.backend.available()
            self._log(f"Calibre : {'OK' if ok else 'NON'} — {info}")
            return
        self.backend.set_ebook_convert(p)
        ok, info = self.backend.available()
        self._log(f"Calibre configuré : {'OK' if ok else 'NON'} — {info}")

    def run_diagnostics(self):
        self._log("=== DIAGNOSTICS ===")
        self._log(f"Python : {sys.executable}")
        self._log(f"Version: {sys.version}")
        ok, info = self.backend.available()
        self._log(f"Calibre : {'OK' if ok else 'NON'} — {info}")
        self._log("===================")

    def _get_out_dir_for_file(self, src: Path) -> Path:
        if self.output_mode.get() == "per_file_convert":
            return src.parent / "convert"
        d = self.single_dir.get().strip()
        if not d:
            raise RuntimeError("Veuillez choisir un dossier de sortie (mode dossier unique).")
        return Path(d)

    def start_convert(self):
        if self.is_converting:
            return
        if not self.files:
            messagebox.showwarning("Aucun fichier", "Veuillez ajouter au moins un fichier EPUB.")
            return

        ok, info = self.backend.available()
        if not ok:
            messagebox.showerror(
                "Calibre introuvable",
                "Calibre (ebook-convert) n'est pas accessible.\n"
                "Installez Calibre ou sélectionnez ebook-convert.exe via 'Parcourir…'.\n\n"
                f"Détail: {info}"
            )
            return

        self.is_converting = True
        self.btn_convert.configure(state="disabled")
        self.progress.configure(text="Conversion en cours…")
        self._log("Démarrage conversion…")

        threading.Thread(target=self._convert_thread, daemon=True).start()

    def _convert_thread(self):
        ok_count = 0
        fail_count = 0
        try:
            total = len(self.files)
            for i, src in enumerate(self.files, start=1):
                try:
                    out_dir = self._get_out_dir_for_file(src)

                    # Read settings
                    page_size = (self.page_size.get().strip() or "a4").lower()
                    try:
                        base_font = int(self.base_font_size.get().strip() or "12")
                    except ValueError:
                        base_font = 12

                    self._ui_progress(f"{i}/{total} : {src.name}")
                    self._log(f"---\nSource: {src}\nSortie dossier: {out_dir}\nParamètres: paper={page_size}, base_font={base_font}")

                    pdf = self.backend.convert_to_pdf(
                        src, out_dir,
                        page_size=page_size,
                        base_font_size=base_font
                    )

                    self._log(f"OK   : {src.name} → {pdf}")
                    ok_count += 1

                except Exception as e:
                    fail_count += 1
                    self._log(f"ECHEC: {src} ({type(e).__name__}: {e})")
                    self._log(traceback.format_exc())

        except Exception as e:
            self._log(f"ERREUR FATALE: {e}")
            self._log(traceback.format_exc())
        finally:
            self._ui_done(ok_count, fail_count)

    def _ui_progress(self, text: str):
        self.root.after(0, lambda: self.progress.configure(text=text))

    def _ui_done(self, ok: int, fail: int):
        def done():
            self.is_converting = False
            self.btn_convert.configure(state="normal")
            self.progress.configure(text=f"Terminé — OK: {ok}, Échecs: {fail}")
            if fail == 0:
                messagebox.showinfo("Conversion terminée", f"Conversion terminée.\nOK: {ok}")
            else:
                messagebox.showwarning("Conversion terminée", f"Conversion terminée.\nOK: {ok}\nÉchecs: {fail}\nVoir journal.")
        self.root.after(0, done)

    def _log(self, msg: str):
        self.root.after(0, lambda: self._append_log(msg))

    def _append_log(self, msg: str):
        self.log.insert(tk.END, msg + "\n")
        self.log.see(tk.END)


def main():
    root = TkinterDnD.Tk() if TkinterDnD is not None else tk.Tk()
    try:
        ttk.Style().theme_use("clam")
    except Exception:
        pass
    App(root)
    root.geometry("980x720")
    root.mainloop()


if __name__ == "__main__":
    main()
