#!/usr/bin/env python3
"""
build.py – Compilation + installation de TranscribeStation
Produit un binaire onefile PyInstaller et installe les raccourcis système.

Usage :
    python build.py                  # build + install (plateforme courante)
    python build.py --linux          # build Linux
    python build.py --windows        # build Windows (Wine ou .bat natif)
    python build.py --both           # Linux + Windows
    python build.py --install-only   # installe sans recompiler
    python build.py --clean          # nettoyer build/ dist/ *.spec icon.*

Raccourcis créés :
    Linux   → ~/.local/share/applications/TranscribeStation.desktop
              ~/.local/share/icons/hicolor/256x256/apps/TranscribeStation.png
              ~/Bureau/TranscribeStation.desktop  (si dossier existe)
    Windows → %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\TranscribeStation.lnk
              %USERPROFILE%\\Desktop\\TranscribeStation.lnk
"""

import sys
import os
import shutil
import platform
import subprocess
import argparse
import textwrap
import urllib.request
import zipfile
import tempfile
from pathlib import Path

# ─── Configuration ────────────────────────────────────────────────────────────
APP_NAME    = "TranscribeStation"
APP_VERSION = "1.2.0"
APP_AUTHOR  = "H3Campus"
APP_URL     = "https://github.com/h3campus/transcribe-station"
APP_COMMENT = "Gestionnaire de transcription audio – JT-Tools by Johnny"
APP_COMMENT_WIN = "Gestionnaire de transcription audio - JT-Tools by Johnny"
APP_CATS    = "Audio;AudioVideo;Office;"
MAIN_SCRIPT = "transcribe_station.py"
ICON_ICO    = Path("icon.ico")
ICON_PNG    = Path("icon.png")
BUILD_DIR   = Path("build")
DIST_DIR    = Path("dist")

HIDDEN_IMPORTS = [
    "PySide6.QtMultimedia",
    "PySide6.QtCore",
    "PySide6.QtGui",
    "PySide6.QtWidgets",
    "soundfile",
    "numpy",
    "hid",
    "cffi",
    "_cffi_backend",
]

# --collect-all hid : inclut la DLL native hidapi.dll dans le bundle Windows
COLLECT_ALL = ["hid"]

ADD_DATA: list[str] = []

# hidapi.dll natif Windows (absent du package pip hid 1.x qui est un pur wrapper ctypes)
HIDAPI_VERSION = "0.14.0"
HIDAPI_DLL_URL = (
    f"https://github.com/libusb/hidapi/releases/download/"
    f"hidapi-{HIDAPI_VERSION}/hidapi-win.zip"
)

# ─── Couleurs terminal ─────────────────────────────────────────────────────────
R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'
C='\033[0;36m'; B='\033[1m'; N='\033[0m'
def ok(m):  print(f"{G}[OK]{N}    {m}")
def info(m):print(f"{C}[..]{N}    {m}")
def warn(m):print(f"{Y}[!!]{N}    {m}")
def err(m): print(f"{R}[XX]{N}    {m}", file=sys.stderr)
def step(m):print(f"\n{B}{m}{N}")

# ─── hidapi.dll ───────────────────────────────────────────────────────────────
def _fetch_hidapi_dll() -> Path | None:
    """Télécharge hidapi.dll (x64) depuis les releases GitHub, mise en cache dans build/."""
    dll_cache = BUILD_DIR / "hidapi.dll"
    if dll_cache.exists():
        ok(f"hidapi.dll en cache : {dll_cache}")
        return dll_cache
    info(f"Téléchargement hidapi.dll v{HIDAPI_VERSION} depuis GitHub...")
    BUILD_DIR.mkdir(parents=True, exist_ok=True)
    try:
        with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
            tmp_path = tmp.name
        urllib.request.urlretrieve(HIDAPI_DLL_URL, tmp_path)
        with zipfile.ZipFile(tmp_path) as zf:
            candidates = [n for n in zf.namelist()
                          if n.lower().endswith("hidapi.dll") and "x64" in n.lower()]
            if not candidates:
                candidates = [n for n in zf.namelist()
                              if n.lower().endswith("hidapi.dll")]
            if candidates:
                with zf.open(candidates[0]) as src, open(dll_cache, "wb") as dst:
                    dst.write(src.read())
                ok(f"hidapi.dll extrait ({candidates[0]}) → {dll_cache}")
                return dll_cache
            warn("hidapi.dll introuvable dans l'archive")
    except Exception as exc:
        warn(f"Échec téléchargement hidapi.dll : {exc}")
    finally:
        try:
            os.unlink(tmp_path)
        except Exception:
            pass
    return None


def _install_hidapi_dll_onedir(internal_dir: Path) -> None:
    """Copie hidapi.dll dans _internal/ d'un bundle onedir."""
    if not internal_dir.exists():
        warn(f"Dossier _internal introuvable : {internal_dir}")
        return
    dll = _fetch_hidapi_dll()
    if dll:
        dest = internal_dir / "hidapi.dll"
        shutil.copy2(dll, dest)
        ok(f"hidapi.dll installé → {dest}")
    else:
        warn("hidapi.dll non disponible — le pédalier ne fonctionnera pas sans cette DLL.")


