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 = {".doc", ".docx"}


def is_windows() -> bool:
    return sys.platform.startswith("win")


def unique_path(path: Path) -> Path:
    """Avoid overwrite by adding _1, _2, ..."""
    if not path.exists():
        return path
    stem = path.stem
    suffix = path.suffix
    parent = path.parent
    i = 1
    while True:
        candidate = parent / f"{stem}_{i}{suffix}"
        if not candidate.exists():
            return candidate
        i += 1


def parse_drop_files(data: str) -> List[Path]:
    """
    tkinterdnd2 gives a string that may include braces around paths with spaces.
    Example: '{C:/My Files/a.docx} {C:/b.doc}'
    """
    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_soffice_candidates() -> List[Path]:
    """
    Try to locate LibreOffice 'soffice' on Windows even if it's not in PATH.
    Returns a list of candidate Paths (existing files).
    """
    cands: List[Path] = []

    # 1) PATH
    for exe in ("soffice", "soffice.exe", "libreoffice", "libreoffice.exe"):
        p = shutil.which(exe)
        if p:
            cands.append(Path(p))

    # 2) Common install locations (Windows)
    if is_windows():
        common = [
            r"C:\Program Files\LibreOffice\program\soffice.exe",
            r"C:\Program Files (x86)\LibreOffice\program\soffice.exe",
        ]
        for s in common:
            cands.append(Path(s))

        # 3) Registry: App Paths (if present)
        try:
            import winreg  # type: ignore

            keys = [
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\soffice.exe"),
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\SOFFICE.EXE"),
            ]
            for root, k in keys:
                try:
                    with winreg.OpenKey(root, k) as hk:
                        val, _ = winreg.QueryValueEx(hk, None)
                        if val:
                            cands.append(Path(val))
                except Exception:
                    pass
        except Exception:
            pass

    # Filter existing files, deduplicate
    out: List[Path] = []
    seen = set()
    for p in cands:
        try:
            pp = p.resolve()
        except Exception:
            pp = p
        if pp.exists() and pp.is_file():
            s = str(pp).lower()
            if s not in seen:
                seen.add(s)
                out.append(pp)

    return out


class ConverterBackend:
    name = "base"

    def available(self) -> Tuple[bool, str]:
        """Return (available?, details)"""
        raise NotImplementedError

    def convert_to_pdf(self, src: Path, out_dir: Path) -> Path:
        raise NotImplementedError


class WordCOMBackend(ConverterBackend):
    name = "word"

    def __init__(self):
        self._ok_import = False
        self._win32 = None
        self._last_error = ""
        if is_windows():
            try:
                import win32com.client  # type: ignore
                self._win32 = win32com.client
                self._ok_import = True
            except Exception as e:
                self._ok_import = False
                self._last_error = f"Import pywin32 (win32com) impossible : {e}"

    def available(self) -> Tuple[bool, str]:
        if not is_windows():
            return False, "Non-Windows : backend Word COM indisponible."
        if not self._ok_import:
            return False, self._last_error or "pywin32 non importable dans cet environnement Python."
        try:
            app = self._win32.Dispatch("Word.Application")
            app.Quit()
            return True, "COM OK (Dispatch Word.Application)."
        except Exception as e:
            return False, f"COM Word inaccessible : {e}"

    def convert_to_pdf(self, src: Path, out_dir: Path) -> Path:
        out_dir.mkdir(parents=True, exist_ok=True)
        target = unique_path(out_dir / (src.stem + ".pdf"))

        word = None
        doc = None
        try:
            word = self._win32.Dispatch("Word.Application")
            word.Visible = False
            word.DisplayAlerts = 0  # wdAlertsNone

            # Options d'ouverture plus tolérantes
            # NB: certains paramètres varient selon versions Office ; on reste sur un set raisonnable.
            doc = word.Documents.Open(
                str(src),
                ReadOnly=True,
                AddToRecentFiles=False,
                ConfirmConversions=False,
                NoEncodingDialog=True,
                OpenAndRepair=True,
            )

            # wdExportFormatPDF = 17
            doc.ExportAsFixedFormat(
                OutputFileName=str(target),
                ExportFormat=17
            )

            if not target.exists():
                raise RuntimeError("Word n'a pas produit de PDF (fichier de sortie introuvable).")

            return target

        finally:
            try:
                if doc is not None:
                    doc.Close(False)
            except Exception:
                pass
            try:
                if word is not None:
                    word.Quit()
            except Exception:
                pass


