#!/usr/bin/env python3
"""Ta machine peut-elle faire tourner le Monteur IA ? Système, mémoire, disque, carte graphique.

Bibliothèque standard seulement : lancé avant toute installation, et copié tel quel dans atelier-montage
(page /verifier du site, avant achat).

    python verifier.py            # rapport lisible, code de sortie 1 si un point bloque
    python verifier.py --json     # même chose pour l'installeur

Seuils PROVISOIRES : à figer avec les mesures sur une machine 8 Go (R3) et les retours des testeurs (phase T).
"""
import ctypes
import json
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path

RAM_MIN_GO = 7.5            # « 8 Go » annoncés ≈ 7,6-7,9 Go visibles
RAM_CONSEILLEE_GO = 16
DISQUE_MIN_GO = 10          # runtime ≈ 3 Go + pack précision ≈ 2 Go + projets
WINDOWS_BUILD_MIN = 17763   # Windows 10 1809 (tar, curl, TLS 1.2 présents)
MACOS_MIN = (12, 0)

OK, KO, INFO = "ok", "bloquant", "info"


def _go(octets: float) -> float:
    return round(octets / 1024 ** 3, 1)


def _fr(x: float) -> str:
    return f"{x:g}".replace(".", ",")


def memoire_go() -> float | None:
    try:
        if os.name == "nt":
            class Etat(ctypes.Structure):
                _fields_ = [("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
                            ("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
                            ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
                            ("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
                            ("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
            e = Etat()
            e.dwLength = ctypes.sizeof(Etat)
            ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(e))
            return _go(e.ullTotalPhys)
        if sys.platform == "darwin":
            return _go(int(subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, check=True).stdout))
        return _go(os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES"))
    except (OSError, ValueError, subprocess.SubprocessError, AttributeError):
        return None


def gpu_nvidia() -> str | None:
    """Nom de la carte NVIDIA (pilote présent), sinon None. macOS : jamais (pas de CUDA)."""
    if sys.platform == "darwin" or not shutil.which("nvidia-smi"):
        return None
    try:
        r = subprocess.run(["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
                           capture_output=True, text=True, timeout=15)
    except (OSError, subprocess.SubprocessError):
        return None
    ligne = r.stdout.strip().splitlines()[0] if r.returncode == 0 and r.stdout.strip() else ""
    return ligne or None


def verifier(dossier: Path | None = None) -> dict:
    """Liste de points {etat, quoi, conseil} + `bloquant` (bool) + `gpu` (nom NVIDIA ou None)."""
    points = []

    def point(etat, quoi, conseil=""):
        points.append({"etat": etat, "quoi": quoi, "conseil": conseil})

    machine = platform.machine().lower()
    if os.name == "nt":
        build = sys.getwindowsversion().build
        if build < WINDOWS_BUILD_MIN:
            point(KO, f"Windows trop ancien (build {build})", "il faut Windows 10 (version 1809 ou plus récente) ou Windows 11")
        elif machine not in ("amd64", "x86_64"):
            point(KO, f"processeur {machine} non pris en charge", "le Monteur IA tourne sur les PC Windows Intel / AMD (x64)")
        else:
            point(OK, f"Windows {'11' if build >= 22000 else '10'} (build {build})")
    elif sys.platform == "darwin":
        v = tuple(int(x) for x in (platform.mac_ver()[0] or "0").split(".")[:2] + ["0"])[:2]
        if v < MACOS_MIN:
            point(KO, f"macOS {platform.mac_ver()[0]} trop ancien", "mets à jour macOS (12 Monterey minimum) dans Réglages > Général > Mise à jour")
        else:
            puce = "Apple Silicon" if machine == "arm64" else "Intel"
            point(OK, f"macOS {platform.mac_ver()[0]} ({puce})")
    else:
        point(INFO, f"{platform.system()} : non pris en charge officiellement (Windows et macOS seulement)")

    ram = memoire_go()
    if ram is None:
        point(INFO, "mémoire non mesurée")
    elif ram < RAM_MIN_GO:
        point(KO, f"mémoire {_fr(ram)} Go", "il faut au moins 8 Go de mémoire vive : la transcription et le rendu ne tiendraient pas")
    elif ram < RAM_CONSEILLEE_GO:
        point(OK, f"mémoire {_fr(ram)} Go (ça marche ; ferme les autres applications pendant un rendu)")
    else:
        point(OK, f"mémoire {_fr(ram)} Go")

    cible = dossier or Path.home()
    try:
        libre = _go(shutil.disk_usage(cible).free)
        if libre < DISQUE_MIN_GO:
            point(KO, f"disque : {_fr(libre)} Go libres", f"libère de la place : il faut {DISQUE_MIN_GO} Go libres sur le disque de ton dossier personnel")
        else:
            point(OK, f"disque : {_fr(libre)} Go libres")
    except OSError:
        point(INFO, "espace disque non mesuré")

    coeurs = os.cpu_count() or 0
    point(INFO if coeurs >= 4 else KO, f"{coeurs} cœurs de processeur",
          "" if coeurs >= 4 else "il faut au moins 4 cœurs : le rendu serait beaucoup trop long")

    gpu = gpu_nvidia()
    if gpu:
        point(OK, f"carte graphique NVIDIA ({gpu}) : transcription ~4x plus rapide")
    elif sys.platform == "darwin" and machine == "arm64":
        point(INFO, "puce Apple : transcription sur le processeur (rapide sur M1 et plus récents)")
    else:
        point(INFO, "pas de carte NVIDIA : transcription sur le processeur (ça marche, plus lent)")

    return {"points": points, "bloquant": any(p["etat"] == KO for p in points), "gpu": gpu,
            "ram_go": ram, "systeme": platform.platform()}


def afficher(res: dict) -> None:
    signes = {OK: "  ✓", KO: "  ✗", INFO: "  ·"}
    print("\nMonteur IA — vérification de la machine\n")
    for p in res["points"]:
        print(f"{signes[p['etat']]} {p['quoi']}" + (f"\n      → {p['conseil']}" if p["conseil"] else ""))
    print("\n" + ("Ta machine est prête pour le Monteur IA." if not res["bloquant"]
                  else "Ta machine ne peut pas faire tourner le Monteur IA en l'état : corrige les points ✗ ci-dessus.") + "\n")


def main() -> None:
    try:
        sys.stdout.reconfigure(encoding="utf-8")
    except (AttributeError, ValueError):
        pass
    res = verifier()
    if "--json" in sys.argv:
        print(json.dumps(res, ensure_ascii=False))
    else:
        afficher(res)
    sys.exit(1 if res["bloquant"] else 0)


if __name__ == "__main__":
    main()