# ─── Helpers ──────────────────────────────────────────────────────────────────
def run(cmd: list, check=True) -> int:
    info(" ".join(str(c) for c in cmd))
    r = subprocess.run(cmd, check=False)
    if check and r.returncode != 0:
        err(f"Échec (code {r.returncode})")
        sys.exit(r.returncode)
    return r.returncode

def pip_install(pkgs: list, python=sys.executable):
    run([python, "-m", "pip", "install", "--quiet"] + pkgs)

def validate_wine_pyside(wine: str, wine_python: str) -> bool:
    probe = subprocess.run(
        [
            wine, wine_python, "-c",
            (
                "from pathlib import Path; "
                "import importlib.metadata as md; "
                "import PySide6; "
                "qt_dir = Path(PySide6.__file__).resolve().parent; "
                "missing = [name for name in ('Qt6Core.dll', 'Qt6Multimedia.dll') if not (qt_dir / name).exists()]; "
                "print('PYSIDE6_VERSION=' + md.version('PySide6')); "
                "print('QT_DIR=' + str(qt_dir).replace(chr(92), '/')); "
                "print('MISSING_DLLS=' + ','.join(missing)); "
                "from PySide6 import QtCore; "
                "print('QTCORE_OK=1')"
            ),
        ],
        check=False,
        capture_output=True,
        text=True,
    )
    output = (probe.stdout or "") + (probe.stderr or "")
    for line in output.splitlines():
        if line.strip():
            info(f"Wine Qt probe : {line}")
    if probe.returncode != 0:
        missing = ""
        for line in output.splitlines():
            if line.startswith("MISSING_DLLS="):
                missing = line.split("=", 1)[1].strip()
                break
        err("PySide6 est installé dans Wine, mais QtCore ne se charge pas.")
        if missing:
            err(f"DLL Qt manquantes dans le wheel Windows : {missing}")
        err("Le binaire Windows généré via Wine serait invalide.")
        err("Utilisez le script build_windows.bat sur Windows natif.")
        return False
    return True

# ─── Génération d'icônes ──────────────────────────────────────────────────────
def _draw_icon(draw, sz):
    """Dessine le logo waveform TranscribeStation."""
    draw.rounded_rectangle(
        [0, 0, sz - 1, sz - 1],
        radius=int(sz * 0.18),
        fill=(59, 110, 220, 255),
    )
    bars = [0.25, 0.45, 0.70, 0.90, 0.65, 1.0, 0.55, 0.80, 0.40, 0.20]
    n, mg = len(bars), sz * 0.13
    bw  = (sz - 2 * mg) / (n * 1.7)
    gap = (sz - 2 * mg) / n
    mid = sz / 2
    for i, h in enumerate(bars):
        x  = mg + i * gap + (gap - bw) / 2
        bh = h * sz * 0.36
        draw.rounded_rectangle(
            [x, mid - bh, x + bw, mid + bh],
            radius=bw / 2,
            fill=(255, 255, 255, 215),
        )

def generate_icons() -> bool:
    """Génère icon.png (256px) et icon.ico (multi-tailles). Retourne True si OK."""
    try:
        from PIL import Image, ImageDraw
    except ImportError:
        warn("Pillow absent → icônes ignorées  (pip install Pillow)")
        return False

    sizes_ico = [16, 32, 48, 64, 128, 256]
    images = []
    for sz in sizes_ico:
        img  = Image.new("RGBA", (sz, sz), (0, 0, 0, 0))
        _draw_icon(ImageDraw.Draw(img), sz)
        images.append(img)

    # PNG 256 px pour Linux XDG
    images[-1].save(str(ICON_PNG))
    ok(f"Icône PNG : {ICON_PNG}")

    # ICO multi-tailles pour Windows
    images[0].save(
        str(ICON_ICO), format="ICO",
        sizes=[(s, s) for s in sizes_ico],
        append_images=images[1:],
    )
    ok(f"Icône ICO : {ICON_ICO}")
    return True