class LibreOfficeBackend(ConverterBackend):
    name = "libreoffice"

    def __init__(self, soffice_path: Optional[str] = None):
        self._soffice: Optional[Path] = None
        self._details = ""

        if soffice_path:
            p = Path(soffice_path).expanduser()
            if p.exists() and p.is_file():
                self._soffice = p
                self._details = f"Utilisation du chemin sélectionné : {p}"
                return
            else:
                self._details = f"Chemin fourni invalide : {p}"

        cands = find_soffice_candidates()
        if cands:
            self._soffice = cands[0]
            self._details = f"Détecté : {self._soffice}"
        else:
            self._details = "Aucun 'soffice' détecté (PATH + emplacements standards)."

    def set_soffice(self, soffice_path: str) -> None:
        p = Path(soffice_path).expanduser()
        if p.exists() and p.is_file():
            self._soffice = p
            self._details = f"Utilisation du chemin sélectionné : {p}"
        else:
            self._soffice = None
            self._details = f"Chemin fourni invalide : {p}"

    def available(self) -> Tuple[bool, str]:
        if self._soffice is None:
            return False, self._details
        return True, self._details

    def convert_to_pdf(self, src: Path, out_dir: Path) -> Path:
        if self._soffice is None:
            raise RuntimeError("LibreOffice indisponible : 'soffice' introuvable (ou non configuré).")

        out_dir.mkdir(parents=True, exist_ok=True)

        cmd = [
            str(self._soffice),
            "--headless",
            "--nologo",
            "--nofirststartwizard",
            "--convert-to",
            "pdf",
            "--outdir",
            str(out_dir),
            str(src),
        ]
        result = subprocess.run(cmd, capture_output=True, text=True)

        if result.returncode != 0:
            raise RuntimeError(
                "LibreOffice a échoué.\n"
                f"Commande: {' '.join(cmd)}\n"
                f"STDOUT: {result.stdout}\n"
                f"STDERR: {result.stderr}"
            )

        produced = out_dir / (src.stem + ".pdf")
        if not produced.exists():
            # fallback search
            pdfs = list(out_dir.glob(src.stem + "*.pdf"))
            if not pdfs:
                raise RuntimeError(
                    "LibreOffice a indiqué une conversion réussie, "
                    "mais aucun PDF n'a été trouvé dans le dossier de sortie."
                )
            produced = pdfs[0]

        # anti-écrasement : renommer si besoin
        unique = unique_path(out_dir / (src.stem + ".pdf"))
        if unique != produced and produced.exists():
            try:
                produced.rename(unique)
                produced = unique
            except Exception:
                pass

        return produced