# ─── Raccourci GNOME / Linux ──────────────────────────────────────────────────
def install_linux_shortcut(binary: Path):
    step("📌  Installation du raccourci GNOME...")

    binary = binary.resolve()

    # 1. Icône → XDG hicolor
    xdg_icon_dir = Path.home() / ".local/share/icons/hicolor/256x256/apps"
    xdg_icon_dir.mkdir(parents=True, exist_ok=True)
    xdg_icon = xdg_icon_dir / f"{APP_NAME}.png"
    if ICON_PNG.exists():
        shutil.copy(ICON_PNG, xdg_icon)
        ok(f"Icône installée : {xdg_icon}")
    else:
        warn("icon.png absent — raccourci sans icône")

    # 2. Fichier .desktop → applications
    apps_dir = Path.home() / ".local/share/applications"
    apps_dir.mkdir(parents=True, exist_ok=True)
    desktop_path = apps_dir / f"{APP_NAME}.desktop"

    desktop_content = textwrap.dedent(f"""\
        [Desktop Entry]
        Version=1.0
        Type=Application
        Name={APP_NAME}
        GenericName=Transcription Audio
        Comment={APP_COMMENT}
        Exec={binary}
        Icon={APP_NAME}
        Terminal=false
        Categories={APP_CATS}
        StartupNotify=true
        StartupWMClass={APP_NAME}
        Keywords=transcription;audio;dictée;pédalier;
    """)
    desktop_path.write_text(desktop_content, encoding="utf-8")
    desktop_path.chmod(0o755)
    ok(f".desktop installé : {desktop_path}")

    # 3. Raccourci Bureau (Desktop)
    for bureau in ["Bureau", "Desktop", "Escritorio"]:
        bureau_dir = Path.home() / bureau
        if bureau_dir.is_dir():
            bureau_link = bureau_dir / f"{APP_NAME}.desktop"
            shutil.copy(desktop_path, bureau_link)
            bureau_link.chmod(0o755)
            ok(f"Raccourci bureau : {bureau_link}")
            break

    # 4. Mettre à jour la base des .desktop
    for cmd in [
        ["update-desktop-database", str(apps_dir)],
        ["gtk-update-icon-cache", "-f", "-t",
         str(Path.home() / ".local/share/icons/hicolor")],
        ["xdg-desktop-menu", "forceupdate"],
    ]:
        if shutil.which(cmd[0]):
            run(cmd, check=False)

    ok("Raccourci GNOME installé. Visible au prochain rechargement de session.")

def uninstall_linux_shortcut():
    step("🗑  Désinstallation raccourci GNOME...")
    targets = [
        Path.home() / f".local/share/applications/{APP_NAME}.desktop",
        Path.home() / f".local/share/icons/hicolor/256x256/apps/{APP_NAME}.png",
    ]
    for bureau in ["Bureau", "Desktop", "Escritorio"]:
        targets.append(Path.home() / bureau / f"{APP_NAME}.desktop")
    for t in targets:
        if t.exists():
            t.unlink()
            ok(f"Supprimé : {t}")

# ─── Raccourci Windows (depuis Windows natif) ─────────────────────────────────
def install_windows_shortcut(binary: Path):
    """Crée Menu Démarrer + Bureau via PowerShell (à appeler depuis Windows)."""
    step("📌  Installation du raccourci Windows...")
    binary = binary.resolve()

    # param() en premiere ligne : ExePath est passe par le bat via -ExePath "..."
    # Ne PAS hardcoder $ExePath dans le corps du script (serait ecrase par la ligne param)
    ps_script = textwrap.dedent(f"""
        param([string]$ExePath)
        $AppName = "{APP_NAME}"
        $Comment = "{APP_COMMENT_WIN}"

        function New-Shortcut([string]$dest) {{
            $wsh = New-Object -ComObject WScript.Shell
            $lnk = $wsh.CreateShortcut($dest)
            $lnk.TargetPath       = $ExePath
            $lnk.IconLocation     = "$ExePath,0"
            $lnk.Description      = $Comment
            $lnk.WorkingDirectory = [System.IO.Path]::GetDirectoryName($ExePath)
            $lnk.Save()
            Write-Host "[OK] Raccourci : $dest"
        }}

        $startMenu = "$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs"
        New-Shortcut "$startMenu\\$AppName.lnk"

        $desktop = [Environment]::GetFolderPath("Desktop")
        New-Shortcut "$desktop\\$AppName.lnk"

        Write-Host ""
        Write-Host "[OK] Shortcuts installed successfully."
    """).strip()

    ps_file = DIST_DIR / "windows" / "install_shortcut.ps1"
    ps_file.parent.mkdir(parents=True, exist_ok=True)
    ps_file.write_text(ps_script, encoding="utf-8-sig")
    ok(f"Script PowerShell généré : {ps_file}")

    if platform.system() == "Windows":
        info("Exécution de PowerShell...")
        run(["powershell", "-ExecutionPolicy", "Bypass",
             "-File", str(ps_file)], check=False)
    else:
        warn("Cross-compilation : exécutez ce script sur la machine Windows cible :")
        warn(f"  powershell -ExecutionPolicy Bypass -File {ps_file}")