class App:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("DOC/DOCX → PDF (Drag & Drop)")

        self.files: List[Path] = []
        self.is_converting = False

        # UI variables
        self.output_mode = tk.StringVar(value="per_file_convert")
        self.backend_mode = tk.StringVar(value="auto")
        self.single_dir = tk.StringVar(value="")
        self.soffice_path = tk.StringVar(value="")

        # Backends
        self.backend_word = WordCOMBackend()
        self.backend_lo = LibreOfficeBackend()

        self._build_ui()
        self._setup_dnd()

        self._log("Prêt. Glissez-déposez des fichiers .doc/.docx, ou utilisez 'Ajouter…'.")
        self._log("Astuce : cliquez sur 'Diagnostics' si aucune conversion ne démarre.")

    def _build_ui(self):
        frm = ttk.Frame(self.root, padding=12)
        frm.pack(fill="both", expand=True)

        # Drop zone
        self.drop = ttk.LabelFrame(frm, text="Zone de dépôt", padding=10)
        self.drop.pack(fill="x", expand=False)

        self.drop_label = ttk.Label(
            self.drop,
            text="Glissez-déposez ici vos fichiers .doc / .docx",
            anchor="center"
        )
        self.drop_label.pack(fill="x")

        # File list
        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="Fichiers à 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")

        # Output options
        out = ttk.LabelFrame(frm, text="Sortie PDF", padding=10)
        out.pack(fill="x", expand=False)

        ttk.Radiobutton(
            out,
            text="Créer un sous-dossier /convert dans le dossier de chaque fichier",
            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")

        # Backend options
        bck = ttk.LabelFrame(frm, text="Moteur de conversion", padding=10)
        bck.pack(fill="x", expand=False, pady=(10, 0))

        ttk.Radiobutton(bck, text="Auto (recommandé)", variable=self.backend_mode, value="auto").pack(anchor="w")
        ttk.Radiobutton(bck, text="Microsoft Word (Windows + Word requis)", variable=self.backend_mode, value="word").pack(anchor="w")
        ttk.Radiobutton(bck, text="LibreOffice (soffice requis)", variable=self.backend_mode, value="libreoffice").pack(anchor="w")

        # LibreOffice path chooser
        lo = ttk.LabelFrame(frm, text="LibreOffice (optionnel) — chemin vers soffice.exe", padding=10)
        lo.pack(fill="x", expand=False, pady=(10, 0))

        lo_row = ttk.Frame(lo)
        lo_row.pack(fill="x")
        self.soffice_entry = ttk.Entry(lo_row, textvariable=self.soffice_path)
        self.soffice_entry.pack(side="left", fill="x", expand=True)
        ttk.Button(lo_row, text="Parcourir…", command=self.choose_soffice).pack(side="left", padx=(8, 0))
        ttk.Button(lo_row, text="Appliquer", command=self.apply_soffice).pack(side="left", padx=(8, 0))

        # Actions + log
        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("*"):
                    if f.suffix.lower() in SUPPORTED_EXTS and f.is_file():
                        added += self._add_one(f)
            else:
                added += self._add_one(p)
        if added:
            self._log(f"{added} fichier(s) ajouté(s).")
        else:
            self._log("Aucun fichier .doc/.docx valide n'a été 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 fichiers DOC/DOCX",
            filetypes=[("Documents Word", "*.doc *.docx")]
        )
        self._add_paths([Path(x) for x in fpaths])

    def remove_selected(self):
        sel = list(self.listbox.curselection())
        if not sel:
            return
        for idx in reversed(sel):
            self.listbox.delete(idx)
            del self.files[idx]
        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):
        mode = self.output_mode.get()
        self.dir_entry.configure(state="normal" if mode == "single_folder" else "disabled")

    def choose_soffice(self):
        initial = r"C:\Program Files\LibreOffice\program" if is_windows() else ""
        f = filedialog.askopenfilename(
            title="Choisir soffice.exe",
            initialdir=initial,
            filetypes=[("LibreOffice soffice", "soffice.exe" if is_windows() else "soffice"), ("Tous fichiers", "*.*")]
        )
        if f:
            self.soffice_path.set(f)

    def apply_soffice(self):
        p = self.soffice_path.get().strip()
        if not p:
            self.backend_lo = LibreOfficeBackend(None)
            self._log("Chemin LibreOffice vidé → retour à la détection automatique.")
            return
        self.backend_lo.set_soffice(p)
        ok, info = self.backend_lo.available()
        self._log(f"LibreOffice configuré : {'OK' if ok else 'NON'} — {info}")

    def _select_backend(self) -> ConverterBackend:
        mode = self.backend_mode.get()

        if mode == "word":
            ok, info = self.backend_word.available()
            if not ok:
                raise RuntimeError("Backend Word indisponible : " + info)
            return self.backend_word

        if mode == "libreoffice":
            ok, info = self.backend_lo.available()
            if not ok:
                raise RuntimeError("Backend LibreOffice indisponible : " + info)
            return self.backend_lo

        # auto
        ok_w, info_w = self.backend_word.available()
        if ok_w:
            return self.backend_word
        ok_l, info_l = self.backend_lo.available()
        if ok_l:
            return self.backend_lo

        raise RuntimeError(
            "Aucun moteur de conversion n'est disponible.\n"
            f"- Word : {info_w}\n"
            f"- LibreOffice : {info_l}\n"
            "Actions possibles :\n"
            "1) Vérifier que pywin32 est installé dans CE Python (voir Diagnostics)\n"
            "2) Ou sélectionner manuellement soffice.exe (section LibreOffice)."
        )

    def _get_out_dir_for_file(self, src: Path) -> Path:
        mode = self.output_mode.get()
        if mode == "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 .doc ou .docx.")
            return

        self.is_converting = True
        self.btn_convert.configure(state="disabled")
        self.progress.configure(text="Conversion en cours…")
        self._log("Démarrage de la conversion…")

        t = threading.Thread(target=self._convert_thread, daemon=True)
        t.start()

    def _convert_thread(self):
        ok = 0
        fail = 0
        try:
            backend = self._select_backend()
            self._log(f"Moteur utilisé : {backend.name}")

            total = len(self.files)
            for i, src in enumerate(self.files, start=1):
                try:
                    out_dir = self._get_out_dir_for_file(src)
                    self._ui_progress(f"{i}/{total} : {src.name}")
                    pdf = backend.convert_to_pdf(src, out_dir)
                    self._log(f"OK   : {src.name}  →  {pdf}")
                    ok += 1
                except Exception as e:
                    fail += 1
                    self._log(f"ECHEC: {src} ({type(e).__name__}: {e})")
                    self._log(traceback.format_exc())

        except Exception as e:
            self._log("ERREUR FATALE : " + str(e))
            self._log(traceback.format_exc())
        finally:
            self._ui_done(ok, fail)

    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}\nConsultez le 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 run_diagnostics(self):
        self._log("=== DIAGNOSTICS ===")
        self._log(f"Python executable : {sys.executable}")
        self._log(f"Python version    : {sys.version}")

        ok_w, info_w = self.backend_word.available()
        self._log(f"Word backend      : {'OK' if ok_w else 'NON'} — {info_w}")

        ok_l, info_l = self.backend_lo.available()
        self._log(f"LibreOffice       : {'OK' if ok_l else 'NON'} — {info_l}")

        if is_windows():
            self._log("Conseil : si Word=NON et que vous êtes sûr d'avoir pywin32, "
                      "c'est souvent que vous l'avez installé dans un autre Python que celui-ci.")
        self._log("===================")


def main():
    if TkinterDnD is not None:
        root = TkinterDnD.Tk()
    else:
        root = tk.Tk()

    try:
        ttk.Style().theme_use("clam")
    except Exception:
        pass

    App(root)
    root.geometry("980x720")
    root.mainloop()


if __name__ == "__main__":
    main()