# ─── Build Linux ──────────────────────────────────────────────────────────────
def build_linux(install: bool = True):
    step("🐧  Build Linux")
    pip_install(["pyinstaller", "Pillow"])
    generate_icons()

    run([sys.executable, "-m", "PyInstaller",
         "--onefile", "--clean", "--noconfirm",
         f"--name={APP_NAME}",
         "--distpath=dist/linux",
         "--workpath=build/linux",
         "--specpath=build/linux",
         "--log-level=WARN",
         *(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
         *(f"--collect-all={m}" for m in COLLECT_ALL),
         *(f"--add-data={d}" for d in ADD_DATA),
         f"--icon={ICON_ICO}" if ICON_ICO.exists() else "",
         MAIN_SCRIPT,
    ])

    binary = Path(f"dist/linux/{APP_NAME}")
    if binary.exists():
        binary.chmod(0o755)
        size = binary.stat().st_size // (1024 * 1024)
        ok(f"Binaire Linux : {binary}  ({size} Mo)")
        if install:
            install_linux_shortcut(binary)
    else:
        err("Binaire introuvable dans dist/linux/")

# ─── Build Windows ────────────────────────────────────────────────────────────
def build_windows(install: bool = True):
    step("🪟  Build Windows")

    # ── Sur machine Windows native ────────────────────────────────────────────
    if platform.system() == "Windows":
        pip_install(["pyinstaller", "Pillow"])
        generate_icons()
        icon_arg = f"--icon={ICON_ICO.resolve()}" if ICON_ICO.exists() else ""
        main_script = str(Path(MAIN_SCRIPT).resolve())
        run([sys.executable, "-m", "PyInstaller",
             "--onedir", "--clean", "--noconfirm", "--windowed",
             f"--name={APP_NAME}",
             "--distpath=dist/windows",
             "--workpath=build/windows",
             "--specpath=build/windows",
             "--log-level=WARN",
             *(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
             *(f"--collect-all={m}" for m in COLLECT_ALL),
             icon_arg,
             main_script,
        ])
        binary = Path(f"dist/windows/{APP_NAME}/{APP_NAME}.exe")
        if binary.exists():
            ok(f"Binaire Windows : {binary}")
            _install_hidapi_dll_onedir(Path(f"dist/windows/{APP_NAME}/_internal"))
            if install:
                install_windows_shortcut(binary)
        else:
            err("Binaire introuvable.")
        return

    # ── Cross-compilation depuis Linux via Wine ────────────────────────────────
    wine = shutil.which("wine") or shutil.which("wine64")
    wine_python = None
    if wine:
        for candidate in [
            Path.home() / ".wine/drive_c/Python312/python.exe",
            Path.home() / ".wine/drive_c/Python311/python.exe",
            Path.home() / ".wine/drive_c/Python310/python.exe",
        ]:
            if candidate.exists():
                wine_python = candidate
                break

    if wine and wine_python:
        info(f"Wine Python : {wine_python}")
        pip_install(["pyinstaller", "PySide6", "numpy", "soundfile", "hid", "Pillow"],
                    python=str(wine_python))
        generate_icons()
        if not validate_wine_pyside(wine, str(wine_python)):
            sys.exit(1)
        # Récupérer hidapi.dll pour l'embarquer dans le onefile via --add-binary
        hidapi_dll = _fetch_hidapi_dll()
        add_binary_args = [f"--add-binary={hidapi_dll}:."] if hidapi_dll else []
        if not hidapi_dll:
            warn("hidapi.dll non disponible — le pédalier ne fonctionnera pas dans le .exe Wine")
        run([wine, str(wine_python), "-m", "PyInstaller",
             "--onefile", "--clean", "--noconfirm", "--windowed",
             f"--name={APP_NAME}",
             "--distpath=dist/windows",
             "--workpath=build/windows",
             "--specpath=build/windows",
             "--log-level=WARN",
             *(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
             *(f"--collect-all={m}" for m in COLLECT_ALL),
             *add_binary_args,
             f"--icon={ICON_ICO}" if ICON_ICO.exists() else "",
             MAIN_SCRIPT,
        ])
        binary = Path(f"dist/windows/{APP_NAME}.exe")
        if binary.exists():
            size = binary.stat().st_size // (1024 * 1024)
            ok(f"Binaire Windows : {binary}  ({size} Mo)")
        install_windows_shortcut(Path(f"dist/windows/{APP_NAME}.exe"))
    else:
        warn("Wine ou Python Windows introuvable → génération du script natif Windows")
        _write_windows_bat(install=install)

def _write_windows_bat(install: bool = True):
    """Genere build_windows.bat + _build_icons.py dans dist/ pour execution native sur Windows."""
    out_dir = Path("dist") / "windows"
    out_dir.mkdir(parents=True, exist_ok=True)
    bat_path   = out_dir / "build_windows.bat"
    icons_path = out_dir / "_build_icons.py"

    # ── Script Python icones dans un fichier separe (evite l'interpretation batch) ──
    icon_py = (
        "from PIL import Image, ImageDraw\n"
        "sizes = [16, 32, 48, 64, 128, 256]\n"
        "imgs = []\n"
        "for sz in sizes:\n"
        "    img = Image.new('RGBA', (sz, sz), (0, 0, 0, 0))\n"
        "    d = ImageDraw.Draw(img)\n"
        "    d.rounded_rectangle([0, 0, sz-1, sz-1], radius=int(sz*.18), fill=(59, 110, 220, 255))\n"
        "    bars = [.25, .45, .7, .9, .65, 1., .55, .8, .4, .2]\n"
        "    n, mg = len(bars), sz*.13\n"
        "    bw = (sz - 2*mg) / (n * 1.7)\n"
        "    gap = (sz - 2*mg) / n\n"
        "    mid = sz / 2\n"
        "    for i, h in enumerate(bars):\n"
        "        x = mg + i*gap + (gap - bw) / 2\n"
        "        bh = h * sz * .36\n"
        "        d.rounded_rectangle([x, mid-bh, x+bw, mid+bh], radius=bw/2, fill=(255, 255, 255, 215))\n"
        "    imgs.append(img)\n"
        "imgs[-1].save('icon.png')\n"
        "imgs[0].save('icon.ico', format='ICO', sizes=[(s,s) for s in sizes], append_images=imgs[1:])\n"
        "print('[OK] Icones generees')\n"
    )
    icons_path.write_text(icon_py, encoding="utf-8")
    ok(f"Script icones genere : {icons_path}")

    # ── Arguments hidden-import + collect-all PyInstaller ──
    hi_lines = " ^\n    ".join(f"--hidden-import {h}" for h in HIDDEN_IMPORTS)
    ca_lines = " ^\n    ".join(f"--collect-all {m}" for m in COLLECT_ALL)

    shortcut_block = ""
    if install:
        shortcut_block = (
            "echo [5/5] Installation des raccourcis...\n"
            "REM Raccourci vers binaire LOCAL (WScript.Shell refuse les chemins reseau/partages)\n"
            f'set "LOCAL_EXE=%LOCAL_DIST%\\{APP_NAME}\\{APP_NAME}.exe"\n'
            f'if exist "%PROJECT_ROOT%dist\\windows\\install_shortcut.ps1" (\n'
            f'    powershell -NoProfile -ExecutionPolicy Bypass ^\n'
            f'      -File "%PROJECT_ROOT%dist\\windows\\install_shortcut.ps1" ^\n'
            f'      -ExePath "%LOCAL_EXE%"\n'
            f') else (\n'
            f'    echo [!!] Script PowerShell introuvable\n'
            f')\n'
        )

    # Venv court pour contourner MAX_PATH avec PySide6 (Microsoft Store Python)
    # C:\ts\.venv\Lib\site-packages\ = 30 chars vs ~140 chars pour AppData\Packages\...
    VENV = "C:\\ts\\.venv"

    # ASCII uniquement dans le bat
    bat = (
        "@echo off\n"
        "setlocal EnableDelayedExpansion\n"
        f"REM build_windows.bat -- {APP_NAME} v{APP_VERSION}\n"
        "REM A executer sur une machine Windows avec Python 3.12+\n"
        "echo.\n"
        f"echo *** Build {APP_NAME} v{APP_VERSION} pour Windows ***\n"
        "echo.\n"
        "\n"
        "REM -- Localisation du projet\n"
        "set \"SCRIPT_DIR=%~dp0\"\n"
        "for %%I in (\"%SCRIPT_DIR%\\..\") do set \"PROJECT_ROOT=%%~fI\\\"\n"
        f"if exist \"%PROJECT_ROOT%{MAIN_SCRIPT}\" goto ROOT_FOUND\n"
        "for %%I in (\"%SCRIPT_DIR%\\..\\..\") do set \"PROJECT_ROOT=%%~fI\\\"\n"
        f"if exist \"%PROJECT_ROOT%{MAIN_SCRIPT}\" goto ROOT_FOUND\n"
        f"echo [XX] {MAIN_SCRIPT} introuvable.\n"
        "pause & exit /b 1\n"
        "\n"
        ":ROOT_FOUND\n"
        "echo [OK] Dossier projet : %PROJECT_ROOT%\n"
        "echo.\n"
        "\n"
        "REM -- Localisation d'un vrai Python (evite le stub Windows Store)\n"
        "set \"REAL_PYTHON=\"\n"
        "\n"
        "REM Essai 1 : py launcher (installe avec Python officiel)\n"
        "where py >nul 2>&1\n"
        "if not errorlevel 1 (\n"
        "    py -3 -c \"import sys; print(sys.executable)\" >nul 2>&1\n"
        "    if not errorlevel 1 (\n"
        "        for /f \"delims=\" %%P in ('py -3 -c \"import sys; print(sys.executable)\"') do set \"REAL_PYTHON=%%P\"\n"
        "    )\n"
        ")\n"
        "\n"
        "REM Essai 2 : emplacements standard Python 3.x\n"
        "if not defined REAL_PYTHON (\n"
        "    for %%V in (313 312 311 310 39) do (\n"
        "        if not defined REAL_PYTHON (\n"
        "            if exist \"C:\\Python%%V\\python.exe\" set \"REAL_PYTHON=C:\\Python%%V\\python.exe\"\n"
        "        )\n"
        "    )\n"
        ")\n"
        "REM Essai 3 : AppData\\Local\\Programs\\Python\n"
        "if not defined REAL_PYTHON (\n"
        "    for %%V in (313 312 311 310 39) do (\n"
        "        if not defined REAL_PYTHON (\n"
        "            set \"_CANDIDATE=%LOCALAPPDATA%\\Programs\\Python\\Python%%V\\python.exe\"\n"
        "            if exist \"!_CANDIDATE!\" set \"REAL_PYTHON=!_CANDIDATE!\"\n"
        "        )\n"
        "    )\n"
        ")\n"
        "\n"
        "if not defined REAL_PYTHON (\n"
        "    echo [XX] Python 3 introuvable. Installez Python depuis https://www.python.org/downloads/\n"
        "    echo [XX] Assurez-vous de cocher \"Add python.exe to PATH\" lors de l'installation.\n"
        "    pause & exit /b 1\n"
        ")\n"
        "echo [OK] Python trouve : %REAL_PYTHON%\n"
        "\n"
        "REM -- Venv a chemin court pour eviter MAX_PATH avec PySide6\n"
        f"set \"VENV={VENV}\"\n"
        "set \"VPYTHON=%VENV%\\Scripts\\python.exe\"\n"
        "set \"VPIP=%VENV%\\Scripts\\pip.exe\"\n"
        "echo [1/5] Preparation venv dans %VENV%...\n"
        "if exist \"%VENV%\" (\n"
        "    \"%VPYTHON%\" -c \"import sys\" >nul 2>&1\n"
        "    if errorlevel 1 (\n"
        "        echo [!!] Venv existant invalide - recreation...\n"
        "        rmdir /S /Q \"%VENV%\"\n"
        "    )\n"
        ")\n"
        "if not exist \"%VPYTHON%\" (\n"
        "    mkdir C:\\ts 2>nul\n"
        "    \"%REAL_PYTHON%\" -m venv \"%VENV%\"\n"
        "    if errorlevel 1 ( echo ERREUR venv & pause & exit /b 1 )\n"
        ")\n"
        "echo [OK] Venv pret.\n"
        "\n"
        "echo [2/5] Installation des dependances Python...\n"
        "\"%VPIP%\" install --quiet --upgrade pip\n"
        "\"%VPIP%\" install --quiet pyinstaller PySide6 numpy soundfile hid Pillow\n"
        "if errorlevel 1 ( echo ERREUR pip & pause & exit /b 1 )\n"
        "\"%VPYTHON%\" -c \"from PySide6.QtMultimedia import QMediaPlayer; print('[OK] PySide6 + QtMultimedia OK')\"\n"
        "if errorlevel 1 ( echo ERREUR PySide6.QtMultimedia & pause & exit /b 1 )\n"
        "\n"
        "echo [3/5] Generation des icones...\n"
        "copy /Y \"%SCRIPT_DIR%_build_icons.py\" \"%PROJECT_ROOT%_build_icons.py\" > nul\n"
        "\"%VPYTHON%\" \"%PROJECT_ROOT%_build_icons.py\"\n"
        "if errorlevel 1 ( echo ERREUR icones & pause & exit /b 1 )\n"
        "del \"%PROJECT_ROOT%_build_icons.py\" > nul 2>&1\n"
        "\n"
        "REM -- Build dans C:\\ts (chemin local court) pour eviter les erreurs reseau/MAX_PATH\n"
        "set \"LOCAL_DIST=C:\\ts\\dist\"\n"
        "set \"LOCAL_BUILD=C:\\ts\\build\"\n"
        "\n"
        "echo [4/5] Compilation PyInstaller (sortie locale C:\\ts\\dist)...\n"
        "\"%VENV%\\Scripts\\pyinstaller.exe\" --onedir --windowed --clean --noconfirm ^\n"
        f"    --name {APP_NAME} ^\n"
        "    --distpath \"%LOCAL_DIST%\" ^\n"
        "    --workpath \"%LOCAL_BUILD%\" ^\n"
        "    --specpath \"%LOCAL_BUILD%\" ^\n"
        "    --icon \"%PROJECT_ROOT%icon.ico\" ^\n"
        f"    {hi_lines} ^\n"
        f"    {ca_lines} ^\n"
        f"    \"%PROJECT_ROOT%{MAIN_SCRIPT}\"\n"
        "if errorlevel 1 ( echo ERREUR PyInstaller & pause & exit /b 1 )\n"
        "\n"
        "REM -- Ajout de hidapi.dll (absent du package pip hid)\n"
        f"set \"HIDAPI_URL=https://github.com/libusb/hidapi/releases/download/hidapi-{HIDAPI_VERSION}/hidapi-win.zip\"\n"
        f"set \"HIDAPI_ZIP=C:\\ts\\hidapi-win.zip\"\n"
        f"set \"HIDAPI_DLL=%LOCAL_DIST%\\{APP_NAME}\\_internal\\hidapi.dll\"\n"
        "if not exist \"%HIDAPI_DLL%\" (\n"
        "    echo    Telechargement hidapi.dll...\n"
        "    powershell -NoProfile -Command \"Invoke-WebRequest -Uri '%HIDAPI_URL%' -OutFile '%HIDAPI_ZIP%'\"\n"
        "    if not errorlevel 1 (\n"
        "        powershell -NoProfile -Command \""
        f"Expand-Archive -Force '%HIDAPI_ZIP%' 'C:\\\\ts\\\\hidapi_tmp'; "
        f"$dll = Get-ChildItem 'C:\\\\ts\\\\hidapi_tmp' -Recurse -Filter hidapi.dll | "
        f"Where-Object {{ $_.FullName -match 'x64' }} | Select-Object -First 1; "
        f"if ($dll) {{ Copy-Item $dll.FullName '%HIDAPI_DLL%'; Write-Host '[OK] hidapi.dll installe' }} "
        f"else {{ Write-Host '[!!] hidapi.dll x64 introuvable dans archive' }}; "
        f"Remove-Item 'C:\\\\ts\\\\hidapi_tmp' -Recurse -Force\"\n"
        "    ) else (\n"
        "        echo [!!] Echec telechargement hidapi.dll - le pedalier ne fonctionnera pas.\n"
        "    )\n"
        ") else (\n"
        "    echo [OK] hidapi.dll deja present.\n"
        ")\n"
        "\n"
        "echo    Copie du binaire vers le dossier projet...\n"
        f"if exist \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\" rmdir /S /Q \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\"\n"
        f"xcopy /E /I /Y \"%LOCAL_DIST%\\{APP_NAME}\" \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\\\"\n"
        "if errorlevel 1 ( echo ERREUR copie xcopy & pause & exit /b 1 )\n"
        f"echo [OK] Binaire copie dans %PROJECT_ROOT%dist\\windows\\{APP_NAME}\\\n"
        "\n"
        + shortcut_block +
        "\n"
        "copy /Y \"%PROJECT_ROOT%icon.ico\" \"C:\\ts\\icon.ico\" > nul\n"
        "\n"
        "echo [6/6] Creation de l'installeur (Inno Setup)...\n"
        "set \"ISCC_EXE=\"\n"
        "if exist \"C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe\" "
        "set \"ISCC_EXE=C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe\"\n"
        "if exist \"C:\\Program Files\\Inno Setup 6\\ISCC.exe\" "
        "set \"ISCC_EXE=C:\\Program Files\\Inno Setup 6\\ISCC.exe\"\n"
        "if not defined ISCC_EXE (\n"
        "    echo [!!] Inno Setup 6 non installe - installeur ignore.\n"
        "    echo [!!] Telecharger gratuitement : https://jrsoftware.org/isdl.php\n"
        "    echo [!!] Puis relancez ce script pour generer l'installeur.\n"
        "    goto BUILD_DONE\n"
        ")\n"
        f"mkdir C:\\ts\\installer 2>nul\n"
        f"echo    Compilation Inno Setup...\n"
        f"\"%ISCC_EXE%\" \"%SCRIPT_DIR%{APP_NAME}.iss\"\n"
        "if errorlevel 1 ( echo [!!] Erreur Inno Setup & goto BUILD_DONE )\n"
        f"echo [OK] Installeur : C:\\ts\\installer\\{APP_NAME}_Setup_v{APP_VERSION}.exe\n"
        "\n"
        ":BUILD_DONE\n"
        "echo.\n"
        f"echo *** Build termine ! ***\n"
        f"echo Binaire    : %PROJECT_ROOT%dist\\windows\\{APP_NAME}\\{APP_NAME}.exe\n"
        f"echo Installeur : C:\\ts\\installer\\{APP_NAME}_Setup_v{APP_VERSION}.exe (si Inno Setup installe)\n"
        "echo.\n"
        "pause\n"
    )

    bat_crlf = bat.replace("\n", "\r\n")
    bat_path.write_bytes(bat_crlf.encode("ascii", errors="replace"))
    ok(f"Script Windows genere : {bat_path}")
    ok(f"Script icones genere  : {icons_path}")
    generate_inno_script()
    warn(f"Sur Windows : lancez dist\\windows\\build_windows.bat depuis la racine du projet")
    warn(f"Inno Setup requis pour l'installeur : https://jrsoftware.org/isdl.php")

# ─── Installeur Inno Setup ────────────────────────────────────────────────────
def generate_inno_script():
    """
    Génère TranscribeStation.iss pour Inno Setup 6.
    Sortie : dist/windows/TranscribeStation.iss  (CRLF + BOM UTF-8)
    Compilation : ISCC.exe TranscribeStation.iss
    Résultat    : C:\\ts\\installer\\TranscribeStation_Setup_v{APP_VERSION}.exe
    """
    # On utilise une string ordinaire pour éviter les conflits entre
    # les accolades Inno Setup {app} et le f-string Python.
    # N = APP_NAME, V = APP_VERSION, A = APP_AUTHOR, U = APP_URL
    N, V, A, U = APP_NAME, APP_VERSION, APP_AUTHOR, APP_URL
    iss = (
        "[Setup]\n"
        f"AppName={N}\n"
        f"AppVersion={V}\n"
        f"AppVerName={N} {V}\n"
        f"AppPublisher={A}\n"
        f"AppPublisherURL={U}\n"
        f"AppSupportURL={U}\n"
        f"DefaultDirName={{localappdata}}\\Programs\\{N}\n"
        f"DefaultGroupName={N}\n"
        "AllowNoIcons=yes\n"
        "OutputDir=C:\\ts\\installer\n"
        f"OutputBaseFilename={N}_Setup_v{V}\n"
        "SetupIconFile=C:\\ts\\icon.ico\n"
        "Compression=lzma2/ultra64\n"
        "SolidCompression=yes\n"
        "WizardStyle=modern\n"
        "WizardSizePercent=120\n"
        "PrivilegesRequired=lowest\n"
        "ArchitecturesAllowed=x64compatible\n"
        "ArchitecturesInstallIn64BitMode=x64compatible\n"
        f"UninstallDisplayIcon={{app}}\\{N}.exe\n"
        f"UninstallDisplayName={N} {V}\n"
        "CloseApplications=yes\n"
        "\n"
        "[Languages]\n"
        'Name: "french"; MessagesFile: "compiler:Languages\\French.isl"\n'
        "\n"
        "[Tasks]\n"
        'Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; '
        'GroupDescription: "{cm:AdditionalIcons}"\n'
        "\n"
        "[Files]\n"
        f'Source: "C:\\ts\\dist\\{N}\\*"; '
        'DestDir: "{app}"; '
        "Flags: ignoreversion recursesubdirs createallsubdirs\n"
        "\n"
        "[Icons]\n"
        f'Name: "{{group}}\\{N}"; Filename: "{{app}}\\{N}.exe"\n'
        f'Name: "{{userdesktop}}\\{N}"; Filename: "{{app}}\\{N}.exe"; Tasks: desktopicon\n'
        "\n"
        "[Run]\n"
        f'Filename: "{{app}}\\{N}.exe"; '
        f'Description: "Lancer {N}"; '
        "Flags: nowait postinstall skipifsilent\n"
        "\n"
        "[UninstallDelete]\n"
        'Type: filesandordirs; Name: "{app}"\n'
        "\n"
        "[Code]\n"
        "// Verifie que l'architecture est x64\n"
        "function InitializeSetup(): Boolean;\n"
        "begin\n"
        "  Result := True;\n"
        "  if not Is64BitInstallMode then begin\n"
        f"    MsgBox('{N} necessite Windows 64 bits.', mbError, MB_OK);\n"
        "    Result := False;\n"
        "  end;\n"
        "end;\n"
    )

    out = DIST_DIR / "windows" / f"{APP_NAME}.iss"
    out.parent.mkdir(parents=True, exist_ok=True)
    # BOM UTF-8 + CRLF obligatoires pour Inno Setup
    iss_crlf = iss.replace("\n", "\r\n")
    out.write_bytes(b"\xef\xbb\xbf" + iss_crlf.encode("utf-8"))
    ok(f"Script Inno Setup généré : {out}")
    return out

# ─── Nettoyage ────────────────────────────────────────────────────────────────
def clean():
    step("🧹  Nettoyage...")
    for d in [BUILD_DIR, DIST_DIR]:
        if d.exists():
            shutil.rmtree(d); ok(f"Supprimé : {d}/")
    for f in Path(".").glob("*.spec"):
        f.unlink(); ok(f"Supprimé : {f}")
    for icon in [ICON_ICO, ICON_PNG]:
        if icon.exists():
            icon.unlink(); ok(f"Supprimé : {icon}")
    ok("Nettoyage terminé.")

# ─── Point d'entrée ───────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description=f"Build + install {APP_NAME}",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=textwrap.dedent("""
          Exemples :
            python build.py               build + raccourcis (auto-detect OS)
            python build.py --linux       build Linux + raccourci GNOME
            python build.py --windows     build Windows + raccourci Menu Démarrer
            python build.py --both        les deux
            python build.py --no-install  build sans créer de raccourcis
            python build.py --uninstall   supprimer le raccourci GNOME
            python build.py --clean       supprimer artefacts build/dist/
        """),
    )
    parser.add_argument("--linux",      action="store_true")
    parser.add_argument("--windows",    action="store_true")
    parser.add_argument("--both",       action="store_true")
    parser.add_argument("--no-install", action="store_true",
                        help="Ne pas installer de raccourci")
    parser.add_argument("--install-only", action="store_true",
                        help="Installer raccourci sans recompiler")
    parser.add_argument("--uninstall",  action="store_true",
                        help="Désinstaller raccourci GNOME")
    parser.add_argument("--clean",      action="store_true")
    args = parser.parse_args()

    os.chdir(Path(__file__).parent)
    do_install = not args.no_install

    if args.clean:
        clean(); return

    if args.uninstall:
        uninstall_linux_shortcut(); return

    if args.install_only:
        binary = Path(f"dist/linux/{APP_NAME}")
        if binary.exists():
            generate_icons()
            install_linux_shortcut(binary)
        else:
            err(f"Binaire introuvable : {binary}")
            err("Lancez d'abord : python build.py --linux")
        return

    if args.both:
        build_linux(install=do_install)
        build_windows(install=do_install)
        return

    if args.windows:
        build_windows(install=do_install); return

    if args.linux or platform.system() == "Linux":
        build_linux(install=do_install)
    elif platform.system() == "Windows":
        build_windows(install=do_install)
    else:
        err(f"Plateforme inconnue : {platform.system()}")
        err("Utilisez --linux ou --windows")

if __name__ == "__main__":
    main()
